LCOV - code coverage report
Current view: top level - contrib/pg_plan_advice - pgpa_output.c (source / functions) Coverage Total Hit
Test: PostgreSQL 19devel Lines: 95.1 % 206 196
Test Date: 2026-03-16 00:15:06 Functions: 100.0 % 13 13
Legend: Lines:     hit not hit

            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
      78          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              :         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              :             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              :             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)
     416            0 :             elog(ERROR, "no identifier for RTI %d", rti);
     417              : 
     418          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              : 
     453            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 *
     461          146 : pgpa_cstring_scan_strategy(pgpa_scan_strategy strategy)
     462              : {
     463          146 :     switch (strategy)
     464              :     {
     465            0 :         case PGPA_SCAN_ORDINARY:
     466            0 :             return "ORDINARY_SCAN";
     467           78 :         case PGPA_SCAN_SEQ:
     468           78 :             return "SEQ_SCAN";
     469            3 :         case PGPA_SCAN_BITMAP_HEAP:
     470            3 :             return "BITMAP_HEAP_SCAN";
     471            0 :         case PGPA_SCAN_FOREIGN:
     472            0 :             return "FOREIGN_JOIN";
     473           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              : 
     483            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 *
     491           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              : 
     506            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
     524          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)
     532            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              :      */
     540          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)
     562            0 :         return;
     563              : 
     564              :     /* Insert a newline at the previous cursor location. */
     565           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              : }
        

Generated by: LCOV version 2.0-1