From 58d2a74b3c8ff8d6e9cec6698f67f66811d1e05c Mon Sep 17 00:00:00 2001 From: Zhang Wenchao Date: Tue, 14 Jul 2026 18:49:19 +0800 Subject: [PATCH] Fix ORCA plan param bitmaps and Material rescan shielding ORCA-translated plans get their extParam/allParam bitmapsets from SetParamIds(), called piecemeal by every translator function. These bitmaps are what rescan correctness hangs on: the executor propagates chgParam to a child only if the child's allParam contains the changed param (UpdateChangedParamSet()), and a Material node relies on that to discard its tuplestore when a param of an enclosing SubPlan changes. Any translator path that misses the call leaves a node with an empty bitmap, which silently breaks the chgParam chain below it: a Material sitting above a subtree that consumes a correlated param then replays the first outer row's result for every subsequent row. GPDB 7.5.x shipped exactly this class of wrong-results regression for correlated subqueries computing UNNEST over an outer column (fixed in 7.6.0). Two hardenings: 1. Replace the per-translator SetParamIds() calls with one authoritative pass in the ORCA post-processing (orca.c), run over the final shape of the plan after all plan mutations. Every plan node, in the main tree and in every subplan, now gets its bitmaps computed in one place, so a future translator path cannot reintroduce the wrong-results hazard by forgetting a call. The pass counts only PARAM_EXEC params, matching the regular planner's finalize_primnode(): PARAM_EXTERN paramids live in a separate numbering space (the client's $n) and never change during execution, so including them would alias unrelated exec params and trigger spurious rescans of materialized subtrees. 2. Set Material's cdb_shield_child_from_rescans only when the subtree below actually contains a Motion. That flag exists to protect Motions, which cannot be rescanned, from rescan and squelch (see the planner-side uses in pathnode.c); the ORCA translator set it unconditionally on every Material. For a Motion-free subtree the shield adds no benefit, and if the subtree consumes params of an enclosing SubPlan it needlessly makes the cached result's invalidation depend solely on the param bitmaps being right. The ORCA optimizer model itself is sound and unchanged: a Spool over a subtree with outer refs requests Rescannable from its children (CPhysicalSpool::PrsRequired), i.e. it already assumes the executor rebuilds the spooled result whenever the correlated params change. The new orca_material_rescan regression test pins the vulnerable plan shape - a correlated SubPlan whose nestloop inner side is a Material over a Motion-free ProjectSet computing UNNEST over an outer column - and verifies per-row results. Its tables are left unanalyzed on purpose: with default cardinalities ORCA places the ProjectSet under the inner-side Material, which is the shape this fix protects. --- .../translate/CTranslatorDXLToPlStmt.cpp | 81 +++----------- src/backend/optimizer/plan/orca.c | 71 ++++++++++++ .../gpopt/translate/CTranslatorDXLToPlStmt.h | 3 - .../regress/expected/orca_material_rescan.out | 101 ++++++++++++++++++ src/test/regress/greenplum_schedule | 2 +- src/test/regress/sql/orca_material_rescan.sql | 76 +++++++++++++ 6 files changed, 262 insertions(+), 72 deletions(-) create mode 100644 src/test/regress/expected/orca_material_rescan.out create mode 100644 src/test/regress/sql/orca_material_rescan.sql diff --git a/src/backend/gpopt/translate/CTranslatorDXLToPlStmt.cpp b/src/backend/gpopt/translate/CTranslatorDXLToPlStmt.cpp index 3abfd5fed5a..2e12a5e7aa8 100644 --- a/src/backend/gpopt/translate/CTranslatorDXLToPlStmt.cpp +++ b/src/backend/gpopt/translate/CTranslatorDXLToPlStmt.cpp @@ -550,34 +550,6 @@ CTranslatorDXLToPlStmt::TranslateDXLOperatorToPlan( return plan; } -//--------------------------------------------------------------------------- -// @function: -// CTranslatorDXLToPlStmt::SetParamIds -// -// @doc: -// Set the bitmapset with the param_ids defined in the plan -// -//--------------------------------------------------------------------------- -void -CTranslatorDXLToPlStmt::SetParamIds(Plan *plan) -{ - List *params_node_list = gpdb::ExtractNodesPlan( - plan, T_Param, true /* descend_into_subqueries */); - - ListCell *lc = nullptr; - - Bitmapset *bitmapset = nullptr; - - ForEach(lc, params_node_list) - { - Param *param = (Param *) lfirst(lc); - bitmapset = gpdb::BmsAddMember(bitmapset, param->paramid); - } - - plan->extParam = bitmapset; - plan->allParam = bitmapset; -} - List * CTranslatorDXLToPlStmt::TranslatePartOids(IMdIdArray *parts, INT lockmode) { @@ -730,7 +702,6 @@ CTranslatorDXLToPlStmt::TranslateDXLTblScan( // translate operator costs TranslatePlanCosts(tbl_scan_dxlnode, plan); - SetParamIds(plan); return plan_return; } @@ -835,7 +806,6 @@ CTranslatorDXLToPlStmt::TranslateDXLParallelTblScan( plan->plan_rows = ceil(plan->plan_rows / parallel_workers); } - SetParamIds(plan); return plan_return; } @@ -1027,7 +997,6 @@ CTranslatorDXLToPlStmt::TranslateDXLIndexScan( * As of 8.4, the indexstrategy and indexsubtype fields are no longer * available or needed in IndexScan. Ignore them. */ - SetParamIds(plan); return (Plan *) index_scan; } @@ -1173,7 +1142,6 @@ CTranslatorDXLToPlStmt::TranslateDXLIndexOnlyScan( } index_scan->indexqual = index_cond; - SetParamIds(plan); return (Plan *) index_scan; } @@ -1513,7 +1481,6 @@ CTranslatorDXLToPlStmt::TranslateDXLLimit( limit->limitOffset = limit_offset; } - SetParamIds(plan); // cleanup child_contexts->Release(); @@ -1751,7 +1718,6 @@ CTranslatorDXLToPlStmt::TranslateDXLHashJoin( plan->lefttree = left_plan; plan->righttree = right_plan; - SetParamIds(plan); // cleanup translation_context_arr_with_siblings->Release(); @@ -1844,7 +1810,6 @@ CTranslatorDXLToPlStmt::TranslateDXLTvf( } func_scan->functions = ListMake1(rtfunc); - SetParamIds(plan); return (Plan *) func_scan; } @@ -2218,7 +2183,6 @@ CTranslatorDXLToPlStmt::TranslateDXLNLJoin( } plan->lefttree = left_plan; plan->righttree = right_plan; - SetParamIds(plan); // cleanup translation_context_arr_with_siblings->Release(); @@ -2331,7 +2295,6 @@ CTranslatorDXLToPlStmt::TranslateDXLMergeJoin( plan->lefttree = left_plan; plan->righttree = right_plan; - SetParamIds(plan); merge_join->mergeFamilies = (Oid *) gpdb::GPDBAlloc(sizeof(Oid) * num_join_conds); @@ -2438,7 +2401,6 @@ CTranslatorDXLToPlStmt::TranslateDXLHash( plan->qual = NIL; hash->rescannable = false; - SetParamIds(plan); return (Plan *) hash; } @@ -2734,7 +2696,6 @@ CTranslatorDXLToPlStmt::TranslateDXLMotion( plan->plan_rows = ceil(plan->plan_rows / sendslice->parallel_workers); } - SetParamIds(plan); return (Plan *) motion; } @@ -2867,7 +2828,6 @@ CTranslatorDXLToPlStmt::TranslateDXLRedistributeMotionToResultHashFilters( plan->lefttree = child_plan; - SetParamIds(plan); Plan *child_result = (Plan *) result; @@ -2920,7 +2880,6 @@ CTranslatorDXLToPlStmt::TranslateDXLRedistributeMotionToResultHashFilters( plan->qual = NIL; plan->lefttree = child_result; - SetParamIds(plan); return (Plan *) result; } @@ -3195,7 +3154,6 @@ CTranslatorDXLToPlStmt::TranslateDXLAgg( m_dxl_to_plstmt_context->ResetAggInfosAndTransInfos(); - SetParamIds(plan); // cleanup child_contexts->Release(); @@ -3525,7 +3483,6 @@ CTranslatorDXLToPlStmt::TranslateDXLWindowAgg( } } - SetParamIds(plan); // cleanup child_contexts->Release(); @@ -3729,7 +3686,6 @@ CTranslatorDXLToPlStmt::TranslateDXLWindowHashAgg( } } - SetParamIds(plan); // cleanup child_contexts->Release(); @@ -3803,7 +3759,6 @@ CTranslatorDXLToPlStmt::TranslateDXLSort( TranslateSortCols(sort_col_list_dxl, &child_context, sort->sortColIdx, sort->sortOperators, sort->collations, sort->nullsFirst); - SetParamIds(plan); // cleanup child_contexts->Release(); @@ -3924,7 +3879,6 @@ CTranslatorDXLToPlStmt::TranslateDXLProjectSet(const CDXLNode *result_dxlnode) // translate operator costs TranslatePlanCosts(result_dxlnode, plan); - SetParamIds(plan); return (Plan *) project_set; } @@ -4272,7 +4226,6 @@ CTranslatorDXLToPlStmt::TranslateDXLResult( plan->qual = quals_list; result->resconstantqual = (Node *) one_time_quals_list; - SetParamIds(plan); // Creating project set nodes plan tree Plan *project_set_parent_plan = CreateProjectSetNodeTree( @@ -4393,7 +4346,6 @@ CTranslatorDXLToPlStmt::TranslateDXLPartSelector( partition_selector->part_prune_info = MakeNode(PartitionPruneInfo); partition_selector->part_prune_info->prune_infos = prune_infos; - SetParamIds(plan); // cleanup child_contexts->Release(); @@ -4566,7 +4518,6 @@ CTranslatorDXLToPlStmt::TranslateDXLAppend( nullptr, // translate context for the base table child_contexts, output_context); - SetParamIds(plan); // cleanup child_contexts->Release(); @@ -4597,8 +4548,6 @@ CTranslatorDXLToPlStmt::TranslateDXLMaterialize( CDXLPhysicalMaterialize::Cast(materialize_dxlnode->GetOperator()); materialize->cdb_strict = materialize_dxlop->IsEager(); - // ensure that executor actually materializes results - materialize->cdb_shield_child_from_rescans = true; // translate operator costs TranslatePlanCosts(materialize_dxlnode, plan); @@ -4628,7 +4577,19 @@ CTranslatorDXLToPlStmt::TranslateDXLMaterialize( plan->lefttree = child_plan; - SetParamIds(plan); + // Shield the child from rescans only if there is a Motion somewhere in + // the subtree: Motions cannot be rescanned, so the Material must then + // keep its tuplestore across rescans and squelching must not propagate + // below it. For a Motion-free subtree rely on normal executor + // semantics instead; in particular, if the subtree refers to exec + // params of an enclosing SubPlan, the materialized result must be + // discarded and rebuilt whenever those params change (chgParam), and + // shielding it would add no benefit while making that dependency + // fragile. + List *child_motions = + gpdb::ExtractNodesPlan(child_plan, T_Motion, + true /* descendIntoSubqueries */); + materialize->cdb_shield_child_from_rescans = (NIL != child_motions); // cleanup child_contexts->Release(); @@ -4688,7 +4649,6 @@ CTranslatorDXLToPlStmt::TranslateDXLCTEProducerToSharedScan( plan->lefttree = child_plan; plan->qual = NIL; - SetParamIds(plan); // cleanup child_contexts->Release(); @@ -4781,7 +4741,6 @@ CTranslatorDXLToPlStmt::TranslateDXLCTEConsumerToSharedScan( plan->qual = nullptr; - SetParamIds(plan); // DON'T REMOVE, if current consumer need projection, then we can direct add it. // we still keep the path of projection in consumer @@ -4847,7 +4806,6 @@ CTranslatorDXLToPlStmt::TranslateDXLSequence( nullptr, // base table translation context child_contexts, output_context); - SetParamIds(plan); // cleanup child_contexts->Release(); @@ -4941,7 +4899,6 @@ CTranslatorDXLToPlStmt::TranslateDXLDynTblScan( security_query_quals = gpdb::ListConcat(security_query_quals, query_quals); plan->qual = security_query_quals; - SetParamIds(plan); return plan; } @@ -5027,7 +4984,6 @@ CTranslatorDXLToPlStmt::TranslateDXLDynIdxOnlyScan( dyn_idx_only_scan->indexscan.indexqual = index_cond; - SetParamIds(plan); return (Plan *) dyn_idx_only_scan; } @@ -5108,7 +5064,6 @@ CTranslatorDXLToPlStmt::TranslateDXLDynIdxScan( dyn_idx_only_scan->indexscan.indexqual = index_cond; dyn_idx_only_scan->indexscan.indexqualorig = index_orig_cond; - SetParamIds(plan); return (Plan *) dyn_idx_only_scan; } @@ -5279,7 +5234,6 @@ CTranslatorDXLToPlStmt::TranslateDXLDynForeignScan( // translate operator costs TranslatePlanCosts(dyn_foreign_scan_dxlnode, plan); - SetParamIds(plan); return plan; } @@ -5441,7 +5395,6 @@ CTranslatorDXLToPlStmt::TranslateDXLDml( result_plan->lefttree = child_plan; result_plan->targetlist = dml_target_list; - SetParamIds(result_plan); if (m_cmd_type == CMD_UPDATE && isSplit) { @@ -5454,7 +5407,6 @@ CTranslatorDXLToPlStmt::TranslateDXLDml( final_result->resconstantqual = (Node *) gpdb::LAppend(NIL, gpdb::MakeBoolConst(true /*value*/, false /*isnull*/)); - SetParamIds(final_result_plan); result = final_result; result_plan = final_result_plan; @@ -5484,7 +5436,6 @@ CTranslatorDXLToPlStmt::TranslateDXLDml( plan->targetlist = NIL; plan->plan_node_id = m_dxl_to_plstmt_context->GetNextPlanId(); - SetParamIds(plan); if (m_is_tgt_tbl_distributed) { @@ -5731,7 +5682,6 @@ CTranslatorDXLToPlStmt::TranslateDXLSplit( plan->lefttree = child_plan; plan->plan_node_id = m_dxl_to_plstmt_context->GetNextPlanId(); - SetParamIds(plan); // cleanup child_contexts->Release(); @@ -5812,7 +5762,6 @@ CTranslatorDXLToPlStmt::TranslateDXLAssert( GPOS_ASSERT(gpdb::ListLength(plan->qual) == gpdb::ListLength(assert_node->errmessage)); - SetParamIds(plan); // cleanup child_contexts->Release(); @@ -6851,7 +6800,6 @@ CTranslatorDXLToPlStmt::TranslateDXLCtas( child_contexts, output_context); SetVarTypMod(phy_ctas_dxlop, target_list); - SetParamIds(plan); // cleanup child_contexts->Release(); @@ -6875,7 +6823,6 @@ CTranslatorDXLToPlStmt::TranslateDXLCtas( result_plan->lefttree = plan; result_plan->targetlist = target_list; - SetParamIds(result_plan); plan = (Plan *) result; @@ -7109,7 +7056,6 @@ CTranslatorDXLToPlStmt::TranslateDXLBitmapTblScan( bitmap_tbl_scan->scan.plan.lefttree = TranslateDXLBitmapAccessPath( bitmap_access_path_dxlnode, output_context, md_rel, table_descr, &base_table_context, ctxt_translation_prev_siblings, bitmap_tbl_scan); - SetParamIds(plan); if (is_dynamic) { @@ -7279,7 +7225,6 @@ CTranslatorDXLToPlStmt::TranslateDXLBitmapIndexProbe( * As of 8.4, the indexstrategy and indexsubtype fields are no longer * available or needed in IndexScan. Ignore them. */ - SetParamIds(plan); return plan; } diff --git a/src/backend/optimizer/plan/orca.c b/src/backend/optimizer/plan/orca.c index cc5a70639cf..9a6d3e1f219 100644 --- a/src/backend/optimizer/plan/orca.c +++ b/src/backend/optimizer/plan/orca.c @@ -45,6 +45,8 @@ #include "utils/syscache.h" #include "catalog/pg_proc.h" #include "catalog/pg_namespace.h" +#include "cdb/cdbllize.h" +#include "optimizer/walkers.h" /* GPORCA entry point */ extern PlannedStmt * GPOPTOptimizedPlan(Query *parse, bool *had_unexpected_failure, OptimizerOptions *opts); @@ -53,6 +55,7 @@ static Plan *remove_redundant_results(PlannerInfo *root, Plan *plan); static Node *remove_redundant_results_mutator(Node *node, void *); static bool can_replace_tlist(Plan *plan); static Node *push_down_expr_mutator(Node *node, List *child_tlist); +static bool set_plan_param_bitmaps_walker(Node *node, void *context); /* * Logging of optimization outcome @@ -352,6 +355,28 @@ optimize_query(Query *parse, int cursorOptions, ParamListInfo boundParams, Optim result->planTree = remove_redundant_results(root, result->planTree); + /* + * Compute the extParam/allParam bitmapsets of every Plan node, for the + * final shape of the plan. + * + * Rescan correctness depends on these: the executor propagates chgParam + * to a child only if the child's allParam contains the changed param + * (see UpdateChangedParamSet()), and e.g. a Material node relies on that + * to discard its tuplestore when a param of an enclosing SubPlan + * changes; a stale bitmap silently yields wrong results. Plans from the + * Postgres planner get these bits from SS_finalize_plan(); for ORCA + * plans we compute them here in one authoritative pass, instead of + * relying on every translator function to fill them in piecemeal. + */ + { + plan_tree_base_prefix ctx; + + ctx.node = (Node *) root; + foreach(lp, glob->subplans) + set_plan_param_bitmaps_walker((Node *) lfirst(lp), &ctx); + set_plan_param_bitmaps_walker((Node *) result->planTree, &ctx); + } + /* * To save on memory, and on the network bandwidth when the plan is * dispatched to QEs, strip all subquery RTEs of the original Query @@ -412,6 +437,52 @@ optimize_query(Query *parse, int cursorOptions, ParamListInfo boundParams, Optim return result; } +/* + * Set the extParam/allParam bitmapsets of every Plan node to the set of + * PARAM_EXEC params referenced in the node's subtree. + * + * SubPlan bodies are not entered here (extract_nodes_plan() collects the + * params of a SubPlan's testexpr and args, but does not descend into the + * body); the caller walks each entry of glob->subplans separately instead, + * so every plan node in the tree gets its bitmaps set exactly once. + */ +static bool +set_plan_param_bitmaps_walker(Node *node, void *context) +{ + if (node == NULL) + return false; + + if (is_plan_node(node)) + { + Plan *plan = (Plan *) node; + List *params; + Bitmapset *bms = NULL; + ListCell *lc; + + params = extract_nodes_plan(plan, T_Param, + true /* descendIntoSubqueries */); + foreach(lc, params) + { + Param *param = (Param *) lfirst(lc); + + /* + * extParam/allParam must contain only PARAM_EXEC paramids (see + * plannodes.h); PARAM_EXTERN params live in a separate numbering + * space and never change during execution. + */ + if (param->paramkind == PARAM_EXEC) + bms = bms_add_member(bms, param->paramid); + } + list_free(params); + + plan->extParam = bms; + plan->allParam = bms; + } + + return plan_tree_walker(node, set_plan_param_bitmaps_walker, context, + false /* recurse_into_subplans */); +} + /* * ORCA tends to generate gratuitous Result nodes for various reasons. We * try to clean it up here, as much as we can, by eliminating the Results diff --git a/src/include/gpopt/translate/CTranslatorDXLToPlStmt.h b/src/include/gpopt/translate/CTranslatorDXLToPlStmt.h index 3cd5fc9d638..f1f9df67076 100644 --- a/src/include/gpopt/translate/CTranslatorDXLToPlStmt.h +++ b/src/include/gpopt/translate/CTranslatorDXLToPlStmt.h @@ -190,9 +190,6 @@ class CTranslatorDXLToPlStmt static JoinType GetGPDBJoinTypeFromDXLJoinType(EdxlJoinType join_type); private: - // Set the bitmapset of a plan to the list of param_ids defined by the plan - static void SetParamIds(Plan *); - static List *TranslatePartOids(IMdIdArray *parts, INT lockmode); static List *TranslateJoinPruneParamids( diff --git a/src/test/regress/expected/orca_material_rescan.out b/src/test/regress/expected/orca_material_rescan.out new file mode 100644 index 00000000000..96e1ac3f5a3 --- /dev/null +++ b/src/test/regress/expected/orca_material_rescan.out @@ -0,0 +1,101 @@ +-- +-- Test rescan of Materialize in ORCA-translated correlated SubPlans. +-- +-- A Materialize sitting above a subtree that consumes a param of an +-- enclosing SubPlan (here: a ProjectSet computing UNNEST over an outer +-- column) must discard its tuplestore and rebuild whenever the param +-- changes. The executor propagates chgParam to a child only if the +-- child's allParam bitmapset contains the changed param, so this hangs +-- on the ORCA translator producing correct param bitmaps for every +-- node, and on the Materialize not shielding a Motion-free child from +-- rescans. When either is broken, the Materialize replays the first +-- outer row's result for every subsequent row. GPDB 7.5.x shipped +-- exactly this class of wrong-results regression. +-- +-- The tables deliberately have no stats: with default cardinalities +-- ORCA puts the ProjectSet on the nestloop inner side underneath a +-- Materialize, which is the vulnerable plan shape. Keep them +-- unanalyzed. +-- +CREATE SCHEMA orca_material_rescan; +SET search_path TO orca_material_rescan; +CREATE TABLE ae_event ( + seq_nvl numeric(10) NOT NULL, + ae_rule_id_ctn varchar(200) NULL, + ae_occr_dtm timestamp NULL +) USING ao_row DISTRIBUTED RANDOMLY; +CREATE TABLE ae_rule ( + ae_rule_id varchar(50) NOT NULL, + ae_rule_nm varchar(200) NULL +) USING ao_row DISTRIBUTED RANDOMLY; +INSERT INTO ae_rule VALUES + ('R_01', 'Overheating'), + ('R_02', NULL), + ('R_03', 'Calibration Error'), + ('R_04', 'Sensor Offline'); +-- Edge cases: rule id lists with a duplicate, an id with no match in +-- ae_rule, and empty lists. Every row must get its own aggregate, not +-- a replay of the first row's. +INSERT INTO ae_event VALUES + (1, 'R_01,R_02', '2026-06-10 08:59:49'), + (2, 'R_03', '2026-06-10 08:59:50'), + (3, '', '2026-06-10 08:59:51'), + (4, 'R_04,R_99', '2026-06-10 08:59:52'), + (5, 'R_01,R_01', '2026-06-10 08:59:53'), + (6, '', '2026-06-10 08:59:54'); +SELECT $query$ +SELECT seq_nvl, ae_rule_id_ctn + , ( SELECT string_agg(rul_nm, ',' ORDER BY ae_rule_id) + FROM ( SELECT coalesce(ae_rule_nm, ae_rule_id) AS rul_nm, ae_rule_id + FROM ae_rule t1 + JOIN ( SELECT unnest(string_to_array(sh.ae_rule_id_ctn, ',')) ) t2(column2) + ON t1.ae_rule_id = t2.column2 ) t + ) AS rule_ctrl +FROM ae_event sh +WHERE sh.ae_occr_dtm >= '2026-06-10 06:00:00' + AND sh.ae_occr_dtm < '2026-06-11 06:00:00' +ORDER BY seq_nvl +$query$ AS qry \gset +SET optimizer TO on; +-- The shape under test: the SubPlan's nestloop inner side must be a +-- Materialize over a Motion-free ProjectSet consuming the outer param. +EXPLAIN (COSTS OFF) +:qry ; + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ + Sort + Sort Key: sh.seq_nvl + -> Result + -> Gather Motion 3:1 (slice1; segments: 3) + -> Seq Scan on ae_event sh + Filter: ((ae_occr_dtm >= 'Wed Jun 10 06:00:00 2026'::timestamp without time zone) AND (ae_occr_dtm < 'Thu Jun 11 06:00:00 2026'::timestamp without time zone)) + SubPlan 1 + -> Aggregate + -> Nested Loop + Join Filter: ((t1.ae_rule_id)::text = (unnest(string_to_array((sh.ae_rule_id_ctn)::text, ','::text)))) + -> Materialize + -> Gather Motion 3:1 (slice2; segments: 3) + -> Seq Scan on ae_rule t1 + -> Materialize + -> ProjectSet + -> Result + Optimizer: GPORCA +(17 rows) + +:qry ; + seq_nvl | ae_rule_id_ctn | rule_ctrl +---------+----------------+------------------------- + 1 | R_01,R_02 | Overheating,R_02 + 2 | R_03 | Calibration Error + 3 | | + 4 | R_04,R_99 | Sensor Offline + 5 | R_01,R_01 | Overheating,Overheating + 6 | | +(6 rows) + +RESET optimizer; +RESET search_path; +DROP SCHEMA orca_material_rescan CASCADE; +NOTICE: drop cascades to 2 other objects +DETAIL: drop cascades to table orca_material_rescan.ae_event +drop cascades to table orca_material_rescan.ae_rule diff --git a/src/test/regress/greenplum_schedule b/src/test/regress/greenplum_schedule index 84e8766844b..0cda6a6f5f7 100755 --- a/src/test/regress/greenplum_schedule +++ b/src/test/regress/greenplum_schedule @@ -64,7 +64,7 @@ test: leastsquares opr_sanity_gp decode_expr bitmapscan bitmapscan_ao case_gp li # below test(s) inject faults so each of them need to be in a separate group test: gpcopy -test: orca_static_pruning orca_groupingsets_fallbacks +test: orca_static_pruning orca_groupingsets_fallbacks orca_material_rescan test: filter gpctas gpdist gpdist_opclasses gpdist_legacy_opclasses matrix sublink table_functions olap_setup complex opclass_ddl information_schema guc_env_var gp_explain distributed_transactions explain_format olap_plans gp_copy_dtx # below test(s) inject faults so each of them need to be in a separate group test: guc_gp diff --git a/src/test/regress/sql/orca_material_rescan.sql b/src/test/regress/sql/orca_material_rescan.sql new file mode 100644 index 00000000000..bd11d5a3016 --- /dev/null +++ b/src/test/regress/sql/orca_material_rescan.sql @@ -0,0 +1,76 @@ +-- +-- Test rescan of Materialize in ORCA-translated correlated SubPlans. +-- +-- A Materialize sitting above a subtree that consumes a param of an +-- enclosing SubPlan (here: a ProjectSet computing UNNEST over an outer +-- column) must discard its tuplestore and rebuild whenever the param +-- changes. The executor propagates chgParam to a child only if the +-- child's allParam bitmapset contains the changed param, so this hangs +-- on the ORCA translator producing correct param bitmaps for every +-- node, and on the Materialize not shielding a Motion-free child from +-- rescans. When either is broken, the Materialize replays the first +-- outer row's result for every subsequent row. GPDB 7.5.x shipped +-- exactly this class of wrong-results regression. +-- +-- The tables deliberately have no stats: with default cardinalities +-- ORCA puts the ProjectSet on the nestloop inner side underneath a +-- Materialize, which is the vulnerable plan shape. Keep them +-- unanalyzed. +-- +CREATE SCHEMA orca_material_rescan; +SET search_path TO orca_material_rescan; + +CREATE TABLE ae_event ( + seq_nvl numeric(10) NOT NULL, + ae_rule_id_ctn varchar(200) NULL, + ae_occr_dtm timestamp NULL +) USING ao_row DISTRIBUTED RANDOMLY; + +CREATE TABLE ae_rule ( + ae_rule_id varchar(50) NOT NULL, + ae_rule_nm varchar(200) NULL +) USING ao_row DISTRIBUTED RANDOMLY; + +INSERT INTO ae_rule VALUES + ('R_01', 'Overheating'), + ('R_02', NULL), + ('R_03', 'Calibration Error'), + ('R_04', 'Sensor Offline'); + +-- Edge cases: rule id lists with a duplicate, an id with no match in +-- ae_rule, and empty lists. Every row must get its own aggregate, not +-- a replay of the first row's. +INSERT INTO ae_event VALUES + (1, 'R_01,R_02', '2026-06-10 08:59:49'), + (2, 'R_03', '2026-06-10 08:59:50'), + (3, '', '2026-06-10 08:59:51'), + (4, 'R_04,R_99', '2026-06-10 08:59:52'), + (5, 'R_01,R_01', '2026-06-10 08:59:53'), + (6, '', '2026-06-10 08:59:54'); + +SELECT $query$ +SELECT seq_nvl, ae_rule_id_ctn + , ( SELECT string_agg(rul_nm, ',' ORDER BY ae_rule_id) + FROM ( SELECT coalesce(ae_rule_nm, ae_rule_id) AS rul_nm, ae_rule_id + FROM ae_rule t1 + JOIN ( SELECT unnest(string_to_array(sh.ae_rule_id_ctn, ',')) ) t2(column2) + ON t1.ae_rule_id = t2.column2 ) t + ) AS rule_ctrl +FROM ae_event sh +WHERE sh.ae_occr_dtm >= '2026-06-10 06:00:00' + AND sh.ae_occr_dtm < '2026-06-11 06:00:00' +ORDER BY seq_nvl +$query$ AS qry \gset + +SET optimizer TO on; + +-- The shape under test: the SubPlan's nestloop inner side must be a +-- Materialize over a Motion-free ProjectSet consuming the outer param. +EXPLAIN (COSTS OFF) +:qry ; + +:qry ; + +RESET optimizer; +RESET search_path; +DROP SCHEMA orca_material_rescan CASCADE;