OSDN Git Service

Add new explain properties.
[pgstoreplans/pg_store_plans.git] / pgsp_json_text.c
1 #include "postgres.h"
2 #include "miscadmin.h"
3 #include "nodes/nodes.h"
4 #include "nodes/bitmapset.h"
5 #include "utils/json.h"
6 #include "utils/jsonapi.h"
7 #include "utils/builtins.h"
8
9 #include "pgsp_json_text.h"
10 #include "pgsp_json_int.h"
11
12 static void clear_nodeval(node_vals *vals);
13 static void print_current_node(pgspParserContext *ctx);
14 static void print_current_trig_node(pgspParserContext *ctx);
15 static void print_prop(StringInfo s, char *prepstr,
16                                            const char *prop, int leve, int exind);
17 static void print_prop_if_exists(StringInfo s, char *prepstr,
18                                                                  const char *prop, int leve, int exind);
19 static void print_prop_if_nz(StringInfo s, char *prepstr,
20                                                          const char *prop, int leve, int exind);
21 static void json_text_objstart(void *state);
22 static void json_text_objend(void *state);
23 static void json_text_ofstart(void *state, char *fname, bool isnull);
24 static void json_text_ofend(void *state, char *fname, bool isnull);
25 static void json_text_scalar(void *state, char *token, JsonTokenType tokentype);
26
27 /* Parser callbacks for plan textization */
28 SETTERDECL(node_type)
29 {
30         word_table *p;
31
32         vals->node_type = val;
33         vals->nodetag = T_Invalid;
34
35         p = search_word_table(nodetypes, val, PGSP_JSON_TEXTIZE);
36         if (p)
37         {
38                 vals->node_type = (p->textname ? p->textname : p->longname);
39                 vals->nodetag = p->tag;
40         }
41 }
42
43 SETTERDECL(output)
44 {
45         if(!vals->output)
46         {
47                 vals->output = makeStringInfo();
48                 appendStringInfoString(vals->output, val);
49         }
50         else
51         {
52                 appendStringInfoString(vals->output, ", ");
53                 appendStringInfoString(vals->output, val);
54         }
55 }
56 SETTERDECL(strategy)
57 {
58         word_table *p;
59         
60         p = search_word_table(strategies, val, PGSP_JSON_TEXTIZE);
61
62         switch (vals->nodetag)
63         {
64         case T_Agg:
65                 switch (p->tag)
66                 {
67                 case S_Hashed:
68                         vals->node_type = "HashAggregate"; break;
69                 case S_Sorted:
70                         vals->node_type = "GroupAggregate"; break;
71                 default:
72                         break;
73                 }
74                 break;
75
76         case T_SetOp:
77                 if (p->tag == S_Hashed)
78                         vals->node_type = "HashSetOp";
79                 break;
80
81         default:
82                 break;
83         }
84 }
85 CONVERSION_SETTER(scan_dir, conv_scandir);
86 SQLQUOTE_SETTER(obj_name);
87 SQLQUOTE_SETTER(alias);
88 SQLQUOTE_SETTER(schema_name);
89 DEFAULT_SETTER(merge_cond);
90 CONVERSION_SETTER(join_type, conv_jointype);
91 CONVERSION_SETTER(setopcommand, conv_setsetopcommand);
92 CONVERSION_SETTER(sort_method, conv_sortmethod);
93 DEFAULT_SETTER(sort_key);
94 SQLQUOTE_SETTER(index_name);
95 DEFAULT_SETTER(startup_cost);
96 DEFAULT_SETTER(total_cost);
97 DEFAULT_SETTER(plan_rows);
98 DEFAULT_SETTER(plan_width);
99 DEFAULT_SETTER(sort_space_used);
100 CONVERSION_SETTER(sort_space_type, conv_sortspacetype);
101 DEFAULT_SETTER(filter);
102 DEFAULT_SETTER(join_filter);
103 DEFAULT_SETTER(func_call);
104 DEFAULT_SETTER(index_cond);
105 DEFAULT_SETTER(recheck_cond);
106 CONVERSION_SETTER(operation, conv_operation);
107 DEFAULT_SETTER(subplan_name);
108 DEFAULT_SETTER(hash_cond);
109 DEFAULT_SETTER(tid_cond);
110 DEFAULT_SETTER(filter_removed);
111 DEFAULT_SETTER(idxrchk_removed);
112 DEFAULT_SETTER(peak_memory_usage);
113 DEFAULT_SETTER(org_hash_batches);
114 DEFAULT_SETTER(hash_batches);
115 DEFAULT_SETTER(hash_buckets);
116 DEFAULT_SETTER(actual_startup_time);
117 DEFAULT_SETTER(actual_total_time);
118 DEFAULT_SETTER(actual_rows);
119 DEFAULT_SETTER(actual_loops);
120 DEFAULT_SETTER(heap_fetches);
121 DEFAULT_SETTER(shared_hit_blks);
122 DEFAULT_SETTER(shared_read_blks);
123 DEFAULT_SETTER(shared_dirtied_blks);
124 DEFAULT_SETTER(shared_written_blks);
125 DEFAULT_SETTER(local_hit_blks);
126 DEFAULT_SETTER(local_read_blks);
127 DEFAULT_SETTER(local_dirtied_blks);
128 DEFAULT_SETTER(local_written_blks);
129 DEFAULT_SETTER(temp_read_blks);
130 DEFAULT_SETTER(temp_written_blks);
131 DEFAULT_SETTER(io_read_time);
132 DEFAULT_SETTER(io_write_time);
133 SQLQUOTE_SETTER(trig_name);
134 SQLQUOTE_SETTER(trig_relation);
135 DEFAULT_SETTER(trig_time);
136 DEFAULT_SETTER(trig_calls);
137 DEFAULT_SETTER(plan_time);
138 DEFAULT_SETTER(exec_time);
139 DEFAULT_SETTER(exact_heap_blks);
140 DEFAULT_SETTER(lossy_heap_blks);
141 DEFAULT_SETTER(joinfilt_removed);
142
143 #define ISZERO(s) (!s || strcmp(s, "0") == 0 || strcmp(s, "0.000") == 0 )
144 #define HASSTRING(s) (s && strlen(s) > 0)
145 #define TEXT_LEVEL_STEP 6
146 #define TEXT_INDENT_OFFSET 2
147 #define TEXT_INDENT_BASE(l, e)                                                                                  \
148         ((l < 2) ? 0 : (TEXT_LEVEL_STEP * (l - 2) + TEXT_INDENT_OFFSET) + e)
149 #define TEXT_INDENT_DETAILS(l, e)                                       \
150         (TEXT_INDENT_BASE(l, e) + ((l < 2) ? 2 : 6))
151
152 static void
153 print_obj_name(pgspParserContext *ctx)
154 {
155         node_vals *v = ctx->nodevals;
156         StringInfo s = ctx->dest;
157         bool on_written = false;
158
159         if (HASSTRING(v->obj_name))
160         {
161                 on_written = true;
162                 appendStringInfoString(s, " on ");
163                 if (HASSTRING(v->schema_name))
164                 {
165                         appendStringInfoString(s, v->schema_name);
166                         appendStringInfoChar(s, '.');
167                 }
168                 appendStringInfoString(s, v->obj_name);
169         }
170         if (HASSTRING(v->alias) &&
171                 (!HASSTRING(v->obj_name) || strcmp(v->obj_name, v->alias) != 0))
172         {
173                 if (!on_written)
174                         appendStringInfoString(s, " on ");
175                 else
176                         appendStringInfoChar(s, ' ');
177                 appendStringInfoString(s, v->alias);
178         }
179 }
180
181
182 static void
183 print_prop(StringInfo s, char *prepstr,
184                    const char *prop, int level, int exind)
185 {
186         if (level > 0)
187         {
188                 appendStringInfoString(s, "\n");
189                 appendStringInfoSpaces(s, TEXT_INDENT_DETAILS(level, exind));
190         }
191         appendStringInfoString(s, prepstr);
192         appendStringInfoString(s, prop);
193 }
194
195 static void
196 print_prop_if_exists(StringInfo s, char *prepstr,
197                                          const char *prop, int level, int exind)
198 {
199         if (HASSTRING(prop))
200                 print_prop(s, prepstr, prop, level, exind);
201 }
202
203 static void
204 print_prop_if_nz(StringInfo s, char *prepstr,
205                                  const char *prop, int level, int exind)
206 {
207         if (!ISZERO(prop))
208                 print_prop(s, prepstr, prop, level, exind);
209 }
210
211 static void 
212 print_current_node(pgspParserContext *ctx)
213 {
214         node_vals *v = ctx->nodevals;
215         StringInfo s = ctx->dest;
216         int level = ctx->level - 1;
217         bool comma = false;
218         int exind = 0;
219
220         if (v->node_type == T_Invalid)
221                 return;
222
223         if (s->len > 0)
224                 appendStringInfoString(s, "\n");
225         appendStringInfoSpaces(s, TEXT_INDENT_BASE(level, exind));
226
227         if (HASSTRING(v->subplan_name))
228         {
229                 appendStringInfoString(s, v->subplan_name);
230                 appendStringInfoString(s, "\n");
231                 exind = 2;
232                 appendStringInfoSpaces(s, TEXT_INDENT_BASE(level, exind));
233         }
234         
235         if (level > 1)
236                 appendStringInfoString(s, "->  ");
237
238         switch (v->nodetag)
239         {
240         case T_ModifyTable:
241         case T_SeqScan:
242         case T_BitmapHeapScan:
243         case T_TidScan:
244         case T_SubqueryScan:
245         case T_FunctionScan:
246         case T_ValuesScan:
247         case T_CteScan:
248         case T_WorkTableScan:
249         case T_ForeignScan:
250                 if (v->nodetag == T_ModifyTable)
251                         appendStringInfoString(s, v->operation);
252                 else
253                         appendStringInfoString(s, v->node_type);
254
255                 print_obj_name(ctx);
256                 break;
257
258         case T_IndexScan:
259         case T_IndexOnlyScan:
260         case T_BitmapIndexScan:
261                 appendStringInfoString(s, v->node_type);
262                 print_prop_if_exists(s, " ", v->scan_dir, 0, 0);
263                 print_prop_if_exists(s, " using ", v->index_name, 0, 0);
264                 print_obj_name(ctx);
265                 break;
266
267         case T_NestLoop:
268         case T_MergeJoin:
269         case T_HashJoin:
270                 appendStringInfoString(s, v->node_type);
271                 if (v->join_type && strcmp(v->join_type, "Inner") != 0)
272                 {
273                         appendStringInfoChar(s, ' ');
274                         appendStringInfoString(s, v->join_type);
275                 }
276                 if (v->nodetag != T_NestLoop)
277                         appendStringInfoString(s, " Join");
278                 break;
279
280         case T_SetOp:
281                 appendStringInfoString(s, v->node_type);
282                 print_prop_if_exists(s, " ", v->setopcommand, 0, 0);
283                 break;
284
285         default:
286                 appendStringInfoString(s, v->node_type);
287                 break;
288         }
289         
290         if (!ISZERO(v->startup_cost) &&
291                 !ISZERO(v->total_cost) &&
292                 HASSTRING(v->plan_rows) &&
293                 HASSTRING(v->plan_width))
294         {
295                 appendStringInfoString(s, "  (cost=");
296                 appendStringInfoString(s, v->startup_cost);
297                 appendStringInfoString(s, "..");
298                 appendStringInfoString(s, v->total_cost);
299                 appendStringInfoString(s, " rows=");
300                 appendStringInfoString(s, v->plan_rows);
301                 appendStringInfoString(s, " width=");
302                 appendStringInfoString(s, v->plan_width);
303                 appendStringInfoString(s, ")");
304         }
305
306         if (HASSTRING(v->actual_loops) && ISZERO(v->actual_loops))
307                 appendStringInfoString(s, " (never executed)");
308         else if (HASSTRING(v->actual_rows) &&
309                          HASSTRING(v->actual_loops) &&
310                          HASSTRING(v->actual_startup_time) &&
311                          HASSTRING(v->actual_total_time))
312         {
313                 appendStringInfoString(s, " (actual ");
314                 appendStringInfoString(s, "time=");
315                 appendStringInfoString(s, v->actual_startup_time);
316                 appendStringInfoString(s, "..");
317                 appendStringInfoString(s, v->actual_total_time);
318                 appendStringInfoString(s, " ");
319
320                 appendStringInfoString(s, "rows=");
321                 appendStringInfoString(s, v->actual_rows);
322
323                 appendStringInfoString(s, " loops=");
324                 appendStringInfoString(s, v->actual_loops);
325
326                 appendStringInfoString(s, ")");
327         }
328
329         if (v->output)
330         {
331                 appendStringInfoString(s, "\n");
332                 appendStringInfoSpaces(s, TEXT_INDENT_DETAILS(level, exind));
333                 appendStringInfoString(s, "Output: ");
334                 appendStringInfoString(s, v->output->data);
335         }
336         print_prop_if_exists(s, "Merge Cond: ", v->merge_cond, level, exind);
337         print_prop_if_exists(s, "Hash Cond: " , v->hash_cond, level, exind);
338         print_prop_if_exists(s, "Tid Cond: " , v->tid_cond, level, exind);
339         print_prop_if_exists(s, "Join Filter: " , v->join_filter, level, exind);
340         print_prop_if_exists(s, "Index Cond: " , v->index_cond, level, exind);
341         print_prop_if_exists(s, "Recheck Cond: ", v->recheck_cond, level, exind);
342         print_prop_if_exists(s, "Sort Key: ", v->sort_key, level, exind);
343
344         if (HASSTRING(v->sort_method))
345         {
346                 appendStringInfoString(s, "\n");
347                 appendStringInfoSpaces(s, TEXT_INDENT_DETAILS(level, exind));
348                 appendStringInfoString(s, "Sort Method: ");
349                 appendStringInfoString(s, v->sort_method);
350
351                 if (HASSTRING(v->sort_space_type) &&
352                         HASSTRING(v->sort_space_used))
353                 {
354                         appendStringInfoString(s, "  ");
355                         appendStringInfoString(s, v->sort_space_type);
356                         appendStringInfoString(s, ": ");
357                         appendStringInfoString(s, v->sort_space_used);
358                         appendStringInfoString(s, "kB");
359                 }
360         }
361
362         print_prop_if_exists(s, "Function Call: ", v->func_call, level, exind);
363         print_prop_if_exists(s, "Filter: ", v->filter, level, exind);
364         print_prop_if_nz(s, "Rows Removed by Filter: ",
365                                                  v->filter_removed, level, exind);
366         print_prop_if_nz(s, "Rows Removed by Index Recheck: ",
367                                                  v->idxrchk_removed, level, exind);
368         print_prop_if_nz(s, "Rows Removed by Join Filter: ",
369                                                  v->joinfilt_removed, level, exind);
370
371         if (HASSTRING(v->exact_heap_blks) ||
372                 HASSTRING(v->lossy_heap_blks))
373         {
374                 appendStringInfoString(s, "\n");
375                 appendStringInfoSpaces(s, TEXT_INDENT_DETAILS(level, exind));
376                 appendStringInfoString(s, "Heap Blocks:");
377                 print_prop_if_nz(s, " exact=", v->exact_heap_blks, 0, exind);
378                 print_prop_if_nz(s, " lossy=", v->lossy_heap_blks, 0, exind);
379         }
380
381         if (!ISZERO(v->hash_buckets))
382         {
383                 appendStringInfoString(s, "\n");
384                 appendStringInfoSpaces(s, TEXT_INDENT_DETAILS(level, exind));
385                 appendStringInfoString(s, "Buckets: ");
386                 appendStringInfoString(s, v->hash_buckets);
387                 if (!ISZERO(v->hash_batches))
388                 {
389                         appendStringInfoString(s, "  Batches: ");
390                         appendStringInfoString(s, v->hash_batches);
391                         if (v->org_hash_batches &&
392                                 strcmp(v->hash_batches, v->org_hash_batches) != 0)
393                         {
394                                 appendStringInfoString(s, " (originally ");
395                                 appendStringInfoString(s, v->org_hash_batches);
396                                 appendStringInfoChar(s, ')');
397                         }
398                 }
399                 if (!ISZERO(v->peak_memory_usage))
400                 {
401                         appendStringInfoString(s, "  Memory Usage: ");
402                         appendStringInfoString(s, v->peak_memory_usage);
403                         appendStringInfoString(s, "kB");
404                 }
405         }
406
407         print_prop_if_exists(s, "Heap Fetches: ", v->heap_fetches, level, exind);
408
409         if (!ISZERO(v->shared_hit_blks) ||
410                 !ISZERO(v->shared_read_blks) ||
411                 !ISZERO(v->shared_dirtied_blks) ||
412                 !ISZERO(v->shared_written_blks))
413         {
414                 appendStringInfoString(s, "\n");
415                 appendStringInfoSpaces(s, TEXT_INDENT_DETAILS(level, exind));
416                 appendStringInfoString(s, "Buffers: shared");
417
418                 if (!ISZERO(v->shared_hit_blks))
419                 {
420                         appendStringInfoString(s, " hit=");
421                         appendStringInfoString(s, v->shared_hit_blks);
422                         comma =true;
423                 }
424                 if (!ISZERO(v->shared_read_blks))
425                 {
426                         appendStringInfoString(s, " read=");
427                         appendStringInfoString(s, v->shared_read_blks);
428                         comma =true;
429                 }
430                 if (!ISZERO(v->shared_dirtied_blks))
431                 {
432                         appendStringInfoString(s, " dirtied=");
433                         appendStringInfoString(s, v->shared_dirtied_blks);
434                         comma =true;
435                 }
436                 if (!ISZERO(v->shared_written_blks))
437                 {
438                         appendStringInfoString(s, " written=");
439                         appendStringInfoString(s, v->shared_written_blks);
440                         comma =true;
441                 }
442         }
443         if (!ISZERO(v->local_hit_blks) ||
444                 !ISZERO(v->local_read_blks) ||
445                 !ISZERO(v->local_dirtied_blks) ||
446                 !ISZERO(v->local_written_blks))
447         {
448                 if (comma)
449                         appendStringInfoString(s, ", ");
450                 else
451                 {
452                         appendStringInfoSpaces(s, TEXT_INDENT_DETAILS(level, exind));
453                         appendStringInfoString(s, "Buffers: ");
454                 }
455
456                 appendStringInfoString(s, "local");
457                 if (!ISZERO(v->local_hit_blks))
458                 {
459                         appendStringInfoString(s, " hit=");
460                         appendStringInfoString(s, v->local_hit_blks);
461                         comma =true;
462                 }
463                 if (!ISZERO(v->local_read_blks))
464                 {
465                         appendStringInfoString(s, " read=");
466                         appendStringInfoString(s, v->local_read_blks);
467                         comma =true;
468                 }
469                 if (!ISZERO(v->local_dirtied_blks))
470                 {
471                         appendStringInfoString(s, " dirtied=");
472                         appendStringInfoString(s, v->local_dirtied_blks);
473                         comma =true;
474                 }
475                 if (!ISZERO(v->local_written_blks))
476                 {
477                         appendStringInfoString(s, " written=");
478                         appendStringInfoString(s, v->local_written_blks);
479                         comma =true;
480                 }
481         }
482         if (!ISZERO(v->temp_read_blks) ||
483                 !ISZERO(v->temp_written_blks))
484         {
485                 if (comma)
486                         appendStringInfoString(s, ", ");
487                 else
488                 {
489                         appendStringInfoSpaces(s, TEXT_INDENT_DETAILS(level, exind));
490                         appendStringInfoString(s, "Buffers: ");
491                 }
492
493                 appendStringInfoString(s, "temp");
494                 if (!ISZERO(v->temp_read_blks))
495                 {
496                         appendStringInfoString(s, " read=");
497                         appendStringInfoString(s, v->temp_read_blks);
498                         comma =true;
499                 }
500                 if (!ISZERO(v->temp_written_blks))
501                 {
502                         appendStringInfoString(s, " written=");
503                         appendStringInfoString(s, v->temp_written_blks);
504                         comma =true;
505                 }
506         }
507 }
508
509 static void
510 print_current_trig_node(pgspParserContext *ctx)
511 {
512         node_vals *v = ctx->nodevals;
513         StringInfo s = ctx->dest;
514
515         if (HASSTRING(v->trig_name) && !ISZERO(v->trig_time))
516         {
517                 if (s->len > 0)
518                         appendStringInfoString(s, "\n");
519                 appendStringInfoString(s, "Trigger ");
520                 appendStringInfoString(s, v->trig_name);
521                 appendStringInfoString(s, ": time=");
522                 appendStringInfoString(s, v->trig_time);
523                 appendStringInfoString(s, " calls=");
524                 appendStringInfoString(s, v->trig_calls);
525         }
526 }
527
528
529 static void
530 clear_nodeval(node_vals *vals)
531 {
532         memset(vals, 0, sizeof(node_vals));
533 }
534
535 static void
536 json_text_objstart(void *state)
537 {
538         pgspParserContext *ctx = (pgspParserContext *)state;
539         clear_nodeval(ctx->nodevals);
540         ctx->level++;
541 }
542 static void
543 json_text_objend(void *state)
544 {
545         pgspParserContext *ctx = (pgspParserContext *)state;
546         switch (ctx->processing)
547         {
548         case P_Plan:
549                 print_current_node(ctx);
550                 break;
551         case P_Triggers:
552                 print_current_trig_node(ctx);
553                 break;
554         default:
555                 break;
556         }
557
558         clear_nodeval(ctx->nodevals);
559         ctx->last_elem_is_object = true;
560         ctx->level--;
561 }
562
563 static void
564 json_text_ofstart(void *state, char *fname, bool isnull)
565 {
566         word_table *p;
567         pgspParserContext *ctx = (pgspParserContext *)state;
568
569         ctx->setter = NULL;
570         p = search_word_table(propfields, fname, PGSP_JSON_TEXTIZE);
571
572         if (!p)
573         {
574                 ereport(DEBUG1,
575                                 (errmsg("Short JSON parser encoutered unknown field name: \"%s\", skipped.", fname),
576                                  errdetail_log("INPUT: \"%s\"", ctx->org_string)));
577         }
578         else
579         {
580                 /* Print node immediately if next level of Plan/Plans comes */
581                 if (p->tag == P_Plan || p->tag == P_Plans)
582                 {
583                         print_current_node(ctx);
584                         clear_nodeval(ctx->nodevals);
585                 }
586
587                 
588                 if (p->tag == P_Plan || p->tag == P_Triggers)
589                         ctx->processing = p->tag;
590                 ctx->setter = p->setter;
591         }
592 }
593
594 static void
595 json_text_ofend(void *state, char *fname, bool isnull)
596 {
597         pgspParserContext *ctx = (pgspParserContext *)state;
598         node_vals *v = ctx->nodevals;
599
600         /* Planning/Execution time to be appeared at the end of plan */
601         if (HASSTRING(v->plan_time) ||
602                 HASSTRING(v->exec_time))
603         {
604                 if (HASSTRING(v->plan_time))
605                 {
606                         appendStringInfoString(ctx->dest, "\nPlanning Time: ");
607                         appendStringInfoString(ctx->dest, v->plan_time);
608                         appendStringInfoString(ctx->dest, " ms");
609                 }
610                 else
611                 {
612                         appendStringInfoString(ctx->dest, "\nExecution Time: ");
613                         appendStringInfoString(ctx->dest, v->exec_time);
614                         appendStringInfoString(ctx->dest, " ms");
615                 }
616                 clear_nodeval(v);
617         }
618 }
619
620 static void
621 json_text_scalar(void *state, char *token, JsonTokenType tokentype)
622 {
623         pgspParserContext *ctx = (pgspParserContext *)state;
624
625         if (ctx->setter)
626                 ctx->setter(ctx->nodevals, token);
627 }
628
629 char *
630 pgsp_json_textize(char *json)
631 {
632         JsonLexContext lex;
633         JsonSemAction sem;
634         pgspParserContext       ctx;
635
636         init_json_lex_context(&lex, json);
637         init_parser_context(&ctx, PGSP_JSON_TEXTIZE, json, NULL, 0);
638
639         ctx.nodevals = (node_vals*)palloc0(sizeof(node_vals));
640
641         sem.semstate = (void*)&ctx;
642         sem.object_start       = json_text_objstart;
643         sem.object_end         = json_text_objend;
644         sem.array_start        = NULL;
645         sem.array_end          = NULL;
646         sem.object_field_start = json_text_ofstart;
647         sem.object_field_end   = json_text_ofend;
648         sem.array_element_start= NULL;
649         sem.array_element_end  = NULL;
650         sem.scalar             = json_text_scalar;
651
652
653         if (!run_pg_parse_json(&lex, &sem))
654         {
655                 if (ctx.nodevals->node_type)
656                         print_current_node(&ctx);
657
658                 if (ctx.dest->len > 0 &&
659                         ctx.dest->data[ctx.dest->len - 1] != '\n')
660                         appendStringInfoChar(ctx.dest, '\n');
661                 
662                 if (ctx.dest->len == 0)
663                         appendStringInfoString(ctx.dest, "<Input was not JSON>");
664                 else
665                         appendStringInfoString(ctx.dest, "<truncated>");
666         }
667
668         pfree(ctx.nodevals);
669
670         return ctx.dest->data;
671 }