Age Owner Branch data TLA Line data Source code
1 : : /*-------------------------------------------------------------------------
2 : : *
3 : : * pgpa_output.c
4 : : * produce textual output from the results of a plan tree walk
5 : : *
6 : : * Copyright (c) 2016-2026, PostgreSQL Global Development Group
7 : : *
8 : : * contrib/pg_plan_advice/pgpa_output.c
9 : : *
10 : : *-------------------------------------------------------------------------
11 : : */
12 : :
13 : : #include "postgres.h"
14 : :
15 : : #include "pgpa_output.h"
16 : : #include "pgpa_scan.h"
17 : :
18 : : #include "nodes/parsenodes.h"
19 : : #include "parser/parsetree.h"
20 : : #include "utils/builtins.h"
21 : : #include "utils/lsyscache.h"
22 : :
23 : : /*
24 : : * Context object for textual advice generation.
25 : : *
26 : : * rt_identifiers is the caller-provided array of range table identifiers.
27 : : * See the comments at the top of pgpa_identifier.c for more details.
28 : : *
29 : : * buf is the caller-provided output buffer.
30 : : *
31 : : * wrap_column is the wrap column, so that we don't create output that is
32 : : * too wide. See pgpa_maybe_linebreak() and comments in pgpa_output_advice.
33 : : */
34 : : typedef struct pgpa_output_context
35 : : {
36 : : const char **rid_strings;
37 : : StringInfo buf;
38 : : int wrap_column;
39 : : } pgpa_output_context;
40 : :
41 : : static void pgpa_output_unrolled_join(pgpa_output_context *context,
42 : : pgpa_unrolled_join *join);
43 : : static void pgpa_output_join_member(pgpa_output_context *context,
44 : : pgpa_join_member *member);
45 : : static void pgpa_output_scan_strategy(pgpa_output_context *context,
46 : : pgpa_scan_strategy strategy,
47 : : List *scans);
48 : : static void pgpa_output_relation_name(pgpa_output_context *context, Oid relid);
49 : : static void pgpa_output_query_feature(pgpa_output_context *context,
50 : : pgpa_qf_type type,
51 : : List *query_features);
52 : : static void pgpa_output_simple_strategy(pgpa_output_context *context,
53 : : char *strategy,
54 : : List *relid_sets);
55 : : static void pgpa_output_no_gather(pgpa_output_context *context,
56 : : Bitmapset *relids);
57 : : static void pgpa_output_relations(pgpa_output_context *context, StringInfo buf,
58 : : Bitmapset *relids);
59 : :
60 : : static char *pgpa_cstring_join_strategy(pgpa_join_strategy strategy);
61 : : static char *pgpa_cstring_scan_strategy(pgpa_scan_strategy strategy);
62 : : static char *pgpa_cstring_query_feature_type(pgpa_qf_type type);
63 : :
64 : : static void pgpa_maybe_linebreak(StringInfo buf, int wrap_column);
65 : :
66 : : /*
67 : : * Append query advice to the provided buffer.
68 : : *
69 : : * Before calling this function, 'walker' must be used to iterate over the
70 : : * main plan tree and all subplans from the PlannedStmt.
71 : : *
72 : : * 'rt_identifiers' is a table of unique identifiers, one for each RTI.
73 : : * See pgpa_create_identifiers_for_planned_stmt().
74 : : *
75 : : * Results will be appended to 'buf'.
76 : : */
77 : : void
3 rhaas@postgresql.org 78 :GNC 120 : pgpa_output_advice(StringInfo buf, pgpa_plan_walker_context *walker,
79 : : pgpa_identifier *rt_identifiers)
80 : : {
81 : 120 : Index rtable_length = list_length(walker->pstmt->rtable);
82 : : ListCell *lc;
83 : : pgpa_output_context context;
84 : :
85 : : /* Basic initialization. */
86 : 120 : memset(&context, 0, sizeof(pgpa_output_context));
87 : 120 : context.buf = buf;
88 : :
89 : : /*
90 : : * Convert identifiers to string form. Note that the loop variable here is
91 : : * not an RTI, because RTIs are 1-based. Some RTIs will have no
92 : : * identifier, either because the reloptkind is RTE_JOIN or because that
93 : : * portion of the query didn't make it into the final plan.
94 : : */
95 : 120 : context.rid_strings = palloc0_array(const char *, rtable_length);
96 [ + + ]: 489 : for (int i = 0; i < rtable_length; ++i)
97 [ + + ]: 369 : if (rt_identifiers[i].alias_name != NULL)
98 : 299 : context.rid_strings[i] = pgpa_identifier_string(&rt_identifiers[i]);
99 : :
100 : : /*
101 : : * If the user chooses to use EXPLAIN (PLAN_ADVICE) in an 80-column window
102 : : * from a psql client with default settings, psql will add one space to
103 : : * the left of the output and EXPLAIN will add two more to the left of the
104 : : * advice. Thus, lines of more than 77 characters will wrap. We set the
105 : : * wrap limit to 76 here so that the output won't reach all the way to the
106 : : * very last column of the terminal.
107 : : *
108 : : * Of course, this is fairly arbitrary set of assumptions, and one could
109 : : * well make an argument for a different wrap limit, or for a configurable
110 : : * one.
111 : : */
112 : 120 : context.wrap_column = 76;
113 : :
114 : : /*
115 : : * Each piece of JOIN_ORDER() advice fully describes the join order for a
116 : : * a single unrolled join. Merging is not permitted, because that would
117 : : * change the meaning, e.g. SEQ_SCAN(a b c d) means simply that sequential
118 : : * scans should be used for all of those relations, and is thus equivalent
119 : : * to SEQ_SCAN(a b) SEQ_SCAN(c d), but JOIN_ORDER(a b c d) means that "a"
120 : : * is the driving table which is then joined to "b" then "c" then "d",
121 : : * which is totally different from JOIN_ORDER(a b) and JOIN_ORDER(c d).
122 : : */
123 [ + + + + : 203 : foreach(lc, walker->toplevel_unrolled_joins)
+ + ]
124 : : {
125 : 83 : pgpa_unrolled_join *ujoin = lfirst(lc);
126 : :
127 [ + + ]: 83 : if (buf->len > 0)
128 : 11 : appendStringInfoChar(buf, '\n');
129 : 83 : appendStringInfo(context.buf, "JOIN_ORDER(");
130 : 83 : pgpa_output_unrolled_join(&context, ujoin);
131 : 83 : appendStringInfoChar(context.buf, ')');
132 : 83 : pgpa_maybe_linebreak(context.buf, context.wrap_column);
133 : : }
134 : :
135 : : /* Emit join strategy advice. */
136 [ + + ]: 840 : for (int s = 0; s < NUM_PGPA_JOIN_STRATEGY; ++s)
137 : : {
138 : 720 : char *strategy = pgpa_cstring_join_strategy(s);
139 : :
140 : 720 : pgpa_output_simple_strategy(&context,
141 : : strategy,
142 : : walker->join_strategies[s]);
143 : : }
144 : :
145 : : /*
146 : : * Emit scan strategy advice (but not for ordinary scans, which are
147 : : * definitionally uninteresting).
148 : : */
149 [ + + ]: 1080 : for (int c = 0; c < NUM_PGPA_SCAN_STRATEGY; ++c)
150 [ + + ]: 960 : if (c != PGPA_SCAN_ORDINARY)
151 : 840 : pgpa_output_scan_strategy(&context, c, walker->scans[c]);
152 : :
153 : : /* Emit query feature advice. */
154 [ + + ]: 600 : for (int t = 0; t < NUM_PGPA_QF_TYPES; ++t)
155 : 480 : pgpa_output_query_feature(&context, t, walker->query_features[t]);
156 : :
157 : : /* Emit NO_GATHER advice. */
158 : 120 : pgpa_output_no_gather(&context, walker->no_gather_scans);
159 : 120 : }
160 : :
161 : : /*
162 : : * Output the members of an unrolled join, first the outermost member, and
163 : : * then the inner members one by one, as part of JOIN_ORDER() advice.
164 : : */
165 : : static void
166 : 87 : pgpa_output_unrolled_join(pgpa_output_context *context,
167 : : pgpa_unrolled_join *join)
168 : : {
169 : 87 : pgpa_output_join_member(context, &join->outer);
170 : :
171 [ + + ]: 203 : for (int k = 0; k < join->ninner; ++k)
172 : : {
173 : 116 : pgpa_join_member *member = &join->inner[k];
174 : :
175 : 116 : pgpa_maybe_linebreak(context->buf, context->wrap_column);
176 : 116 : appendStringInfoChar(context->buf, ' ');
177 : 116 : pgpa_output_join_member(context, member);
178 : : }
179 : 87 : }
180 : :
181 : : /*
182 : : * Output a single member of an unrolled join as part of JOIN_ORDER() advice.
183 : : */
184 : : static void
185 : 203 : pgpa_output_join_member(pgpa_output_context *context,
186 : : pgpa_join_member *member)
187 : : {
188 [ + + ]: 203 : if (member->unrolled_join != NULL)
189 : : {
190 : 4 : appendStringInfoChar(context->buf, '(');
191 : 4 : pgpa_output_unrolled_join(context, member->unrolled_join);
192 : 4 : appendStringInfoChar(context->buf, ')');
193 : : }
194 : : else
195 : : {
196 : 199 : pgpa_scan *scan = member->scan;
197 : :
198 [ - + ]: 199 : Assert(scan != NULL);
199 [ + + ]: 199 : if (bms_membership(scan->relids) == BMS_SINGLETON)
200 : 198 : pgpa_output_relations(context, context->buf, scan->relids);
201 : : else
202 : : {
203 : 1 : appendStringInfoChar(context->buf, '{');
204 : 1 : pgpa_output_relations(context, context->buf, scan->relids);
205 : 1 : appendStringInfoChar(context->buf, '}');
206 : : }
207 : : }
208 : 203 : }
209 : :
210 : : /*
211 : : * Output advice for a List of pgpa_scan objects.
212 : : *
213 : : * All the scans must use the strategy specified by the "strategy" argument.
214 : : */
215 : : static void
216 : 840 : pgpa_output_scan_strategy(pgpa_output_context *context,
217 : : pgpa_scan_strategy strategy,
218 : : List *scans)
219 : : {
220 : 840 : bool first = true;
221 : :
222 [ + + ]: 840 : if (scans == NIL)
223 : 694 : return;
224 : :
225 [ + + ]: 146 : if (context->buf->len > 0)
226 : 98 : appendStringInfoChar(context->buf, '\n');
227 : 146 : appendStringInfo(context->buf, "%s(",
228 : : pgpa_cstring_scan_strategy(strategy));
229 : :
230 [ + - + + : 541 : foreach_ptr(pgpa_scan, scan, scans)
+ + ]
231 : : {
232 : 249 : Plan *plan = scan->plan;
233 : :
234 [ + + ]: 249 : if (first)
235 : 146 : first = false;
236 : : else
237 : : {
238 : 103 : pgpa_maybe_linebreak(context->buf, context->wrap_column);
239 : 103 : appendStringInfoChar(context->buf, ' ');
240 : : }
241 : :
242 : : /* Output the relation identifiers. */
243 [ + + ]: 249 : if (bms_membership(scan->relids) == BMS_SINGLETON)
244 : 244 : pgpa_output_relations(context, context->buf, scan->relids);
245 : : else
246 : : {
247 : 5 : appendStringInfoChar(context->buf, '(');
248 : 5 : pgpa_output_relations(context, context->buf, scan->relids);
249 : 5 : appendStringInfoChar(context->buf, ')');
250 : : }
251 : :
252 : : /* For index or index-only scans, output index information. */
253 [ + + ]: 249 : if (strategy == PGPA_SCAN_INDEX)
254 : : {
255 [ - + ]: 65 : Assert(IsA(plan, IndexScan));
256 : 65 : pgpa_maybe_linebreak(context->buf, context->wrap_column);
257 : 65 : appendStringInfoChar(context->buf, ' ');
258 : 65 : pgpa_output_relation_name(context, ((IndexScan *) plan)->indexid);
259 : : }
260 [ + + ]: 184 : else if (strategy == PGPA_SCAN_INDEX_ONLY)
261 : : {
262 [ - + ]: 7 : Assert(IsA(plan, IndexOnlyScan));
263 : 7 : pgpa_maybe_linebreak(context->buf, context->wrap_column);
264 : 7 : appendStringInfoChar(context->buf, ' ');
265 : 7 : pgpa_output_relation_name(context,
266 : : ((IndexOnlyScan *) plan)->indexid);
267 : : }
268 : : }
269 : :
270 : 146 : appendStringInfoChar(context->buf, ')');
271 : 146 : pgpa_maybe_linebreak(context->buf, context->wrap_column);
272 : : }
273 : :
274 : : /*
275 : : * Output a schema-qualified relation name.
276 : : */
277 : : static void
278 : 72 : pgpa_output_relation_name(pgpa_output_context *context, Oid relid)
279 : : {
280 : 72 : Oid nspoid = get_rel_namespace(relid);
281 : 72 : char *relnamespace = get_namespace_name_or_temp(nspoid);
282 : 72 : char *relname = get_rel_name(relid);
283 : :
284 : 72 : appendStringInfoString(context->buf, quote_identifier(relnamespace));
285 : 72 : appendStringInfoChar(context->buf, '.');
286 : 72 : appendStringInfoString(context->buf, quote_identifier(relname));
287 : 72 : }
288 : :
289 : : /*
290 : : * Output advice for a List of pgpa_query_feature objects.
291 : : *
292 : : * All features must be of the type specified by the "type" argument.
293 : : */
294 : : static void
295 : 480 : pgpa_output_query_feature(pgpa_output_context *context, pgpa_qf_type type,
296 : : List *query_features)
297 : : {
298 : 480 : bool first = true;
299 : :
300 [ + + ]: 480 : if (query_features == NIL)
301 : 453 : return;
302 : :
303 [ + - ]: 27 : if (context->buf->len > 0)
304 : 27 : appendStringInfoChar(context->buf, '\n');
305 : 27 : appendStringInfo(context->buf, "%s(",
306 : : pgpa_cstring_query_feature_type(type));
307 : :
308 [ + - + + : 83 : foreach_ptr(pgpa_query_feature, qf, query_features)
+ + ]
309 : : {
310 [ + + ]: 29 : if (first)
311 : 27 : first = false;
312 : : else
313 : : {
314 : 2 : pgpa_maybe_linebreak(context->buf, context->wrap_column);
315 : 2 : appendStringInfoChar(context->buf, ' ');
316 : : }
317 : :
318 [ + + ]: 29 : if (bms_membership(qf->relids) == BMS_SINGLETON)
319 : 23 : pgpa_output_relations(context, context->buf, qf->relids);
320 : : else
321 : : {
322 : 6 : appendStringInfoChar(context->buf, '(');
323 : 6 : pgpa_output_relations(context, context->buf, qf->relids);
324 : 6 : appendStringInfoChar(context->buf, ')');
325 : : }
326 : : }
327 : :
328 : 27 : appendStringInfoChar(context->buf, ')');
329 : 27 : pgpa_maybe_linebreak(context->buf, context->wrap_column);
330 : : }
331 : :
332 : : /*
333 : : * Output "simple" advice for a List of Bitmapset objects each of which
334 : : * contains one or more RTIs.
335 : : *
336 : : * By simple, we just mean that the advice emitted follows the most
337 : : * straightforward pattern: the strategy name, followed by a list of items
338 : : * separated by spaces and surrounded by parentheses. Individual items in
339 : : * the list are a single relation identifier for a Bitmapset that contains
340 : : * just one member, or a sub-list again separated by spaces and surrounded
341 : : * by parentheses for a Bitmapset with multiple members. Bitmapsets with
342 : : * no members probably shouldn't occur here, but if they do they'll be
343 : : * rendered as an empty sub-list.
344 : : */
345 : : static void
346 : 720 : pgpa_output_simple_strategy(pgpa_output_context *context, char *strategy,
347 : : List *relid_sets)
348 : : {
349 : 720 : bool first = true;
350 : :
351 [ + + ]: 720 : if (relid_sets == NIL)
352 : 631 : return;
353 : :
354 [ + - ]: 89 : if (context->buf->len > 0)
355 : 89 : appendStringInfoChar(context->buf, '\n');
356 : 89 : appendStringInfo(context->buf, "%s(", strategy);
357 : :
358 [ + - + + : 294 : foreach_node(Bitmapset, relids, relid_sets)
+ + ]
359 : : {
360 [ + + ]: 116 : if (first)
361 : 89 : first = false;
362 : : else
363 : : {
364 : 27 : pgpa_maybe_linebreak(context->buf, context->wrap_column);
365 : 27 : appendStringInfoChar(context->buf, ' ');
366 : : }
367 : :
368 [ + + ]: 116 : if (bms_membership(relids) == BMS_SINGLETON)
369 : 112 : pgpa_output_relations(context, context->buf, relids);
370 : : else
371 : : {
372 : 4 : appendStringInfoChar(context->buf, '(');
373 : 4 : pgpa_output_relations(context, context->buf, relids);
374 : 4 : appendStringInfoChar(context->buf, ')');
375 : : }
376 : : }
377 : :
378 : 89 : appendStringInfoChar(context->buf, ')');
379 : 89 : pgpa_maybe_linebreak(context->buf, context->wrap_column);
380 : : }
381 : :
382 : : /*
383 : : * Output NO_GATHER advice for all relations not appearing beneath any
384 : : * Gather or Gather Merge node.
385 : : */
386 : : static void
387 : 120 : pgpa_output_no_gather(pgpa_output_context *context, Bitmapset *relids)
388 : : {
389 [ + + ]: 120 : if (relids == NULL)
390 : 8 : return;
391 [ + - ]: 112 : if (context->buf->len > 0)
392 : 112 : appendStringInfoChar(context->buf, '\n');
393 : 112 : appendStringInfoString(context->buf, "NO_GATHER(");
394 : 112 : pgpa_output_relations(context, context->buf, relids);
395 : 112 : appendStringInfoChar(context->buf, ')');
396 : : }
397 : :
398 : : /*
399 : : * Output the identifiers for each RTI in the provided set.
400 : : *
401 : : * Identifiers are separated by spaces, and a line break is possible after
402 : : * each one.
403 : : */
404 : : static void
405 : 705 : pgpa_output_relations(pgpa_output_context *context, StringInfo buf,
406 : : Bitmapset *relids)
407 : : {
408 : 705 : int rti = -1;
409 : 705 : bool first = true;
410 : :
411 [ + + ]: 1562 : while ((rti = bms_next_member(relids, rti)) >= 0)
412 : : {
413 : 857 : const char *rid_string = context->rid_strings[rti - 1];
414 : :
415 [ - + ]: 857 : if (rid_string == NULL)
3 rhaas@postgresql.org 416 [ # # ]:UNC 0 : elog(ERROR, "no identifier for RTI %d", rti);
417 : :
3 rhaas@postgresql.org 418 [ + + ]:GNC 857 : if (first)
419 : : {
420 : 705 : first = false;
421 : 705 : appendStringInfoString(buf, rid_string);
422 : : }
423 : : else
424 : : {
425 : 152 : pgpa_maybe_linebreak(buf, context->wrap_column);
426 : 152 : appendStringInfo(buf, " %s", rid_string);
427 : : }
428 : : }
429 : 705 : }
430 : :
431 : : /*
432 : : * Get a C string that corresponds to the specified join strategy.
433 : : */
434 : : static char *
435 : 720 : pgpa_cstring_join_strategy(pgpa_join_strategy strategy)
436 : : {
437 [ + + + + : 720 : switch (strategy)
+ + - ]
438 : : {
439 : 120 : case JSTRAT_MERGE_JOIN_PLAIN:
440 : 120 : return "MERGE_JOIN_PLAIN";
441 : 120 : case JSTRAT_MERGE_JOIN_MATERIALIZE:
442 : 120 : return "MERGE_JOIN_MATERIALIZE";
443 : 120 : case JSTRAT_NESTED_LOOP_PLAIN:
444 : 120 : return "NESTED_LOOP_PLAIN";
445 : 120 : case JSTRAT_NESTED_LOOP_MATERIALIZE:
446 : 120 : return "NESTED_LOOP_MATERIALIZE";
447 : 120 : case JSTRAT_NESTED_LOOP_MEMOIZE:
448 : 120 : return "NESTED_LOOP_MEMOIZE";
449 : 120 : case JSTRAT_HASH_JOIN:
450 : 120 : return "HASH_JOIN";
451 : : }
452 : :
3 rhaas@postgresql.org 453 :UNC 0 : pg_unreachable();
454 : : return NULL;
455 : : }
456 : :
457 : : /*
458 : : * Get a C string that corresponds to the specified scan strategy.
459 : : */
460 : : static char *
3 rhaas@postgresql.org 461 :GNC 146 : pgpa_cstring_scan_strategy(pgpa_scan_strategy strategy)
462 : : {
463 [ - + + - : 146 : switch (strategy)
+ + + +
- ]
464 : : {
3 rhaas@postgresql.org 465 :UNC 0 : case PGPA_SCAN_ORDINARY:
466 : 0 : return "ORDINARY_SCAN";
3 rhaas@postgresql.org 467 :GNC 78 : case PGPA_SCAN_SEQ:
468 : 78 : return "SEQ_SCAN";
469 : 3 : case PGPA_SCAN_BITMAP_HEAP:
470 : 3 : return "BITMAP_HEAP_SCAN";
3 rhaas@postgresql.org 471 :UNC 0 : case PGPA_SCAN_FOREIGN:
472 : 0 : return "FOREIGN_JOIN";
3 rhaas@postgresql.org 473 :GNC 47 : case PGPA_SCAN_INDEX:
474 : 47 : return "INDEX_SCAN";
475 : 7 : case PGPA_SCAN_INDEX_ONLY:
476 : 7 : return "INDEX_ONLY_SCAN";
477 : 7 : case PGPA_SCAN_PARTITIONWISE:
478 : 7 : return "PARTITIONWISE";
479 : 4 : case PGPA_SCAN_TID:
480 : 4 : return "TID_SCAN";
481 : : }
482 : :
3 rhaas@postgresql.org 483 :UNC 0 : pg_unreachable();
484 : : return NULL;
485 : : }
486 : :
487 : : /*
488 : : * Get a C string that corresponds to the query feature type.
489 : : */
490 : : static char *
3 rhaas@postgresql.org 491 :GNC 27 : pgpa_cstring_query_feature_type(pgpa_qf_type type)
492 : : {
493 [ + + + + : 27 : switch (type)
- ]
494 : : {
495 : 6 : case PGPAQF_GATHER:
496 : 6 : return "GATHER";
497 : 7 : case PGPAQF_GATHER_MERGE:
498 : 7 : return "GATHER_MERGE";
499 : 5 : case PGPAQF_SEMIJOIN_NON_UNIQUE:
500 : 5 : return "SEMIJOIN_NON_UNIQUE";
501 : 9 : case PGPAQF_SEMIJOIN_UNIQUE:
502 : 9 : return "SEMIJOIN_UNIQUE";
503 : : }
504 : :
505 : :
3 rhaas@postgresql.org 506 :UNC 0 : pg_unreachable();
507 : : return NULL;
508 : : }
509 : :
510 : : /*
511 : : * Insert a line break into the StringInfoData, if needed.
512 : : *
513 : : * If wrap_column is zero or negative, this does nothing. Otherwise, we
514 : : * consider inserting a newline. We only insert a newline if the length of
515 : : * the last line in the buffer exceeds wrap_column, and not if we'd be
516 : : * inserting a newline at or before the beginning of the current line.
517 : : *
518 : : * The position at which the newline is inserted is simply wherever the
519 : : * buffer ended the last time this function was called. In other words,
520 : : * the caller is expected to call this function every time we reach a good
521 : : * place for a line break.
522 : : */
523 : : static void
3 rhaas@postgresql.org 524 :GNC 817 : pgpa_maybe_linebreak(StringInfo buf, int wrap_column)
525 : : {
526 : : char *trailing_nl;
527 : : int line_start;
528 : : int save_cursor;
529 : :
530 : : /* If line wrapping is disabled, exit quickly. */
531 [ - + ]: 817 : if (wrap_column <= 0)
3 rhaas@postgresql.org 532 :UNC 0 : return;
533 : :
534 : : /*
535 : : * Set line_start to the byte offset within buf->data of the first
536 : : * character of the current line, where the current line means the last
537 : : * one in the buffer. Note that line_start could be the offset of the
538 : : * trailing '\0' if the last character in the buffer is a line break.
539 : : */
3 rhaas@postgresql.org 540 :GNC 817 : trailing_nl = strrchr(buf->data, '\n');
541 [ + + ]: 817 : if (trailing_nl == NULL)
542 : 243 : line_start = 0;
543 : : else
544 : 574 : line_start = (trailing_nl - buf->data) + 1;
545 : :
546 : : /*
547 : : * Remember that the current end of the buffer is a potential location to
548 : : * insert a line break on a future call to this function.
549 : : */
550 : 817 : save_cursor = buf->cursor;
551 : 817 : buf->cursor = buf->len;
552 : :
553 : : /* If we haven't passed the wrap column, we don't need a newline. */
554 [ + + ]: 817 : if (buf->len - line_start <= wrap_column)
555 : 797 : return;
556 : :
557 : : /*
558 : : * It only makes sense to insert a newline at a position later than the
559 : : * beginning of the current line.
560 : : */
561 [ - + ]: 20 : if (save_cursor <= line_start)
3 rhaas@postgresql.org 562 :UNC 0 : return;
563 : :
564 : : /* Insert a newline at the previous cursor location. */
3 rhaas@postgresql.org 565 :GNC 20 : enlargeStringInfo(buf, 1);
566 : 20 : memmove(&buf->data[save_cursor] + 1, &buf->data[save_cursor],
567 : 20 : buf->len - save_cursor);
568 : 20 : ++buf->cursor;
569 : 20 : buf->data[++buf->len] = '\0';
570 : 20 : buf->data[save_cursor] = '\n';
571 : : }
|