Assert that RTIs of joined rels are discoverable from join plans.
authorRobert Haas <rhaas@postgresql.org>
Wed, 16 Apr 2025 12:32:00 +0000 (08:32 -0400)
committerRobert Haas <rhaas@postgresql.org>
Mon, 19 May 2025 15:02:50 +0000 (11:02 -0400)
Every RTI that appears in the joinrel's relid set should be findable
via the outer or inner plan, except for join RTIs which aren't
necessarily preserved in the final plan. This is a requirement if
we want to be able to reliably determine the chosen join order from
the final plan, although it's not sufficient for that goal of itself,
due to further problems created by setrefs-time processing.

Note that this depends on the earlier commit to add a relids field to
Result nodes; without that change, a join tree involving two or more
Result nodes would be fundamentally ambiguous (and even a join tree
involving one could only be interpreted by guessing at its origin).

src/backend/optimizer/plan/createplan.c

index ce0e7ee6b9d80199077bdee0c407096d5624ade2..ccc5f5d3e27e314017c2020ac6f407d3d2acadef 100644 (file)
@@ -320,7 +320,14 @@ static ModifyTable *make_modifytable(PlannerInfo *root, Plan *subplan,
                                                                         int epqParam);
 static GatherMerge *create_gather_merge_plan(PlannerInfo *root,
                                                                                         GatherMergePath *best_path);
+static void assert_join_preserves_scan_rtis(PlannerInfo *root, Path *best_path,
+                                                                                       Plan *outer_plan,
+                                                                                       Plan *inner_plan);
 
+#ifdef USE_ASSERT_CHECKING
+static Bitmapset *get_scanned_rtindexes(PlannerInfo *root, Plan *plan);
+static Bitmapset *remove_join_rtis(PlannerInfo *root, Bitmapset *bms);
+#endif
 
 /*
  * create_plan
@@ -4441,6 +4448,9 @@ create_nestloop_plan(PlannerInfo *root,
 
        copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
 
+       assert_join_preserves_scan_rtis(root, &best_path->jpath.path, outer_plan,
+                                                                       inner_plan);
+
        return join_plan;
 }
 
@@ -4795,6 +4805,9 @@ create_mergejoin_plan(PlannerInfo *root,
        /* Costs of sort and material steps are included in path cost already */
        copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
 
+       assert_join_preserves_scan_rtis(root, &best_path->jpath.path,
+                                                                       outer_plan, inner_plan);
+
        return join_plan;
 }
 
@@ -4968,6 +4981,9 @@ create_hashjoin_plan(PlannerInfo *root,
 
        copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
 
+       assert_join_preserves_scan_rtis(root, &best_path->jpath.path,
+                                                                       outer_plan, inner_plan);
+
        return join_plan;
 }
 
@@ -7444,3 +7460,156 @@ is_projection_capable_plan(Plan *plan)
        }
        return true;
 }
+
+/*
+ * Check that the RTIs of the relations being joined at this level are
+ * properly reflected in the Plan tree.
+ *
+ * We expect to find every non-RTE_JOIN RTI from best_path->parent.relids
+ * mentioned in either the outer or inner subplan.
+ */
+static void
+assert_join_preserves_scan_rtis(PlannerInfo *root, Path *best_path,
+                                                               Plan *outer_plan, Plan *inner_plan)
+{
+#ifdef USE_ASSERT_CHECKING
+       Bitmapset  *outer_scanrelids;
+       Bitmapset  *inner_scanrelids;
+       Bitmapset  *calculated_scanrelids;
+       Bitmapset  *filtered_joinrelids;
+
+       outer_scanrelids = get_scanned_rtindexes(root, outer_plan);
+       inner_scanrelids = get_scanned_rtindexes(root, inner_plan);
+       calculated_scanrelids = bms_union(outer_scanrelids, inner_scanrelids);
+       filtered_joinrelids = remove_join_rtis(root, best_path->parent->relids);
+
+       /* Any given scan RTI should appear on only one side or the other. */
+       Assert(!bms_overlap(inner_scanrelids, outer_scanrelids));
+
+       /*
+        * If this assertion fails, it means that the set of range table indexes
+        * that we found in the inner and outer path tree did not equal the set of
+        * range table indexes that we found for this joinrel, even after
+        * excluding RTE_JOIN range table indexes which are not expect to appear
+        * in the plan tree.
+        *
+        * If this assertion fails due to the addition of a new executor node
+        * type, you probably just need to update get_scanned_rtindexes to know
+        * about the new node. See the header comments for that function for other
+        * places to update at the same time.
+        */
+       Assert(bms_equal(calculated_scanrelids, filtered_joinrelids));
+#endif
+}
+
+#ifdef USE_ASSERT_CHECKING
+/*
+ * Get the set of range table indexes that are scanned by a scan or join node,
+ * or any executor node that could appear beneath a scan or join node.
+ *
+ * We are uninterested in join RTIs here; we're only interested in which RTIs
+ * are scanned at or below a particular plan node, and only if that node can
+ * appear beneath a join.
+ *
+ * When adding new cases to this function, be sure to also update
+ * ExplainPreScanNode, ExplainNode, and overexplain_per_node_hook as
+ * appropriate.
+ */
+static Bitmapset *
+get_scanned_rtindexes(PlannerInfo *root, Plan *plan)
+{
+       switch (nodeTag(plan))
+       {
+               case T_SeqScan:
+               case T_SampleScan:
+               case T_IndexScan:
+               case T_IndexOnlyScan:
+               case T_BitmapHeapScan:
+               case T_TidScan:
+               case T_TidRangeScan:
+               case T_SubqueryScan:
+               case T_FunctionScan:
+               case T_TableFuncScan:
+               case T_ValuesScan:
+               case T_CteScan:
+               case T_NamedTuplestoreScan:
+               case T_WorkTableScan:
+                       return bms_make_singleton(((Scan *) plan)->scanrelid);
+                       break;
+               case T_ForeignScan:
+                       return ((ForeignScan *) plan)->fs_base_relids;
+                       break;
+               case T_CustomScan:
+                       return ((CustomScan *) plan)->custom_relids;
+                       break;
+               case T_Append:
+                       return ((Append *) plan)->apprelids;
+                       break;
+               case T_MergeAppend:
+                       return ((MergeAppend *) plan)->apprelids;
+                       break;
+               case T_Result:
+                       if (plan->lefttree)
+                               return get_scanned_rtindexes(root, plan->lefttree);
+                       else
+                               return remove_join_rtis(root, ((Result *) plan)->relids);
+                       break;
+               case T_HashJoin:
+               case T_MergeJoin:
+               case T_NestLoop:
+                       {
+                               Bitmapset  *outer_scanrelids;
+                               Bitmapset  *inner_scanrelids;
+                               Bitmapset  *combined_scanrelids;
+
+                               outer_scanrelids =
+                                       get_scanned_rtindexes(root, plan->lefttree);
+                               inner_scanrelids =
+                                       get_scanned_rtindexes(root, plan->righttree);
+                               combined_scanrelids =
+                                       bms_union(outer_scanrelids, inner_scanrelids);
+                               inner_scanrelids = remove_join_rtis(root, inner_scanrelids);
+
+                               return remove_join_rtis(root, combined_scanrelids);
+                               break;
+                       }
+               case T_Sort:
+               case T_IncrementalSort:
+               case T_Unique:
+               case T_Agg:
+               case T_Hash:
+               case T_Gather:
+               case T_GatherMerge:
+               case T_Material:
+               case T_Memoize:
+                       return get_scanned_rtindexes(root, plan->lefttree);
+                       break;
+               default:
+                       break;
+       }
+
+       return NULL;
+}
+
+/*
+ * Return a new Bitmapset containing only those range table indexes from the
+ * input set that do not reference an RTE where rtekind == RTE_JOIN.
+ */
+static Bitmapset *
+remove_join_rtis(PlannerInfo *root, Bitmapset *bms)
+{
+       int                     rti = -1;
+
+       bms = bms_copy(bms);
+
+       while ((rti = bms_next_member(bms, rti)) >= 0)
+       {
+               RangeTblEntry *rte = planner_rt_fetch(rti, root);
+
+               if (rte->rtekind == RTE_JOIN)
+                       bms = bms_del_member(bms, rti);
+       }
+
+       return bms;
+}
+#endif