From b8820410dd97b4f7eb08d522a9897f18e844b2e7 Mon Sep 17 00:00:00 2001 From: Hannu Krosing Date: Wed, 16 Sep 2026 13:12:52 +0000 Subject: [PATCH 1/4] Refactor set operation group clauses to decouple from targetlist positions Previously, generate_setop_child_grouplist, generate_setop_grouplist, and add_setop_child_rel_equivalences assumed that groupClauses had a strict 1:1 positional correspondence with all non-junk targetlist columns. This patch decouples set operation group clauses from targetlist positions by matching them through SortGroupClause.tleSortGroupRef. This prepares the planner and equivalence class machinery for set operations that group on a subset of columns. --- src/backend/optimizer/path/equivclass.c | 35 +++++++------- src/backend/optimizer/plan/planner.c | 58 ++++++++++++++---------- src/backend/optimizer/prep/prepunion.c | 24 +++------- src/backend/parser/analyze.c | 9 +++- src/backend/rewrite/rewriteSearchCycle.c | 21 ++++++--- 5 files changed, 80 insertions(+), 67 deletions(-) diff --git a/src/backend/optimizer/path/equivclass.c b/src/backend/optimizer/path/equivclass.c index 393a7a69742..7dec27d6645 100644 --- a/src/backend/optimizer/path/equivclass.c +++ b/src/backend/optimizer/path/equivclass.c @@ -3038,30 +3038,31 @@ add_setop_child_rel_equivalences(PlannerInfo *root, RelOptInfo *child_rel, List *child_tlist, List *setop_pathkeys) { ListCell *lc; - ListCell *lc2 = list_head(setop_pathkeys); - foreach(lc, child_tlist) + foreach(lc, setop_pathkeys) { - TargetEntry *tle = lfirst_node(TargetEntry, lc); + PathKey *pk = lfirst_node(PathKey, lc); + Index ref = pk->pk_eclass->ec_sortref; + TargetEntry *tle = NULL; + ListCell *lc2; EquivalenceMember *parent_em; - PathKey *pk; - if (tle->resjunk) - continue; + /* Find matching non-resjunk tle in child_tlist */ + foreach(lc2, child_tlist) + { + TargetEntry *cur_tle = lfirst_node(TargetEntry, lc2); + if (cur_tle->ressortgroupref == ref && !cur_tle->resjunk) + { + tle = cur_tle; + break; + } + } - if (lc2 == NULL) - elog(ERROR, "too few pathkeys for set operation"); + if (tle == NULL) + elog(ERROR, "could not find target entry for setop pathkey ref %d", ref); - pk = lfirst_node(PathKey, lc2); parent_em = linitial(pk->pk_eclass->ec_members); - /* - * We can safely pass the parent member as the first member in the - * ec_members list as this is added first in generate_union_paths, - * likewise, the JoinDomain can be that of the initial member of the - * Pathkey's EquivalenceClass. We pass -1 for ec_index since we - * maintain the eclass_indexes for the child_rel after the loop. - */ add_child_eq_member(root, pk->pk_eclass, -1, @@ -3071,8 +3072,6 @@ add_setop_child_rel_equivalences(PlannerInfo *root, RelOptInfo *child_rel, parent_em, exprType((Node *) tle->expr), child_rel->relid); - - lc2 = lnext(setop_pathkeys, lc2); } /* diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index c3c158a253d..50d5a140375 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -8591,49 +8591,59 @@ group_by_has_partkey(RelOptInfo *input_rel, * then we return an empty list. This may leave some TLEs with unreferenced * ressortgroupref markings, but that's harmless. */ +static TargetEntry * +get_nth_nonjunk_tle(List *tlist, int n) +{ + ListCell *lc; + int count = 0; + + foreach(lc, tlist) + { + TargetEntry *tle = lfirst_node(TargetEntry, lc); + + if (!tle->resjunk) + { + count++; + if (count == n) + return tle; + } + } + return NULL; +} + static List * generate_setop_child_grouplist(SetOperationStmt *op, List *targetlist) { List *grouplist = copyObject(op->groupClauses); ListCell *lg; - ListCell *lt; - ListCell *ct; - lg = list_head(grouplist); - ct = list_head(op->colTypes); - foreach(lt, targetlist) + foreach(lg, grouplist) { - TargetEntry *tle = (TargetEntry *) lfirst(lt); - SortGroupClause *sgc; + SortGroupClause *sgc = (SortGroupClause *) lfirst(lg); + Index ref = sgc->tleSortGroupRef; + TargetEntry *tle; Oid coltype; - /* resjunk columns could have sortgrouprefs. Leave these alone */ - if (tle->resjunk) - continue; + /* If tleSortGroupRef is not set, we can't map it. */ + if (ref == 0) + elog(ERROR, "missing tleSortGroupRef in setop groupClause"); - /* - * We expect every non-resjunk target to have a SortGroupClause and - * colTypes. - */ - Assert(lg != NULL); - Assert(ct != NULL); - sgc = (SortGroupClause *) lfirst(lg); - coltype = lfirst_oid(ct); + tle = get_nth_nonjunk_tle(targetlist, ref); + if (tle == NULL) + elog(ERROR, "missing target entry for setop groupClause ref %d", ref); + + /* We also need to get the type from op->colTypes */ + Assert(ref <= list_length(op->colTypes)); + coltype = list_nth_oid(op->colTypes, ref - 1); /* reject if target type isn't the same as the setop target type */ if (coltype != exprType((Node *) tle->expr)) return NIL; - lg = lnext(grouplist, lg); - ct = lnext(op->colTypes, ct); - /* assign a tleSortGroupRef, or reuse the existing one */ sgc->tleSortGroupRef = assignSortGroupRef(tle, targetlist); } - Assert(lg == NULL); - Assert(ct == NULL); - return grouplist; } diff --git a/src/backend/optimizer/prep/prepunion.c b/src/backend/optimizer/prep/prepunion.c index b136f12ff3b..1efd0d915b7 100644 --- a/src/backend/optimizer/prep/prepunion.c +++ b/src/backend/optimizer/prep/prepunion.c @@ -1719,28 +1719,18 @@ generate_setop_grouplist(SetOperationStmt *op, List *targetlist) { List *grouplist = copyObject(op->groupClauses); ListCell *lg; - ListCell *lt; - lg = list_head(grouplist); - foreach(lt, targetlist) + foreach(lg, grouplist) { - TargetEntry *tle = (TargetEntry *) lfirst(lt); - SortGroupClause *sgc; + SortGroupClause *sgc = (SortGroupClause *) lfirst(lg); + Index ref = sgc->tleSortGroupRef; + TargetEntry *tle; + Assert(ref > 0 && ref <= list_length(targetlist)); + tle = list_nth(targetlist, ref - 1); Assert(!tle->resjunk); - - /* non-resjunk columns should have sortgroupref = resno */ - Assert(tle->ressortgroupref == tle->resno); - - /* non-resjunk columns should have grouping clauses */ - Assert(lg != NULL); - sgc = (SortGroupClause *) lfirst(lg); - lg = lnext(grouplist, lg); - Assert(sgc->tleSortGroupRef == 0); - - sgc->tleSortGroupRef = tle->ressortgroupref; + Assert(tle->ressortgroupref == ref); } - Assert(lg == NULL); return grouplist; } diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 263d1b6e1cc..e89f4684ade 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -2604,6 +2604,7 @@ constructSetOpTargetlist(ParseState *pstate, SetOperationStmt *op, { ListCell *ltl; ListCell *rtl; + int resno = 1; /* * Verify that the two children have the same number of non-junk columns, @@ -2723,13 +2724,15 @@ constructSetOpTargetlist(ParseState *pstate, SetOperationStmt *op, if (op->op != SETOP_UNION || !op->all) { ParseCallbackState pcbstate; + SortGroupClause *grpcl; setup_parser_errposition_callback(&pcbstate, pstate, bestlocation); /* If it's a recursive union, we need to require hashing support. */ - op->groupClauses = lappend(op->groupClauses, - makeSortGroupClauseForSetOp(rescoltype, recursive)); + grpcl = makeSortGroupClauseForSetOp(rescoltype, recursive); + grpcl->tleSortGroupRef = resno; + op->groupClauses = lappend(op->groupClauses, grpcl); cancel_parser_errposition_callback(&pcbstate); } @@ -2754,6 +2757,8 @@ constructSetOpTargetlist(ParseState *pstate, SetOperationStmt *op, false); *targetlist = lappend(*targetlist, restle); } + + resno++; } } diff --git a/src/backend/rewrite/rewriteSearchCycle.c b/src/backend/rewrite/rewriteSearchCycle.c index 75943072817..524a265a4c9 100644 --- a/src/backend/rewrite/rewriteSearchCycle.c +++ b/src/backend/rewrite/rewriteSearchCycle.c @@ -606,8 +606,11 @@ rewriteSearchAndCycle(CommonTableExpr *cte) sos->colTypmods = lappend_int(sos->colTypmods, -1); sos->colCollations = lappend_oid(sos->colCollations, InvalidOid); if (!sos->all) - sos->groupClauses = lappend(sos->groupClauses, - makeSortGroupClauseForSetOp(search_seq_type, true)); + { + SortGroupClause *sgc = makeSortGroupClauseForSetOp(search_seq_type, true); + sgc->tleSortGroupRef = list_length(sos->colTypes); + sos->groupClauses = lappend(sos->groupClauses, sgc); + } } if (cte->cycle_clause) { @@ -615,15 +618,21 @@ rewriteSearchAndCycle(CommonTableExpr *cte) sos->colTypmods = lappend_int(sos->colTypmods, cte->cycle_clause->cycle_mark_typmod); sos->colCollations = lappend_oid(sos->colCollations, cte->cycle_clause->cycle_mark_collation); if (!sos->all) - sos->groupClauses = lappend(sos->groupClauses, - makeSortGroupClauseForSetOp(cte->cycle_clause->cycle_mark_type, true)); + { + SortGroupClause *sgc = makeSortGroupClauseForSetOp(cte->cycle_clause->cycle_mark_type, true); + sgc->tleSortGroupRef = list_length(sos->colTypes); + sos->groupClauses = lappend(sos->groupClauses, sgc); + } sos->colTypes = lappend_oid(sos->colTypes, RECORDARRAYOID); sos->colTypmods = lappend_int(sos->colTypmods, -1); sos->colCollations = lappend_oid(sos->colCollations, InvalidOid); if (!sos->all) - sos->groupClauses = lappend(sos->groupClauses, - makeSortGroupClauseForSetOp(RECORDARRAYOID, true)); + { + SortGroupClause *sgc = makeSortGroupClauseForSetOp(RECORDARRAYOID, true); + sgc->tleSortGroupRef = list_length(sos->colTypes); + sos->groupClauses = lappend(sos->groupClauses, sgc); + } } /* -- 2.55.0.1082.g2b9226bbc0-goog