1 /*
2  * This file and its contents are licensed under the Timescale License.
3  * Please see the included NOTICE for copyright information and
4  * LICENSE-TIMESCALE for a copy of the license.
5  */
6 
7 /*
8  * This file contains source code that was copied and/or modified from
9  * the PostgreSQL database, which is licensed under the open-source
10  * PostgreSQL License. Please see the NOTICE at the top level
11  * directory for a copy of the PostgreSQL License.
12  */
13 
14 /*
15  * The code is partially copied from nodes/print.c and
16  * backend/optimizer/path/allpaths.c in the PostgreSQL source code, but we
17  * cannot use it out of the box for two reasons:
18  *
19  * The first reason is that the PostgreSQL code prints to standard output
20  * (hence to the log) and we want to build a string buffer to send back in a
21  * notice, we cannot use the functions as they are but have re-implement them.
22  *
23  * We want to send back paths and plans in a notice to the client, to make it
24  * possible to interactively investigate what paths and plans that queries
25  * generate without having to access the log.
26  *
27  * The second reason is that the PostgreSQL code is not aware of our custom
28  * nodes and the hierarchy below them, so we need to have special handling of
29  * custom nodes to get out more information.
30  *
31  * (A third reason is that the printing functions are incomplete and do not
32  * print items below certain nodes, such as Append and MergeAppend, and we are
33  * using them for our purposes and need to have more information about
34  * subpaths than what PostgreSQL prints.)
35  */
36 
37 #include <postgres.h>
38 #include <foreign/fdwapi.h>
39 
40 #include <access/printtup.h>
41 #include <nodes/nodeFuncs.h>
42 #include <nodes/pg_list.h>
43 #include <optimizer/clauses.h>
44 #include <parser/parsetree.h>
45 #include <utils/builtins.h>
46 #include <utils/lsyscache.h>
47 #include <utils/varlena.h>
48 #include <nodes/extensible.h>
49 
50 #include <compat/compat.h>
51 #include "fdw/relinfo.h"
52 #include "fdw/fdw_utils.h"
53 #include "debug.h"
54 
55 static void append_expr(StringInfo buf, const Node *expr, const List *rtable);
56 static void tsl_debug_append_pathlist(StringInfo buf, PlannerInfo *root, List *pathlist, int indent,
57 									  bool isconsidered);
58 
59 static const char *reloptkind_name[] = {
60 	[RELOPT_BASEREL] = "BASEREL",
61 	[RELOPT_JOINREL] = "JOINREL",
62 	[RELOPT_OTHER_MEMBER_REL] = "OTHER_MEMBER_REL",
63 	[RELOPT_OTHER_JOINREL] = "OTHER_JOINREL",
64 	[RELOPT_UPPER_REL] = "UPPER_REL",
65 	[RELOPT_OTHER_UPPER_REL] = "OTHER_UPPER_REL",
66 	[RELOPT_DEADREL] = "DEADREL",
67 };
68 
69 /* clang-format off */
70 static const char *upperrel_stage_name[] = {
71 	[UPPERREL_SETOP] = "SETOP",
72 	[UPPERREL_PARTIAL_GROUP_AGG] = "PARTIAL_GROUP_AGG",
73 	[UPPERREL_GROUP_AGG] = "GROUP_AGG",
74 	[UPPERREL_WINDOW] = "WINDOW",
75 	[UPPERREL_DISTINCT] = "DISTINCT",
76 	[UPPERREL_ORDERED] = "ORDERED",
77 	[UPPERREL_FINAL] = "FINAL",
78 };
79 /* clang-format on */
80 
81 static const char *fdw_rel_type_names[] = {
82 	[TS_FDW_RELINFO_HYPERTABLE_DATA_NODE] = "DATA_NODE",
83 	[TS_FDW_RELINFO_HYPERTABLE] = "HYPERTABLE",
84 	[TS_FDW_RELINFO_FOREIGN_TABLE] = "FOREIGN_TABLE",
85 };
86 
87 static void
append_var_expr(StringInfo buf,const Node * expr,const List * rtable)88 append_var_expr(StringInfo buf, const Node *expr, const List *rtable)
89 {
90 	const Var *var = (const Var *) expr;
91 	char *relname, *attname;
92 
93 	switch (var->varno)
94 	{
95 		case INNER_VAR:
96 			relname = "INNER";
97 			attname = "?";
98 			break;
99 		case OUTER_VAR:
100 			relname = "OUTER";
101 			attname = "?";
102 			break;
103 		case INDEX_VAR:
104 			relname = "INDEX";
105 			attname = "?";
106 			break;
107 		default:
108 		{
109 			RangeTblEntry *rte;
110 
111 			Assert(var->varno > 0 && (int) var->varno <= list_length(rtable));
112 			rte = rt_fetch(var->varno, rtable);
113 			relname = rte->eref->aliasname;
114 			attname = get_rte_attribute_name(rte, var->varattno);
115 		}
116 		break;
117 	}
118 	appendStringInfo(buf, "%s.%s", relname, attname);
119 }
120 
121 static void
append_const_expr(StringInfo buf,const Node * expr,const List * rtable)122 append_const_expr(StringInfo buf, const Node *expr, const List *rtable)
123 {
124 	const Const *c = (const Const *) expr;
125 	Oid typoutput;
126 	bool typIsVarlena;
127 	char *outputstr;
128 
129 	if (c->constisnull)
130 	{
131 		appendStringInfo(buf, "NULL");
132 		return;
133 	}
134 
135 	getTypeOutputInfo(c->consttype, &typoutput, &typIsVarlena);
136 
137 	outputstr = OidOutputFunctionCall(typoutput, c->constvalue);
138 	appendStringInfo(buf, "%s", outputstr);
139 	pfree(outputstr);
140 }
141 
142 static void
append_op_expr(StringInfo buf,const Node * expr,const List * rtable)143 append_op_expr(StringInfo buf, const Node *expr, const List *rtable)
144 {
145 	const OpExpr *e = (const OpExpr *) expr;
146 	char *opname = get_opname(e->opno);
147 	if (list_length(e->args) > 1)
148 	{
149 		append_expr(buf, get_leftop((const Expr *) e), rtable);
150 		appendStringInfo(buf, " %s ", ((opname != NULL) ? opname : "(invalid operator)"));
151 		append_expr(buf, get_rightop((const Expr *) e), rtable);
152 	}
153 	else
154 	{
155 		appendStringInfo(buf, "%s ", ((opname != NULL) ? opname : "(invalid operator)"));
156 		append_expr(buf, get_leftop((const Expr *) e), rtable);
157 	}
158 }
159 
160 static void
append_func_expr(StringInfo buf,const Node * expr,const List * rtable)161 append_func_expr(StringInfo buf, const Node *expr, const List *rtable)
162 {
163 	const FuncExpr *e = (const FuncExpr *) expr;
164 	char *funcname = get_func_name(e->funcid);
165 	ListCell *l;
166 
167 	appendStringInfo(buf, "%s(", ((funcname != NULL) ? funcname : "(invalid function)"));
168 	foreach (l, e->args)
169 	{
170 		append_expr(buf, lfirst(l), rtable);
171 		if (lnext_compat(e->args, l))
172 			appendStringInfoString(buf, ", ");
173 	}
174 	appendStringInfoChar(buf, ')');
175 }
176 
177 static void
append_expr(StringInfo buf,const Node * expr,const List * rtable)178 append_expr(StringInfo buf, const Node *expr, const List *rtable)
179 {
180 	if (expr == NULL)
181 	{
182 		appendStringInfo(buf, "<>");
183 		return;
184 	}
185 
186 	switch (nodeTag(expr))
187 	{
188 		case T_Var:
189 			append_var_expr(buf, expr, rtable);
190 			break;
191 
192 		case T_Const:
193 			append_const_expr(buf, expr, rtable);
194 			break;
195 
196 		case T_OpExpr:
197 			append_op_expr(buf, expr, rtable);
198 			break;
199 
200 		case T_FuncExpr:
201 			append_func_expr(buf, expr, rtable);
202 			break;
203 
204 		default:
205 			appendStringInfo(buf, "unknown expr");
206 			break;
207 	}
208 }
209 
210 static void
append_restrict_clauses(StringInfo buf,PlannerInfo * root,List * clauses)211 append_restrict_clauses(StringInfo buf, PlannerInfo *root, List *clauses)
212 {
213 	ListCell *cell;
214 
215 	foreach (cell, clauses)
216 	{
217 		RestrictInfo *c = lfirst(cell);
218 
219 		append_expr(buf, (Node *) c->clause, root->parse->rtable);
220 		if (lnext_compat(clauses, cell))
221 			appendStringInfoString(buf, ", ");
222 	}
223 }
224 
225 static void
append_relids(StringInfo buf,PlannerInfo * root,Relids relids)226 append_relids(StringInfo buf, PlannerInfo *root, Relids relids)
227 {
228 	int x = -1;
229 	bool first = true;
230 
231 	while ((x = bms_next_member(relids, x)) >= 0)
232 	{
233 		if (!first)
234 			appendStringInfoChar(buf, ' ');
235 		if (x < root->simple_rel_array_size && root->simple_rte_array[x])
236 			appendStringInfo(buf, "%s", root->simple_rte_array[x]->eref->aliasname);
237 		else
238 			appendStringInfo(buf, "%d", x);
239 		first = false;
240 	}
241 }
242 
243 static void
append_pathkeys(StringInfo buf,const List * pathkeys,const List * rtable)244 append_pathkeys(StringInfo buf, const List *pathkeys, const List *rtable)
245 {
246 	const ListCell *i;
247 
248 	appendStringInfoChar(buf, '(');
249 	foreach (i, pathkeys)
250 	{
251 		PathKey *pathkey = (PathKey *) lfirst(i);
252 		EquivalenceClass *eclass;
253 		ListCell *k;
254 		bool first = true;
255 
256 		eclass = pathkey->pk_eclass;
257 		/* chase up, in case pathkey is non-canonical */
258 		while (eclass->ec_merged)
259 			eclass = eclass->ec_merged;
260 
261 		appendStringInfoChar(buf, '(');
262 		foreach (k, eclass->ec_members)
263 		{
264 			EquivalenceMember *mem = (EquivalenceMember *) lfirst(k);
265 
266 			if (first)
267 				first = false;
268 			else
269 				appendStringInfoString(buf, ", ");
270 			append_expr(buf, (Node *) mem->em_expr, rtable);
271 		}
272 		appendStringInfoChar(buf, ')');
273 		if (lnext_compat(pathkeys, i))
274 			appendStringInfoString(buf, ", ");
275 	}
276 	appendStringInfoChar(buf, ')');
277 }
278 
279 /*
280  * Return a relation's name.
281  *
282  * This function guarantees the return of a valid name string for a
283  * relation. For relations that have no unique name we return "-".
284  */
285 static const char *
get_relation_name(PlannerInfo * root,RelOptInfo * rel)286 get_relation_name(PlannerInfo *root, RelOptInfo *rel)
287 {
288 	TsFdwRelInfo *fdw_info = fdw_relinfo_get(rel);
289 
290 	if (NULL != fdw_info)
291 		return fdw_info->relation_name->data;
292 
293 	if (rel->reloptkind == RELOPT_BASEREL)
294 	{
295 		RangeTblEntry *rte = planner_rt_fetch(rel->relid, root);
296 
297 		return get_rel_name(rte->relid);
298 	}
299 
300 	return "-";
301 }
302 
303 /*
304  * Return a string name for the FDW type of a relation.
305  *
306  * For relations that are not an FDW relation we simply return "-".
307  */
308 static const char *
get_fdw_relation_typename(RelOptInfo * rel)309 get_fdw_relation_typename(RelOptInfo *rel)
310 {
311 	TsFdwRelInfo *fdw_info = fdw_relinfo_get(rel);
312 
313 	if (NULL != fdw_info)
314 		return fdw_rel_type_names[fdw_info->type];
315 
316 	return "-";
317 }
318 
319 static void
tsl_debug_append_path(StringInfo buf,PlannerInfo * root,Path * path,int indent)320 tsl_debug_append_path(StringInfo buf, PlannerInfo *root, Path *path, int indent)
321 {
322 	const char *ptype;
323 	const char *extra_info = NULL;
324 	bool join = false;
325 	Path *subpath = NULL;
326 	List *subpath_list = NULL;
327 	int i;
328 
329 	switch (nodeTag(path))
330 	{
331 		case T_Path:
332 			switch (path->pathtype)
333 			{
334 				case T_SeqScan:
335 					ptype = "SeqScan";
336 					break;
337 				case T_SampleScan:
338 					ptype = "SampleScan";
339 					break;
340 				case T_SubqueryScan:
341 					ptype = "SubqueryScan";
342 					break;
343 				case T_FunctionScan:
344 					ptype = "FunctionScan";
345 					break;
346 				case T_TableFuncScan:
347 					ptype = "TableFuncScan";
348 					break;
349 				case T_ValuesScan:
350 					ptype = "ValuesScan";
351 					break;
352 				case T_CteScan:
353 					ptype = "CteScan";
354 					break;
355 				case T_WorkTableScan:
356 					ptype = "WorkTableScan";
357 					break;
358 				default:
359 					ptype = "???Path";
360 					break;
361 			}
362 			break;
363 		case T_IndexPath:
364 			ptype = "IdxScan";
365 			break;
366 		case T_BitmapHeapPath:
367 			ptype = "BitmapHeapScan";
368 			break;
369 		case T_BitmapAndPath:
370 			ptype = "BitmapAndPath";
371 			break;
372 		case T_BitmapOrPath:
373 			ptype = "BitmapOrPath";
374 			break;
375 		case T_TidPath:
376 			ptype = "TidScan";
377 			break;
378 		case T_SubqueryScanPath:
379 			ptype = "SubqueryScanScan";
380 			subpath = castNode(SubqueryScanPath, path)->subpath;
381 			break;
382 		case T_ForeignPath:
383 			ptype = "ForeignScan";
384 			break;
385 		case T_CustomPath:
386 			ptype = "CustomScan";
387 			subpath_list = castNode(CustomPath, path)->custom_paths;
388 			extra_info = castNode(CustomPath, path)->methods->CustomName;
389 			break;
390 		case T_NestPath:
391 			ptype = "NestLoop";
392 			join = true;
393 			break;
394 		case T_MergePath:
395 			ptype = "MergeJoin";
396 			join = true;
397 			break;
398 		case T_HashPath:
399 			ptype = "HashJoin";
400 			join = true;
401 			break;
402 		case T_AppendPath:
403 			ptype = "Append";
404 			subpath_list = castNode(AppendPath, path)->subpaths;
405 			break;
406 		case T_MergeAppendPath:
407 			ptype = "MergeAppend";
408 			subpath_list = castNode(MergeAppendPath, path)->subpaths;
409 			break;
410 		case T_GroupResultPath:
411 			ptype = "GroupResult";
412 			break;
413 		case T_MaterialPath:
414 			ptype = "Material";
415 			subpath = castNode(MaterialPath, path)->subpath;
416 			break;
417 		case T_UniquePath:
418 			ptype = "Unique";
419 			subpath = castNode(UniquePath, path)->subpath;
420 			break;
421 		case T_GatherPath:
422 			ptype = "Gather";
423 			subpath = castNode(GatherPath, path)->subpath;
424 			break;
425 		case T_GatherMergePath:
426 			ptype = "GatherMerge";
427 			subpath = castNode(GatherMergePath, path)->subpath;
428 			break;
429 		case T_ProjectionPath:
430 			ptype = "Projection";
431 			subpath = castNode(ProjectionPath, path)->subpath;
432 			break;
433 		case T_ProjectSetPath:
434 			ptype = "ProjectSet";
435 			subpath = castNode(ProjectSetPath, path)->subpath;
436 			break;
437 		case T_SortPath:
438 			ptype = "Sort";
439 			subpath = castNode(SortPath, path)->subpath;
440 			break;
441 		case T_GroupPath:
442 			ptype = "Group";
443 			subpath = castNode(GroupPath, path)->subpath;
444 			break;
445 		case T_UpperUniquePath:
446 			ptype = "UpperUnique";
447 			subpath = castNode(UpperUniquePath, path)->subpath;
448 			break;
449 		case T_AggPath:
450 			ptype = "Agg";
451 			subpath = castNode(AggPath, path)->subpath;
452 			break;
453 		case T_GroupingSetsPath:
454 			ptype = "GroupingSets";
455 			subpath = castNode(GroupingSetsPath, path)->subpath;
456 			break;
457 		case T_MinMaxAggPath:
458 			ptype = "MinMaxAgg";
459 			break;
460 		case T_WindowAggPath:
461 			ptype = "WindowAgg";
462 			subpath = castNode(WindowAggPath, path)->subpath;
463 			break;
464 		case T_SetOpPath:
465 			ptype = "SetOp";
466 			subpath = castNode(SetOpPath, path)->subpath;
467 			break;
468 		case T_RecursiveUnionPath:
469 			ptype = "RecursiveUnion";
470 			break;
471 		case T_LockRowsPath:
472 			ptype = "LockRows";
473 			subpath = castNode(LockRowsPath, path)->subpath;
474 			break;
475 		case T_ModifyTablePath:
476 			ptype = "ModifyTable";
477 #if PG14_LT
478 			subpath_list = castNode(ModifyTablePath, path)->subpaths;
479 #else
480 			subpath_list = list_make1(castNode(ModifyTablePath, path)->subpath);
481 #endif
482 			break;
483 		case T_LimitPath:
484 			ptype = "Limit";
485 			subpath = castNode(LimitPath, path)->subpath;
486 			break;
487 		default:
488 			ptype = "???Path";
489 			break;
490 	}
491 
492 	for (i = 0; i < indent; i++)
493 		appendStringInfo(buf, "\t");
494 	appendStringInfo(buf, "%s", ptype);
495 	if (extra_info)
496 		appendStringInfo(buf, " (%s)", extra_info);
497 
498 	if (path->parent)
499 	{
500 		appendStringInfo(buf,
501 						 " [rel type: %s, kind: %s",
502 						 get_fdw_relation_typename(path->parent),
503 						 reloptkind_name[path->parent->reloptkind]);
504 		appendStringInfoString(buf, ", parent's base rels: ");
505 		append_relids(buf, root, path->parent->relids);
506 		appendStringInfoChar(buf, ']');
507 	}
508 
509 	if (path->param_info)
510 	{
511 		appendStringInfoString(buf, " required_outer (");
512 		append_relids(buf, root, path->param_info->ppi_req_outer);
513 		appendStringInfoChar(buf, ')');
514 	}
515 
516 	appendStringInfo(buf, " rows=%.0f", path->rows);
517 
518 	if (path->pathkeys)
519 	{
520 		appendStringInfoString(buf, " with pathkeys: ");
521 		append_pathkeys(buf, path->pathkeys, root->parse->rtable);
522 	}
523 
524 	appendStringInfoString(buf, "\n");
525 
526 	if (join)
527 	{
528 		JoinPath *jp = (JoinPath *) path;
529 
530 		for (i = 0; i < indent; i++)
531 			appendStringInfoString(buf, "\t");
532 		appendStringInfoString(buf, "  clauses: ");
533 		append_restrict_clauses(buf, root, jp->joinrestrictinfo);
534 		appendStringInfoString(buf, "\n");
535 
536 		if (IsA(path, MergePath))
537 		{
538 			MergePath *mp = castNode(MergePath, path);
539 
540 			for (i = 0; i < indent; i++)
541 				appendStringInfo(buf, "\t");
542 			appendStringInfo(buf,
543 							 "  sortouter=%d sortinner=%d materializeinner=%d\n",
544 							 ((mp->outersortkeys) ? 1 : 0),
545 							 ((mp->innersortkeys) ? 1 : 0),
546 							 ((mp->materialize_inner) ? 1 : 0));
547 		}
548 
549 		tsl_debug_append_path(buf, root, jp->outerjoinpath, indent + 1);
550 		tsl_debug_append_path(buf, root, jp->innerjoinpath, indent + 1);
551 	}
552 
553 	if (subpath)
554 		tsl_debug_append_path(buf, root, subpath, indent + 1);
555 	if (subpath_list)
556 		tsl_debug_append_pathlist(buf, root, subpath_list, indent + 1, false);
557 }
558 
559 static void
tsl_debug_append_pathlist(StringInfo buf,PlannerInfo * root,List * pathlist,int indent,bool isconsidered)560 tsl_debug_append_pathlist(StringInfo buf, PlannerInfo *root, List *pathlist, int indent,
561 						  bool isconsidered)
562 {
563 	ListCell *cell;
564 	foreach (cell, pathlist)
565 	{
566 		Path *path = isconsidered ? ((ConsideredPath *) lfirst(cell))->path : lfirst(cell);
567 		tsl_debug_append_path(buf, root, path, indent);
568 	}
569 }
570 
571 /*
572  * Check whether a path is the origin of a considered path.
573  *
574  * It is not possible to do a simple memcmp() of paths here because a path
575  * could be a (semi-)shallow copy. Therefore we use the origin of the
576  * ConsideredPath object.
577  */
578 static bool
path_is_origin(const Path * p1,const ConsideredPath * p2)579 path_is_origin(const Path *p1, const ConsideredPath *p2)
580 {
581 	return p2->origin == (uintptr_t) p1;
582 }
583 
584 /*
585  * Print paths that were pruned during planning.
586  *
587  * The pruned paths are those that have been considered but are not in the
588  * rel's pathlist.
589  */
590 static void
tsl_debug_append_pruned_pathlist(StringInfo buf,PlannerInfo * root,RelOptInfo * rel,int indent)591 tsl_debug_append_pruned_pathlist(StringInfo buf, PlannerInfo *root, RelOptInfo *rel, int indent)
592 {
593 	TsFdwRelInfo *fdw_info = fdw_relinfo_get(rel);
594 	ListCell *lc1;
595 
596 	if (NULL == fdw_info || fdw_info->considered_paths == NIL)
597 		return;
598 
599 	foreach (lc1, rel->pathlist)
600 	{
601 		Path *p1 = (Path *) lfirst(lc1);
602 		ListCell *lc2;
603 #if PG13_LT
604 		ListCell *prev = NULL;
605 #endif
606 
607 		foreach (lc2, fdw_info->considered_paths)
608 		{
609 			ConsideredPath *p2 = (ConsideredPath *) lfirst(lc2);
610 
611 			if (path_is_origin(p1, p2))
612 			{
613 				fdw_info->considered_paths =
614 					list_delete_cell_compat(fdw_info->considered_paths, lc2, prev);
615 				fdw_utils_free_path(p2);
616 				break;
617 			}
618 #if PG13_LT
619 			prev = lc2;
620 #endif
621 		}
622 	}
623 
624 	if (fdw_info->considered_paths == NIL)
625 		return;
626 
627 	appendStringInfoString(buf, "Pruned paths:\n");
628 	tsl_debug_append_pathlist(buf, root, fdw_info->considered_paths, indent, true);
629 
630 	foreach (lc1, fdw_info->considered_paths)
631 		fdw_utils_free_path(lfirst(lc1));
632 
633 	fdw_info->considered_paths = NIL;
634 }
635 
636 void
tsl_debug_log_rel_with_paths(PlannerInfo * root,RelOptInfo * rel,UpperRelationKind * upper_stage)637 tsl_debug_log_rel_with_paths(PlannerInfo *root, RelOptInfo *rel, UpperRelationKind *upper_stage)
638 {
639 	StringInfo buf = makeStringInfo();
640 
641 	if (upper_stage != NULL)
642 		appendStringInfo(buf, "Upper rel stage %s:\n", upperrel_stage_name[*upper_stage]);
643 
644 	appendStringInfo(buf,
645 					 "RELOPTINFO [rel name: %s, type: %s, kind: %s, base rel names: ",
646 					 get_relation_name(root, rel),
647 					 get_fdw_relation_typename(rel),
648 					 reloptkind_name[rel->reloptkind]);
649 	append_relids(buf, root, rel->relids);
650 	appendStringInfoChar(buf, ']');
651 	appendStringInfo(buf, " rows=%.0f width=%d\n", rel->rows, rel->reltarget->width);
652 
653 	appendStringInfoString(buf, "Path list:\n");
654 	tsl_debug_append_pathlist(buf, root, rel->pathlist, 1, false);
655 	tsl_debug_append_pruned_pathlist(buf, root, rel, 1);
656 
657 	if (rel->cheapest_parameterized_paths)
658 	{
659 		appendStringInfoString(buf, "\nCheapest parameterized paths:\n");
660 		tsl_debug_append_pathlist(buf, root, rel->cheapest_parameterized_paths, 1, false);
661 	}
662 
663 	if (rel->cheapest_startup_path)
664 	{
665 		appendStringInfoString(buf, "\nCheapest startup path:\n");
666 		tsl_debug_append_path(buf, root, rel->cheapest_startup_path, 1);
667 	}
668 
669 	if (rel->cheapest_total_path)
670 	{
671 		appendStringInfoString(buf, "\nCheapest total path:\n");
672 		tsl_debug_append_path(buf, root, rel->cheapest_total_path, 1);
673 	}
674 
675 	appendStringInfoString(buf, "\n");
676 	ereport(DEBUG2, (errmsg_internal("%s", buf->data)));
677 }
678