From d06e2c0eab94801ef4fd32c7886e876538674af7 Mon Sep 17 00:00:00 2001 From: Colm McHugh Date: Wed, 20 May 2026 08:30:05 +0000 Subject: [PATCH] Fix wrong results for WHERE on inheritance parent column with LEFT JOIN ON FALSE When a local inheritance parent table is cross-joined with a distributed table through LEFT JOIN ... ON FALSE, PostgreSQL's expand_single_inheritance_child() creates child RTEs via memcpy, duplicating Citus's identity marker (values_lists). This causes RelationRestrictionForRelation() to return the child's restriction instead of the parent's. Since Vars in plannerInfo->parse still reference the parent's original rtable position, RequiredAttrNumbersForRelationInternal() finds no matching Vars, causing all columns to be replaced with NULL. Fix by adding an originalRteIndex parameter to RequiredAttrNumbersForRelation() that also searches at the RTE's original position in the query's rtable when it differs from the restriction's index. Fixes: https://github.com/citusdata/citus/issues/8553 --- .../planner/local_distributed_join_planner.c | 39 +++++++++++++++++-- .../distributed/planner/recursive_planning.c | 3 +- .../local_distributed_join_planner.h | 3 +- src/test/regress/expected/local_dist_join.out | 32 +++++++++++++++ src/test/regress/sql/local_dist_join.sql | 21 ++++++++++ 5 files changed, 93 insertions(+), 5 deletions(-) diff --git a/src/backend/distributed/planner/local_distributed_join_planner.c b/src/backend/distributed/planner/local_distributed_join_planner.c index 7b44a9c219d..42a3e5cd22c 100644 --- a/src/backend/distributed/planner/local_distributed_join_planner.c +++ b/src/backend/distributed/planner/local_distributed_join_planner.c @@ -476,7 +476,8 @@ AppendUniqueIndexColumnsToList(Form_pg_index indexForm, List **uniqueIndexGroups */ List * RequiredAttrNumbersForRelation(RangeTblEntry *rangeTableEntry, - PlannerRestrictionContext *plannerRestrictionContext) + PlannerRestrictionContext *plannerRestrictionContext, + int originalRteIndex) { RelationRestriction *relationRestriction = RelationRestrictionForRelation(rangeTableEntry, plannerRestrictionContext); @@ -498,7 +499,35 @@ RequiredAttrNumbersForRelation(RangeTblEntry *rangeTableEntry, */ Query *queryToProcess = plannerInfo->parse; - return RequiredAttrNumbersForRelationInternal(queryToProcess, rteIndex); + List *result = RequiredAttrNumbersForRelationInternal(queryToProcess, rteIndex); + + /* + * When PostgreSQL expands inheritance tables, expand_single_inheritance_child() + * copies the parent RTE via memcpy, which duplicates Citus's identity marker + * (values_lists). This can cause RelationRestrictionForRelation() to return a + * restriction for a child RTE whose index differs from the original parent + * position. Since Vars in plannerInfo->parse still reference the parent's + * original position, we must also search at originalRteIndex to find them. + */ + if (originalRteIndex > 0 && originalRteIndex != rteIndex) + { + List *additional = RequiredAttrNumbersForRelationInternal(queryToProcess, + originalRteIndex); +#if PG_VERSION_NUM >= 170000 + foreach_int(attrNo, additional) + { + result = list_append_unique_int(result, attrNo); + } +#else + ListCell *lc; + foreach(lc, additional) + { + result = list_append_unique_int(result, lfirst_int(lc)); + } +#endif + } + + return result; } @@ -541,9 +570,12 @@ CreateConversionCandidates(PlannerRestrictionContext *plannerRestrictionContext, palloc0(sizeof(ConversionCandidates)); + int rangeTableIndex = 0; RangeTblEntry *rangeTableEntry = NULL; foreach_declared_ptr(rangeTableEntry, rangeTableList) { + rangeTableIndex++; + /* we're only interested in tables */ if (!IsRecursivelyPlannableRelation(rangeTableEntry)) { @@ -566,7 +598,8 @@ CreateConversionCandidates(PlannerRestrictionContext *plannerRestrictionContext, rangeTableEntryDetails->rangeTableEntry = rangeTableEntry; rangeTableEntryDetails->requiredAttributeNumbers = - RequiredAttrNumbersForRelation(rangeTableEntry, plannerRestrictionContext); + RequiredAttrNumbersForRelation(rangeTableEntry, plannerRestrictionContext, + rangeTableIndex); rangeTableEntryDetails->hasConstantFilterOnUniqueColumn = HasConstantFilterOnUniqueColumn(rangeTableEntry, relationRestriction); rangeTableEntryDetails->perminfo = NULL; diff --git a/src/backend/distributed/planner/recursive_planning.c b/src/backend/distributed/planner/recursive_planning.c index adc2c68140f..7875ebc7c76 100644 --- a/src/backend/distributed/planner/recursive_planning.c +++ b/src/backend/distributed/planner/recursive_planning.c @@ -971,7 +971,8 @@ RecursivelyPlanDistributedJoinNode(Node *node, Query *query, PlannerRestrictionContext *restrictionContext = GetPlannerRestrictionContext(recursivePlanningContext); List *requiredAttributes = - RequiredAttrNumbersForRelation(distributedRte, restrictionContext); + RequiredAttrNumbersForRelation(distributedRte, restrictionContext, + rangeTableRef->rtindex); RTEPermissionInfo *perminfo = NULL; if (distributedRte->perminfoindex) diff --git a/src/include/distributed/local_distributed_join_planner.h b/src/include/distributed/local_distributed_join_planner.h index 3390ab213eb..714e3dde969 100644 --- a/src/include/distributed/local_distributed_join_planner.h +++ b/src/include/distributed/local_distributed_join_planner.h @@ -33,7 +33,8 @@ extern void RecursivelyPlanLocalTableJoins(Query *query, RecursivePlanningContext *context); extern List * RequiredAttrNumbersForRelation(RangeTblEntry *relationRte, PlannerRestrictionContext * - plannerRestrictionContext); + plannerRestrictionContext, + int originalRteIndex); extern List * RequiredAttrNumbersForRelationInternal(Query *queryToProcess, int rteIndex); #endif /* LOCAL_DISTRIBUTED_JOIN_PLANNER_H */ diff --git a/src/test/regress/expected/local_dist_join.out b/src/test/regress/expected/local_dist_join.out index 68a71bbc1aa..5ae8ef59cdc 100644 --- a/src/test/regress/expected/local_dist_join.out +++ b/src/test/regress/expected/local_dist_join.out @@ -887,3 +887,35 @@ SELECT COUNT(DISTINCT name) FROM distributed; (1 row) ROLLBACK; +-- Test for inheritance parent column in WHERE with LEFT JOIN ON FALSE +-- Regression test for https://github.com/citusdata/citus/issues/8553 +-- When a local inheritance parent table is cross-joined with a distributed +-- table through LEFT JOIN ... ON FALSE, a WHERE clause on the parent column +-- should not incorrectly drop all rows. +SET citus.use_citus_managed_tables TO off; +CREATE TABLE inh_parent(c0 REAL); +CREATE TABLE inh_child(c1 INT) INHERITS (inh_parent); +RESET citus.use_citus_managed_tables; +INSERT INTO inh_child VALUES (1.0, 1); +-- This query should return 101 rows (1 child row x 101 distributed rows) +SELECT count(*) FROM distributed, inh_child LEFT JOIN inh_parent ON FALSE WHERE inh_child.c0 IS NOT NULL; + count +--------------------------------------------------------------------- + 101 +(1 row) + +-- Additional variations to test the same pattern +SELECT count(*) FROM distributed, inh_child LEFT JOIN inh_parent ON FALSE WHERE inh_child.c0 = 1; + count +--------------------------------------------------------------------- + 101 +(1 row) + +SELECT count(*) FROM distributed, inh_child LEFT JOIN inh_parent ON FALSE; + count +--------------------------------------------------------------------- + 101 +(1 row) + +DROP TABLE inh_child; +DROP TABLE inh_parent; diff --git a/src/test/regress/sql/local_dist_join.sql b/src/test/regress/sql/local_dist_join.sql index f92c3f2c95d..32773d5b696 100644 --- a/src/test/regress/sql/local_dist_join.sql +++ b/src/test/regress/sql/local_dist_join.sql @@ -341,3 +341,24 @@ WHERE distributed.id = local.id; SELECT COUNT(DISTINCT name) FROM distributed; ROLLBACK; + +-- Test for inheritance parent column in WHERE with LEFT JOIN ON FALSE +-- Regression test for https://github.com/citusdata/citus/issues/8553 +-- When a local inheritance parent table is cross-joined with a distributed +-- table through LEFT JOIN ... ON FALSE, a WHERE clause on the parent column +-- should not incorrectly drop all rows. +SET citus.use_citus_managed_tables TO off; +CREATE TABLE inh_parent(c0 REAL); +CREATE TABLE inh_child(c1 INT) INHERITS (inh_parent); +RESET citus.use_citus_managed_tables; +INSERT INTO inh_child VALUES (1.0, 1); + +-- This query should return 101 rows (1 child row x 101 distributed rows) +SELECT count(*) FROM distributed, inh_child LEFT JOIN inh_parent ON FALSE WHERE inh_child.c0 IS NOT NULL; + +-- Additional variations to test the same pattern +SELECT count(*) FROM distributed, inh_child LEFT JOIN inh_parent ON FALSE WHERE inh_child.c0 = 1; +SELECT count(*) FROM distributed, inh_child LEFT JOIN inh_parent ON FALSE; + +DROP TABLE inh_child; +DROP TABLE inh_parent;