Introduce an RTE for the grouping step
authorRichard Guo <rguo@postgresql.org>
Tue, 10 Sep 2024 03:35:34 +0000 (12:35 +0900)
committerRichard Guo <rguo@postgresql.org>
Tue, 10 Sep 2024 03:35:34 +0000 (12:35 +0900)
If there are subqueries in the grouping expressions, each of these
subqueries in the targetlist and HAVING clause is expanded into
distinct SubPlan nodes.  As a result, only one of these SubPlan nodes
would be converted to reference to the grouping key column output by
the Agg node; others would have to get evaluated afresh.  This is not
efficient, and with grouping sets this can cause wrong results issues
in cases where they should go to NULL because they are from the wrong
grouping set.  Furthermore, during re-evaluation, these SubPlan nodes
might use nulled column values from grouping sets, which is not
correct.

This issue is not limited to subqueries.  For other types of
expressions that are part of grouping items, if they are transformed
into another form during preprocessing, they may fail to match lower
target items.  This can also lead to wrong results with grouping sets.

To fix this issue, we introduce a new kind of RTE representing the
output of the grouping step, with columns that are the Vars or
expressions being grouped on.  In the parser, we replace the grouping
expressions in the targetlist and HAVING clause with Vars referencing
this new RTE, so that the output of the parser directly expresses the
semantic requirement that the grouping expressions be gotten from the
grouping output rather than computed some other way.  In the planner,
we first preprocess all the columns of this new RTE and then replace
any Vars in the targetlist and HAVING clause that reference this new
RTE with the underlying grouping expressions, so that we will have
only one instance of a SubPlan node for each subquery contained in the
grouping expressions.

Bump catversion because this changes the querytree produced by the
parser.

Thanks to Tom Lane for the idea to invent a new kind of RTE.

Per reports from Geoff Winkless, Tobias Wendorff, Richard Guo from
various threads.

Author: Richard Guo
Reviewed-by: Ashutosh Bapat, Sutou Kouhei
Discussion: https://postgr.es/m/CAMbWs4_dp7e7oTwaiZeBX8+P1rXw4ThkZxh1QG81rhu9Z47VsQ@mail.gmail.com

25 files changed:
src/backend/commands/explain.c
src/backend/nodes/nodeFuncs.c
src/backend/nodes/outfuncs.c
src/backend/nodes/print.c
src/backend/nodes/readfuncs.c
src/backend/optimizer/path/allpaths.c
src/backend/optimizer/plan/planner.c
src/backend/optimizer/plan/setrefs.c
src/backend/optimizer/prep/prepjointree.c
src/backend/optimizer/util/var.c
src/backend/parser/parse_agg.c
src/backend/parser/parse_relation.c
src/backend/parser/parse_target.c
src/backend/utils/adt/ruleutils.c
src/include/catalog/catversion.h
src/include/commands/explain.h
src/include/nodes/nodeFuncs.h
src/include/nodes/parsenodes.h
src/include/nodes/pathnodes.h
src/include/optimizer/optimizer.h
src/include/parser/parse_node.h
src/include/parser/parse_relation.h
src/test/regress/expected/groupingsets.out
src/test/regress/sql/groupingsets.sql
src/tools/pgindent/typedefs.list

index 11df4a04d430e2d6793eaf2ea08544b7845a42ae..14cd36c87c5a00604ce959e17c3005d683581341 100644 (file)
@@ -879,6 +879,7 @@ ExplainPrintPlan(ExplainState *es, QueryDesc *queryDesc)
 {
        Bitmapset  *rels_used = NULL;
        PlanState  *ps;
+       ListCell   *lc;
 
        /* Set up ExplainState fields associated with this plan tree */
        Assert(queryDesc->plannedstmt != NULL);
@@ -889,6 +890,17 @@ ExplainPrintPlan(ExplainState *es, QueryDesc *queryDesc)
        es->deparse_cxt = deparse_context_for_plan_tree(queryDesc->plannedstmt,
                                                                                                        es->rtable_names);
        es->printed_subplans = NULL;
+       es->rtable_size = list_length(es->rtable);
+       foreach(lc, es->rtable)
+       {
+               RangeTblEntry *rte = lfirst_node(RangeTblEntry, lc);
+
+               if (rte->rtekind == RTE_GROUP)
+               {
+                       es->rtable_size--;
+                       break;
+               }
+       }
 
        /*
         * Sometimes we mark a Gather node as "invisible", which means that it's
@@ -2474,7 +2486,7 @@ show_plan_tlist(PlanState *planstate, List *ancestors, ExplainState *es)
        context = set_deparse_context_plan(es->deparse_cxt,
                                                                           plan,
                                                                           ancestors);
-       useprefix = list_length(es->rtable) > 1;
+       useprefix = es->rtable_size > 1;
 
        /* Deparse each result column (we now include resjunk ones) */
        foreach(lc, plan->targetlist)
@@ -2558,7 +2570,7 @@ show_upper_qual(List *qual, const char *qlabel,
 {
        bool            useprefix;
 
-       useprefix = (list_length(es->rtable) > 1 || es->verbose);
+       useprefix = (es->rtable_size > 1 || es->verbose);
        show_qual(qual, qlabel, planstate, ancestors, useprefix, es);
 }
 
@@ -2648,7 +2660,7 @@ show_grouping_sets(PlanState *planstate, Agg *agg,
        context = set_deparse_context_plan(es->deparse_cxt,
                                                                           planstate->plan,
                                                                           ancestors);
-       useprefix = (list_length(es->rtable) > 1 || es->verbose);
+       useprefix = (es->rtable_size > 1 || es->verbose);
 
        ExplainOpenGroup("Grouping Sets", "Grouping Sets", false, es);
 
@@ -2788,7 +2800,7 @@ show_sort_group_keys(PlanState *planstate, const char *qlabel,
        context = set_deparse_context_plan(es->deparse_cxt,
                                                                           plan,
                                                                           ancestors);
-       useprefix = (list_length(es->rtable) > 1 || es->verbose);
+       useprefix = (es->rtable_size > 1 || es->verbose);
 
        for (keyno = 0; keyno < nkeys; keyno++)
        {
@@ -2900,7 +2912,7 @@ show_tablesample(TableSampleClause *tsc, PlanState *planstate,
        context = set_deparse_context_plan(es->deparse_cxt,
                                                                           planstate->plan,
                                                                           ancestors);
-       useprefix = list_length(es->rtable) > 1;
+       useprefix = es->rtable_size > 1;
 
        /* Get the tablesample method name */
        method_name = get_func_name(tsc->tsmhandler);
@@ -3386,7 +3398,7 @@ show_memoize_info(MemoizeState *mstate, List *ancestors, ExplainState *es)
         * It's hard to imagine having a memoize node with fewer than 2 RTEs, but
         * let's just keep the same useprefix logic as elsewhere in this file.
         */
-       useprefix = list_length(es->rtable) > 1 || es->verbose;
+       useprefix = es->rtable_size > 1 || es->verbose;
 
        /* Set up deparsing context */
        context = set_deparse_context_plan(es->deparse_cxt,
index d2e2af4f8116d410f39c1d31aac56d12969edb1e..0d00e029f32230d027b9853ca1799ac457063ca1 100644 (file)
@@ -2854,6 +2854,11 @@ range_table_entry_walker_impl(RangeTblEntry *rte,
                case RTE_RESULT:
                        /* nothing to do */
                        break;
+               case RTE_GROUP:
+                       if (!(flags & QTW_IGNORE_GROUPEXPRS))
+                               if (WALK(rte->groupexprs))
+                                       return true;
+                       break;
        }
 
        if (WALK(rte->securityQuals))
@@ -3891,6 +3896,15 @@ range_table_mutator_impl(List *rtable,
                        case RTE_RESULT:
                                /* nothing to do */
                                break;
+                       case RTE_GROUP:
+                               if (!(flags & QTW_IGNORE_GROUPEXPRS))
+                                       MUTATE(newrte->groupexprs, rte->groupexprs, List *);
+                               else
+                               {
+                                       /* else, copy grouping exprs as-is */
+                                       newrte->groupexprs = copyObject(rte->groupexprs);
+                               }
+                               break;
                }
                MUTATE(newrte->securityQuals, rte->securityQuals, List *);
                newrt = lappend(newrt, newrte);
index 3337b77ae6d7a1b8cdf96cebfb682e29475aafc6..9827cf16be4607d1b89e46cb90608f21d425ae04 100644 (file)
@@ -562,6 +562,9 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node)
                case RTE_RESULT:
                        /* no extra fields */
                        break;
+               case RTE_GROUP:
+                       WRITE_NODE_FIELD(groupexprs);
+                       break;
                default:
                        elog(ERROR, "unrecognized RTE kind: %d", (int) node->rtekind);
                        break;
index 02798f4482d04e9552d255493f5a0fa485b9ad53..03416e8f4a1f826ad5a451cfce87228676ac60c0 100644 (file)
@@ -300,6 +300,10 @@ print_rt(const List *rtable)
                                printf("%d\t%s\t[result]",
                                           i, rte->eref->aliasname);
                                break;
+                       case RTE_GROUP:
+                               printf("%d\t%s\t[group]",
+                                          i, rte->eref->aliasname);
+                               break;
                        default:
                                printf("%d\t%s\t[unknown rtekind]",
                                           i, rte->eref->aliasname);
index b47950764a43a06bd6044f5f99780ce7ca0ccba2..be5f19dd7f6459b9b13d41e5ed33cd369b5f37e4 100644 (file)
@@ -422,6 +422,9 @@ _readRangeTblEntry(void)
                case RTE_RESULT:
                        /* no extra fields */
                        break;
+               case RTE_GROUP:
+                       READ_NODE_FIELD(groupexprs);
+                       break;
                default:
                        elog(ERROR, "unrecognized RTE kind: %d",
                                 (int) local_node->rtekind);
index 057b4b79ebb8dd702035cef7eb6de4605bf9fc87..172edb643a428fa540dc5986bd9d9bbf590fc1b5 100644 (file)
@@ -731,6 +731,10 @@ set_rel_consider_parallel(PlannerInfo *root, RelOptInfo *rel,
                case RTE_RESULT:
                        /* RESULT RTEs, in themselves, are no problem. */
                        break;
+               case RTE_GROUP:
+                       /* Shouldn't happen; we're only considering baserels here. */
+                       Assert(false);
+                       return;
        }
 
        /*
index 62b2354f0041c28cfb20fa58b327ded4943c80c8..bd4b652f7a3492b06038858c5c87c28286b0a039 100644 (file)
@@ -88,6 +88,7 @@ create_upper_paths_hook_type create_upper_paths_hook = NULL;
 #define EXPRKIND_ARBITER_ELEM          10
 #define EXPRKIND_TABLEFUNC                     11
 #define EXPRKIND_TABLEFUNC_LATERAL     12
+#define EXPRKIND_GROUPEXPR                     13
 
 /*
  * Data specific to grouping sets
@@ -748,6 +749,7 @@ subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root,
         */
        root->hasJoinRTEs = false;
        root->hasLateralRTEs = false;
+       root->group_rtindex = 0;
        hasOuterJoins = false;
        hasResultRTEs = false;
        foreach(l, parse->rtable)
@@ -781,6 +783,10 @@ subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root,
                        case RTE_RESULT:
                                hasResultRTEs = true;
                                break;
+                       case RTE_GROUP:
+                               Assert(parse->hasGroupRTE);
+                               root->group_rtindex = list_cell_number(parse->rtable, l) + 1;
+                               break;
                        default:
                                /* No work here for other RTE types */
                                break;
@@ -836,10 +842,6 @@ subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root,
                preprocess_expression(root, (Node *) parse->targetList,
                                                          EXPRKIND_TARGET);
 
-       /* Constant-folding might have removed all set-returning functions */
-       if (parse->hasTargetSRFs)
-               parse->hasTargetSRFs = expression_returns_set((Node *) parse->targetList);
-
        newWithCheckOptions = NIL;
        foreach(l, parse->withCheckOptions)
        {
@@ -969,6 +971,13 @@ subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root,
                        rte->values_lists = (List *)
                                preprocess_expression(root, (Node *) rte->values_lists, kind);
                }
+               else if (rte->rtekind == RTE_GROUP)
+               {
+                       /* Preprocess the groupexprs list fully */
+                       rte->groupexprs = (List *)
+                               preprocess_expression(root, (Node *) rte->groupexprs,
+                                                                         EXPRKIND_GROUPEXPR);
+               }
 
                /*
                 * Process each element of the securityQuals list as if it were a
@@ -1005,6 +1014,27 @@ subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root,
                }
        }
 
+       /*
+        * Replace any Vars in the subquery's targetlist and havingQual that
+        * reference GROUP outputs with the underlying grouping expressions.
+        *
+        * Note that we need to perform this replacement after we've preprocessed
+        * the grouping expressions.  This is to ensure that there is only one
+        * instance of SubPlan for each SubLink contained within the grouping
+        * expressions.
+        */
+       if (parse->hasGroupRTE)
+       {
+               parse->targetList = (List *)
+                       flatten_group_exprs(root, root->parse, (Node *) parse->targetList);
+               parse->havingQual =
+                       flatten_group_exprs(root, root->parse, parse->havingQual);
+       }
+
+       /* Constant-folding might have removed all set-returning functions */
+       if (parse->hasTargetSRFs)
+               parse->hasTargetSRFs = expression_returns_set((Node *) parse->targetList);
+
        /*
         * In some cases we may want to transfer a HAVING clause into WHERE. We
         * cannot do so if the HAVING clause contains aggregates (obviously) or
@@ -1032,6 +1062,16 @@ subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root,
         * don't emit a bogus aggregated row. (This could be done better, but it
         * seems not worth optimizing.)
         *
+        * Note that a HAVING clause may contain expressions that are not fully
+        * preprocessed.  This can happen if these expressions are part of
+        * grouping items.  In such cases, they are replaced with GROUP Vars in
+        * the parser and then replaced back after we've done with expression
+        * preprocessing on havingQual.  This is not an issue if the clause
+        * remains in HAVING, because these expressions will be matched to lower
+        * target items in setrefs.c.  However, if the clause is moved or copied
+        * into WHERE, we need to ensure that these expressions are fully
+        * preprocessed.
+        *
         * Note that both havingQual and parse->jointree->quals are in
         * implicitly-ANDed-list form at this point, even though they are declared
         * as Node *.
@@ -1051,16 +1091,28 @@ subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root,
                }
                else if (parse->groupClause && !parse->groupingSets)
                {
-                       /* move it to WHERE */
+                       Node       *whereclause;
+
+                       /* Preprocess the HAVING clause fully */
+                       whereclause = preprocess_expression(root, havingclause,
+                                                                                               EXPRKIND_QUAL);
+                       /* ... and move it to WHERE */
                        parse->jointree->quals = (Node *)
-                               lappend((List *) parse->jointree->quals, havingclause);
+                               list_concat((List *) parse->jointree->quals,
+                                                       (List *) whereclause);
                }
                else
                {
-                       /* put a copy in WHERE, keep it in HAVING */
+                       Node       *whereclause;
+
+                       /* Preprocess the HAVING clause fully */
+                       whereclause = preprocess_expression(root, copyObject(havingclause),
+                                                                                               EXPRKIND_QUAL);
+                       /* ... and put a copy in WHERE */
                        parse->jointree->quals = (Node *)
-                               lappend((List *) parse->jointree->quals,
-                                               copyObject(havingclause));
+                               list_concat((List *) parse->jointree->quals,
+                                                       (List *) whereclause);
+                       /* ... and also keep it in HAVING */
                        newHaving = lappend(newHaving, havingclause);
                }
        }
index 7aed84584c67050fe74ee46f370e8ecae732a657..8caf094f7d6ed86f384706b08109e5ebbeabb65e 100644 (file)
@@ -557,6 +557,7 @@ add_rte_to_flat_rtable(PlannerGlobal *glob, List *rteperminfos,
        newrte->coltypes = NIL;
        newrte->coltypmods = NIL;
        newrte->colcollations = NIL;
+       newrte->groupexprs = NIL;
        newrte->securityQuals = NIL;
 
        glob->finalrtable = lappend(glob->finalrtable, newrte);
index 34fbf8ee2378fbaf591b266689f78d2274b572ea..a70404558ff488f01229fdfa167e1b9c7b9cd327 100644 (file)
@@ -1235,6 +1235,7 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte,
                                case RTE_CTE:
                                case RTE_NAMEDTUPLESTORE:
                                case RTE_RESULT:
+                               case RTE_GROUP:
                                        /* these can't contain any lateral references */
                                        break;
                        }
@@ -2218,7 +2219,8 @@ perform_pullup_replace_vars(PlannerInfo *root,
        }
 
        /*
-        * Replace references in the joinaliasvars lists of join RTEs.
+        * Replace references in the joinaliasvars lists of join RTEs and the
+        * groupexprs list of group RTE.
         */
        foreach(lc, parse->rtable)
        {
@@ -2228,6 +2230,10 @@ perform_pullup_replace_vars(PlannerInfo *root,
                        otherrte->joinaliasvars = (List *)
                                pullup_replace_vars((Node *) otherrte->joinaliasvars,
                                                                        rvcontext);
+               else if (otherrte->rtekind == RTE_GROUP)
+                       otherrte->groupexprs = (List *)
+                               pullup_replace_vars((Node *) otherrte->groupexprs,
+                                                                       rvcontext);
        }
 }
 
@@ -2293,6 +2299,7 @@ replace_vars_in_jointree(Node *jtnode,
                                        case RTE_CTE:
                                        case RTE_NAMEDTUPLESTORE:
                                        case RTE_RESULT:
+                                       case RTE_GROUP:
                                                /* these shouldn't be marked LATERAL */
                                                Assert(false);
                                                break;
index 844fc30978b21a5e438150b3561ed3bcba645c3f..b189185fca2d35340d88548abf18179e523b229b 100644 (file)
@@ -81,6 +81,8 @@ static bool pull_var_clause_walker(Node *node,
                                                                   pull_var_clause_context *context);
 static Node *flatten_join_alias_vars_mutator(Node *node,
                                                                                         flatten_join_alias_vars_context *context);
+static Node *flatten_group_exprs_mutator(Node *node,
+                                                                                flatten_join_alias_vars_context *context);
 static Node *add_nullingrels_if_needed(PlannerInfo *root, Node *newnode,
                                                                           Var *oldvar);
 static bool is_standard_join_alias_expression(Node *newnode, Var *oldvar);
@@ -893,6 +895,7 @@ flatten_join_alias_vars_mutator(Node *node,
        }
        /* Already-planned tree not supported */
        Assert(!IsA(node, SubPlan));
+       Assert(!IsA(node, AlternativeSubPlan));
        /* Shouldn't need to handle these planner auxiliary nodes here */
        Assert(!IsA(node, SpecialJoinInfo));
        Assert(!IsA(node, PlaceHolderInfo));
@@ -902,6 +905,141 @@ flatten_join_alias_vars_mutator(Node *node,
                                                                   (void *) context);
 }
 
+/*
+ * flatten_group_exprs
+ *       Replace Vars that reference GROUP outputs with the underlying grouping
+ *       expressions.
+ */
+Node *
+flatten_group_exprs(PlannerInfo *root, Query *query, Node *node)
+{
+       flatten_join_alias_vars_context context;
+
+       /*
+        * We do not expect this to be applied to the whole Query, only to
+        * expressions or LATERAL subqueries.  Hence, if the top node is a Query,
+        * it's okay to immediately increment sublevels_up.
+        */
+       Assert(node != (Node *) query);
+
+       context.root = root;
+       context.query = query;
+       context.sublevels_up = 0;
+       /* flag whether grouping expressions could possibly contain SubLinks */
+       context.possible_sublink = query->hasSubLinks;
+       /* if hasSubLinks is already true, no need to work hard */
+       context.inserted_sublink = query->hasSubLinks;
+
+       return flatten_group_exprs_mutator(node, &context);
+}
+
+static Node *
+flatten_group_exprs_mutator(Node *node,
+                                                       flatten_join_alias_vars_context *context)
+{
+       if (node == NULL)
+               return NULL;
+       if (IsA(node, Var))
+       {
+               Var                *var = (Var *) node;
+               RangeTblEntry *rte;
+               Node       *newvar;
+
+               /* No change unless Var belongs to the GROUP of the target level */
+               if (var->varlevelsup != context->sublevels_up)
+                       return node;            /* no need to copy, really */
+               rte = rt_fetch(var->varno, context->query->rtable);
+               if (rte->rtekind != RTE_GROUP)
+                       return node;
+
+               /* Expand group exprs reference */
+               Assert(var->varattno > 0);
+               newvar = (Node *) list_nth(rte->groupexprs, var->varattno - 1);
+               Assert(newvar != NULL);
+               newvar = copyObject(newvar);
+
+               /*
+                * If we are expanding an expr carried down from an upper query, must
+                * adjust its varlevelsup fields.
+                */
+               if (context->sublevels_up != 0)
+                       IncrementVarSublevelsUp(newvar, context->sublevels_up, 0);
+
+               /* Preserve original Var's location, if possible */
+               if (IsA(newvar, Var))
+                       ((Var *) newvar)->location = var->location;
+
+               /* Detect if we are adding a sublink to query */
+               if (context->possible_sublink && !context->inserted_sublink)
+                       context->inserted_sublink = checkExprHasSubLink(newvar);
+
+               return newvar;
+       }
+
+       if (IsA(node, Aggref))
+       {
+               Aggref     *agg = (Aggref *) node;
+
+               if ((int) agg->agglevelsup == context->sublevels_up)
+               {
+                       /*
+                        * If we find an aggregate call of the original level, do not
+                        * recurse into its normal arguments, ORDER BY arguments, or
+                        * filter; there are no grouped vars there.  But we should check
+                        * direct arguments as though they weren't in an aggregate.
+                        */
+                       agg = copyObject(agg);
+                       agg->aggdirectargs = (List *)
+                               flatten_group_exprs_mutator((Node *) agg->aggdirectargs, context);
+
+                       return (Node *) agg;
+               }
+
+               /*
+                * We can skip recursing into aggregates of higher levels altogether,
+                * since they could not possibly contain Vars of concern to us (see
+                * transformAggregateCall).  We do need to look at aggregates of lower
+                * levels, however.
+                */
+               if ((int) agg->agglevelsup > context->sublevels_up)
+                       return node;
+       }
+
+       if (IsA(node, GroupingFunc))
+       {
+               GroupingFunc *grp = (GroupingFunc *) node;
+
+               /*
+                * If we find a GroupingFunc node of the original or higher level, do
+                * not recurse into its arguments; there are no grouped vars there.
+                */
+               if ((int) grp->agglevelsup >= context->sublevels_up)
+                       return node;
+       }
+
+       if (IsA(node, Query))
+       {
+               /* Recurse into RTE subquery or not-yet-planned sublink subquery */
+               Query      *newnode;
+               bool            save_inserted_sublink;
+
+               context->sublevels_up++;
+               save_inserted_sublink = context->inserted_sublink;
+               context->inserted_sublink = ((Query *) node)->hasSubLinks;
+               newnode = query_tree_mutator((Query *) node,
+                                                                        flatten_group_exprs_mutator,
+                                                                        (void *) context,
+                                                                        QTW_IGNORE_GROUPEXPRS);
+               newnode->hasSubLinks |= context->inserted_sublink;
+               context->inserted_sublink = save_inserted_sublink;
+               context->sublevels_up--;
+               return (Node *) newnode;
+       }
+
+       return expression_tree_mutator(node, flatten_group_exprs_mutator,
+                                                                  (void *) context);
+}
+
 /*
  * Add oldvar's varnullingrels, if any, to a flattened join alias expression.
  * The newnode has been copied, so we can modify it freely.
index bee7d8346a32e65b3f88bac86170cfc01c7ba174..bd095d05c0b21196f328aa5ca40902d4b9ed34ba 100644 (file)
@@ -26,6 +26,7 @@
 #include "parser/parse_clause.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_expr.h"
+#include "parser/parse_relation.h"
 #include "parser/parsetree.h"
 #include "rewrite/rewriteManip.h"
 #include "utils/builtins.h"
@@ -47,11 +48,12 @@ typedef struct
        bool            hasJoinRTEs;
        List       *groupClauses;
        List       *groupClauseCommonVars;
+       List       *gset_common;
        bool            have_non_var_grouping;
        List      **func_grouped_rels;
        int                     sublevels_up;
        bool            in_agg_direct_args;
-} check_ungrouped_columns_context;
+} substitute_grouped_columns_context;
 
 static int     check_agg_arguments(ParseState *pstate,
                                                                List *directargs,
@@ -59,17 +61,20 @@ static int  check_agg_arguments(ParseState *pstate,
                                                                Expr *filter);
 static bool check_agg_arguments_walker(Node *node,
                                                                           check_agg_arguments_context *context);
-static void check_ungrouped_columns(Node *node, ParseState *pstate, Query *qry,
-                                                                       List *groupClauses, List *groupClauseCommonVars,
-                                                                       bool have_non_var_grouping,
-                                                                       List **func_grouped_rels);
-static bool check_ungrouped_columns_walker(Node *node,
-                                                                                  check_ungrouped_columns_context *context);
+static Node *substitute_grouped_columns(Node *node, ParseState *pstate, Query *qry,
+                                                                               List *groupClauses, List *groupClauseCommonVars,
+                                                                               List *gset_common,
+                                                                               bool have_non_var_grouping,
+                                                                               List **func_grouped_rels);
+static Node *substitute_grouped_columns_mutator(Node *node,
+                                                                                               substitute_grouped_columns_context *context);
 static void finalize_grouping_exprs(Node *node, ParseState *pstate, Query *qry,
                                                                        List *groupClauses, bool hasJoinRTEs,
                                                                        bool have_non_var_grouping);
 static bool finalize_grouping_exprs_walker(Node *node,
-                                                                                  check_ungrouped_columns_context *context);
+                                                                                  substitute_grouped_columns_context *context);
+static Var *buildGroupedVar(int attnum, Index ressortgroupref,
+                                                       substitute_grouped_columns_context *context);
 static void check_agglevels_and_constraints(ParseState *pstate, Node *expr);
 static List *expand_groupingset_node(GroupingSet *gs);
 static Node *make_agg_arg(Oid argtype, Oid argcollation);
@@ -1066,7 +1071,9 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
 
 /*
  * parseCheckAggregates
- *     Check for aggregates where they shouldn't be and improper grouping.
+ *     Check for aggregates where they shouldn't be and improper grouping, and
+ *     replace grouped variables in the targetlist and HAVING clause with Vars
+ *     that reference the RTE_GROUP RTE.
  *     This function should be called after the target list and qualifications
  *     are finalized.
  *
@@ -1156,7 +1163,7 @@ parseCheckAggregates(ParseState *pstate, Query *qry)
 
        /*
         * Build a list of the acceptable GROUP BY expressions for use by
-        * check_ungrouped_columns().
+        * substitute_grouped_columns().
         *
         * We get the TLE, not just the expr, because GROUPING wants to know the
         * sortgroupref.
@@ -1209,7 +1216,24 @@ parseCheckAggregates(ParseState *pstate, Query *qry)
        }
 
        /*
-        * Check the targetlist and HAVING clause for ungrouped variables.
+        * If there are any acceptable GROUP BY expressions, build an RTE and
+        * nsitem for the result of the grouping step.
+        */
+       if (groupClauses)
+       {
+               pstate->p_grouping_nsitem =
+                       addRangeTableEntryForGroup(pstate, groupClauses);
+
+               /* Set qry->rtable again in case it was previously NIL */
+               qry->rtable = pstate->p_rtable;
+               /* Mark the Query as having RTE_GROUP RTE */
+               qry->hasGroupRTE = true;
+       }
+
+       /*
+        * Replace grouped variables in the targetlist and HAVING clause with Vars
+        * that reference the RTE_GROUP RTE.  Emit an error message if we find any
+        * ungrouped variables.
         *
         * Note: because we check resjunk tlist elements as well as regular ones,
         * this will also find ungrouped variables that came from ORDER BY and
@@ -1225,10 +1249,12 @@ parseCheckAggregates(ParseState *pstate, Query *qry)
                                                        have_non_var_grouping);
        if (hasJoinRTEs)
                clause = flatten_join_alias_vars(NULL, qry, clause);
-       check_ungrouped_columns(clause, pstate, qry,
-                                                       groupClauses, groupClauseCommonVars,
-                                                       have_non_var_grouping,
-                                                       &func_grouped_rels);
+       qry->targetList = (List *)
+               substitute_grouped_columns(clause, pstate, qry,
+                                                                  groupClauses, groupClauseCommonVars,
+                                                                  gset_common,
+                                                                  have_non_var_grouping,
+                                                                  &func_grouped_rels);
 
        clause = (Node *) qry->havingQual;
        finalize_grouping_exprs(clause, pstate, qry,
@@ -1236,10 +1262,12 @@ parseCheckAggregates(ParseState *pstate, Query *qry)
                                                        have_non_var_grouping);
        if (hasJoinRTEs)
                clause = flatten_join_alias_vars(NULL, qry, clause);
-       check_ungrouped_columns(clause, pstate, qry,
-                                                       groupClauses, groupClauseCommonVars,
-                                                       have_non_var_grouping,
-                                                       &func_grouped_rels);
+       qry->havingQual =
+               substitute_grouped_columns(clause, pstate, qry,
+                                                                  groupClauses, groupClauseCommonVars,
+                                                                  gset_common,
+                                                                  have_non_var_grouping,
+                                                                  &func_grouped_rels);
 
        /*
         * Per spec, aggregates can't appear in a recursive term.
@@ -1253,14 +1281,16 @@ parseCheckAggregates(ParseState *pstate, Query *qry)
 }
 
 /*
- * check_ungrouped_columns -
- *       Scan the given expression tree for ungrouped variables (variables
- *       that are not listed in the groupClauses list and are not within
- *       the arguments of aggregate functions).  Emit a suitable error message
- *       if any are found.
+ * substitute_grouped_columns -
+ *       Scan the given expression tree for grouped variables (variables that
+ *       are listed in the groupClauses list) and replace them with Vars that
+ *       reference the RTE_GROUP RTE.  Emit a suitable error message if any
+ *       ungrouped variables (variables that are not listed in the groupClauses
+ *       list and are not within the arguments of aggregate functions) are
+ *       found.
  *
  * NOTE: we assume that the given clause has been transformed suitably for
- * parser output.  This means we can use expression_tree_walker.
+ * parser output.  This means we can use expression_tree_mutator.
  *
  * NOTE: we recognize grouping expressions in the main query, but only
  * grouping Vars in subqueries.  For example, this will be rejected,
@@ -1273,37 +1303,39 @@ parseCheckAggregates(ParseState *pstate, Query *qry)
  * This appears to require a whole custom version of equal(), which is
  * way more pain than the feature seems worth.
  */
-static void
-check_ungrouped_columns(Node *node, ParseState *pstate, Query *qry,
-                                               List *groupClauses, List *groupClauseCommonVars,
-                                               bool have_non_var_grouping,
-                                               List **func_grouped_rels)
+static Node *
+substitute_grouped_columns(Node *node, ParseState *pstate, Query *qry,
+                                                  List *groupClauses, List *groupClauseCommonVars,
+                                                  List *gset_common,
+                                                  bool have_non_var_grouping,
+                                                  List **func_grouped_rels)
 {
-       check_ungrouped_columns_context context;
+       substitute_grouped_columns_context context;
 
        context.pstate = pstate;
        context.qry = qry;
        context.hasJoinRTEs = false;    /* assume caller flattened join Vars */
        context.groupClauses = groupClauses;
        context.groupClauseCommonVars = groupClauseCommonVars;
+       context.gset_common = gset_common;
        context.have_non_var_grouping = have_non_var_grouping;
        context.func_grouped_rels = func_grouped_rels;
        context.sublevels_up = 0;
        context.in_agg_direct_args = false;
-       check_ungrouped_columns_walker(node, &context);
+       return substitute_grouped_columns_mutator(node, &context);
 }
 
-static bool
-check_ungrouped_columns_walker(Node *node,
-                                                          check_ungrouped_columns_context *context)
+static Node *
+substitute_grouped_columns_mutator(Node *node,
+                                                                  substitute_grouped_columns_context *context)
 {
        ListCell   *gl;
 
        if (node == NULL)
-               return false;
+               return NULL;
        if (IsA(node, Const) ||
                IsA(node, Param))
-               return false;                   /* constants are always acceptable */
+               return node;                    /* constants are always acceptable */
 
        if (IsA(node, Aggref))
        {
@@ -1314,19 +1346,21 @@ check_ungrouped_columns_walker(Node *node,
                        /*
                         * If we find an aggregate call of the original level, do not
                         * recurse into its normal arguments, ORDER BY arguments, or
-                        * filter; ungrouped vars there are not an error.  But we should
-                        * check direct arguments as though they weren't in an aggregate.
-                        * We set a special flag in the context to help produce a useful
+                        * filter; grouped vars there do not need to be replaced and
+                        * ungrouped vars there are not an error.  But we should check
+                        * direct arguments as though they weren't in an aggregate.  We
+                        * set a special flag in the context to help produce a useful
                         * error message for ungrouped vars in direct arguments.
                         */
-                       bool            result;
+                       agg = copyObject(agg);
 
                        Assert(!context->in_agg_direct_args);
                        context->in_agg_direct_args = true;
-                       result = check_ungrouped_columns_walker((Node *) agg->aggdirectargs,
-                                                                                                       context);
+                       agg->aggdirectargs = (List *)
+                               substitute_grouped_columns_mutator((Node *) agg->aggdirectargs,
+                                                                                                  context);
                        context->in_agg_direct_args = false;
-                       return result;
+                       return (Node *) agg;
                }
 
                /*
@@ -1336,7 +1370,7 @@ check_ungrouped_columns_walker(Node *node,
                 * levels, however.
                 */
                if ((int) agg->agglevelsup > context->sublevels_up)
-                       return false;
+                       return node;
        }
 
        if (IsA(node, GroupingFunc))
@@ -1346,7 +1380,7 @@ check_ungrouped_columns_walker(Node *node,
                /* handled GroupingFunc separately, no need to recheck at this level */
 
                if ((int) grp->agglevelsup >= context->sublevels_up)
-                       return false;
+                       return node;
        }
 
        /*
@@ -1358,12 +1392,20 @@ check_ungrouped_columns_walker(Node *node,
         */
        if (context->have_non_var_grouping && context->sublevels_up == 0)
        {
+               int                     attnum = 0;
+
                foreach(gl, context->groupClauses)
                {
-                       TargetEntry *tle = lfirst(gl);
+                       TargetEntry *tle = (TargetEntry *) lfirst(gl);
 
+                       attnum++;
                        if (equal(node, tle->expr))
-                               return false;   /* acceptable, do not descend more */
+                       {
+                               /* acceptable, replace it with a GROUP Var */
+                               return (Node *) buildGroupedVar(attnum,
+                                                                                               tle->ressortgroupref,
+                                                                                               context);
+                       }
                }
        }
 
@@ -1380,22 +1422,31 @@ check_ungrouped_columns_walker(Node *node,
                char       *attname;
 
                if (var->varlevelsup != context->sublevels_up)
-                       return false;           /* it's not local to my query, ignore */
+                       return node;            /* it's not local to my query, ignore */
 
                /*
                 * Check for a match, if we didn't do it above.
                 */
                if (!context->have_non_var_grouping || context->sublevels_up != 0)
                {
+                       int                     attnum = 0;
+
                        foreach(gl, context->groupClauses)
                        {
-                               Var                *gvar = (Var *) ((TargetEntry *) lfirst(gl))->expr;
+                               TargetEntry *tle = (TargetEntry *) lfirst(gl);
+                               Var                *gvar = (Var *) tle->expr;
 
+                               attnum++;
                                if (IsA(gvar, Var) &&
                                        gvar->varno == var->varno &&
                                        gvar->varattno == var->varattno &&
                                        gvar->varlevelsup == 0)
-                                       return false;   /* acceptable, we're okay */
+                               {
+                                       /* acceptable, replace it with a GROUP Var */
+                                       return (Node *) buildGroupedVar(attnum,
+                                                                                                       tle->ressortgroupref,
+                                                                                                       context);
+                               }
                        }
                }
 
@@ -1416,7 +1467,7 @@ check_ungrouped_columns_walker(Node *node,
                 * the constraintDeps list.
                 */
                if (list_member_int(*context->func_grouped_rels, var->varno))
-                       return false;           /* previously proven acceptable */
+                       return node;            /* previously proven acceptable */
 
                Assert(var->varno > 0 &&
                           (int) var->varno <= list_length(context->pstate->p_rtable));
@@ -1431,7 +1482,7 @@ check_ungrouped_columns_walker(Node *node,
                        {
                                *context->func_grouped_rels =
                                        lappend_int(*context->func_grouped_rels, var->varno);
-                               return false;   /* acceptable */
+                               return node;    /* acceptable */
                        }
                }
 
@@ -1456,18 +1507,18 @@ check_ungrouped_columns_walker(Node *node,
        if (IsA(node, Query))
        {
                /* Recurse into subselects */
-               bool            result;
+               Query      *newnode;
 
                context->sublevels_up++;
-               result = query_tree_walker((Query *) node,
-                                                                  check_ungrouped_columns_walker,
-                                                                  (void *) context,
-                                                                  0);
+               newnode = query_tree_mutator((Query *) node,
+                                                                        substitute_grouped_columns_mutator,
+                                                                        (void *) context,
+                                                                        0);
                context->sublevels_up--;
-               return result;
+               return (Node *) newnode;
        }
-       return expression_tree_walker(node, check_ungrouped_columns_walker,
-                                                                 (void *) context);
+       return expression_tree_mutator(node, substitute_grouped_columns_mutator,
+                                                                  (void *) context);
 }
 
 /*
@@ -1475,9 +1526,9 @@ check_ungrouped_columns_walker(Node *node,
  *       Scan the given expression tree for GROUPING() and related calls,
  *       and validate and process their arguments.
  *
- * This is split out from check_ungrouped_columns above because it needs
+ * This is split out from substitute_grouped_columns above because it needs
  * to modify the nodes (which it does in-place, not via a mutator) while
- * check_ungrouped_columns may see only a copy of the original thanks to
+ * substitute_grouped_columns may see only a copy of the original thanks to
  * flattening of join alias vars. So here, we flatten each individual
  * GROUPING argument as we see it before comparing it.
  */
@@ -1486,13 +1537,14 @@ finalize_grouping_exprs(Node *node, ParseState *pstate, Query *qry,
                                                List *groupClauses, bool hasJoinRTEs,
                                                bool have_non_var_grouping)
 {
-       check_ungrouped_columns_context context;
+       substitute_grouped_columns_context context;
 
        context.pstate = pstate;
        context.qry = qry;
        context.hasJoinRTEs = hasJoinRTEs;
        context.groupClauses = groupClauses;
        context.groupClauseCommonVars = NIL;
+       context.gset_common = NIL;
        context.have_non_var_grouping = have_non_var_grouping;
        context.func_grouped_rels = NULL;
        context.sublevels_up = 0;
@@ -1502,7 +1554,7 @@ finalize_grouping_exprs(Node *node, ParseState *pstate, Query *qry,
 
 static bool
 finalize_grouping_exprs_walker(Node *node,
-                                                          check_ungrouped_columns_context *context)
+                                                          substitute_grouped_columns_context *context)
 {
        ListCell   *gl;
 
@@ -1643,6 +1695,38 @@ finalize_grouping_exprs_walker(Node *node,
                                                                  (void *) context);
 }
 
+/*
+ * buildGroupedVar -
+ *       build a Var node that references the RTE_GROUP RTE
+ */
+static Var *
+buildGroupedVar(int attnum, Index ressortgroupref,
+                               substitute_grouped_columns_context *context)
+{
+       Var                *var;
+       ParseNamespaceItem *grouping_nsitem = context->pstate->p_grouping_nsitem;
+       ParseNamespaceColumn *nscol = grouping_nsitem->p_nscolumns + attnum - 1;
+
+       Assert(nscol->p_varno == grouping_nsitem->p_rtindex);
+       Assert(nscol->p_varattno == attnum);
+       var = makeVar(nscol->p_varno,
+                                 nscol->p_varattno,
+                                 nscol->p_vartype,
+                                 nscol->p_vartypmod,
+                                 nscol->p_varcollid,
+                                 context->sublevels_up);
+       /* makeVar doesn't offer parameters for these, so set by hand: */
+       var->varnosyn = nscol->p_varnosyn;
+       var->varattnosyn = nscol->p_varattnosyn;
+
+       if (context->qry->groupingSets &&
+               !list_member_int(context->gset_common, ressortgroupref))
+               var->varnullingrels =
+                       bms_add_member(var->varnullingrels, grouping_nsitem->p_rtindex);
+
+       return var;
+}
+
 
 /*
  * Given a GroupingSet node, expand it and return a list of lists.
index 2f64eaf0e371a82220f54ba97e5d1e81faf61947..8075b1b8a1b678bdccef594b3fa554cbce1c8e7b 100644 (file)
@@ -2557,6 +2557,79 @@ addRangeTableEntryForENR(ParseState *pstate,
                                                                        tupdesc);
 }
 
+/*
+ * Add an entry for grouping step to the pstate's range table (p_rtable).
+ * Then, construct and return a ParseNamespaceItem for the new RTE.
+ */
+ParseNamespaceItem *
+addRangeTableEntryForGroup(ParseState *pstate,
+                                                  List *groupClauses)
+{
+       RangeTblEntry *rte = makeNode(RangeTblEntry);
+       Alias      *eref;
+       List       *groupexprs;
+       List       *coltypes,
+                          *coltypmods,
+                          *colcollations;
+       ListCell   *lc;
+       ParseNamespaceItem *nsitem;
+
+       Assert(pstate != NULL);
+
+       rte->rtekind = RTE_GROUP;
+       rte->alias = NULL;
+
+       eref = makeAlias("*GROUP*", NIL);
+
+       /* fill in any unspecified alias columns, and extract column type info */
+       groupexprs = NIL;
+       coltypes = coltypmods = colcollations = NIL;
+       foreach(lc, groupClauses)
+       {
+               TargetEntry *te = (TargetEntry *) lfirst(lc);
+               char       *colname = te->resname ? pstrdup(te->resname) : "?column?";
+
+               eref->colnames = lappend(eref->colnames, makeString(colname));
+
+               groupexprs = lappend(groupexprs, copyObject(te->expr));
+
+               coltypes = lappend_oid(coltypes,
+                                                          exprType((Node *) te->expr));
+               coltypmods = lappend_int(coltypmods,
+                                                                exprTypmod((Node *) te->expr));
+               colcollations = lappend_oid(colcollations,
+                                                                       exprCollation((Node *) te->expr));
+       }
+
+       rte->eref = eref;
+       rte->groupexprs = groupexprs;
+
+       /*
+        * Set flags.
+        *
+        * The grouping step is never checked for access rights, so no need to
+        * perform addRTEPermissionInfo().
+        */
+       rte->lateral = false;
+       rte->inFromCl = false;
+
+       /*
+        * Add completed RTE to pstate's range table list, so that we know its
+        * index.  But we don't add it to the join list --- caller must do that if
+        * appropriate.
+        */
+       pstate->p_rtable = lappend(pstate->p_rtable, rte);
+
+       /*
+        * Build a ParseNamespaceItem, but don't add it to the pstate's namespace
+        * list --- caller must do that if appropriate.
+        */
+       nsitem = buildNSItemFromLists(rte, list_length(pstate->p_rtable),
+                                                                 coltypes, coltypmods, colcollations);
+
+       return nsitem;
+}
+
 
 /*
  * Has the specified refname been selected FOR UPDATE/FOR SHARE?
@@ -3003,6 +3076,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up,
                        }
                        break;
                case RTE_RESULT:
+               case RTE_GROUP:
                        /* These expose no columns, so nothing to do */
                        break;
                default:
@@ -3317,10 +3391,11 @@ get_rte_attribute_is_dropped(RangeTblEntry *rte, AttrNumber attnum)
                case RTE_TABLEFUNC:
                case RTE_VALUES:
                case RTE_CTE:
+               case RTE_GROUP:
 
                        /*
-                        * Subselect, Table Functions, Values, CTE RTEs never have dropped
-                        * columns
+                        * Subselect, Table Functions, Values, CTE, GROUP RTEs never have
+                        * dropped columns
                         */
                        result = false;
                        break;
index ee6fcd0503a0392c8b5503505c1e8236c70a536b..76bf88c3ca2cf7301889fac7f58a4bdfb23ac3d9 100644 (file)
@@ -420,6 +420,9 @@ markTargetListOrigin(ParseState *pstate, TargetEntry *tle,
                                tle->resorigcol = ste->resorigcol;
                        }
                        break;
+               case RTE_GROUP:
+                       /* We couldn't get here: the RTE_GROUP RTE has not been added */
+                       break;
        }
 }
 
@@ -1681,6 +1684,12 @@ expandRecordVariable(ParseState *pstate, Var *var, int levelsup)
                                /* else fall through to inspect the expression */
                        }
                        break;
+               case RTE_GROUP:
+
+                       /*
+                        * We couldn't get here: the RTE_GROUP RTE has not been added.
+                        */
+                       break;
        }
 
        /*
index 54b3542894b3acba70de44f34afacbfd46b23e7b..ee1b7f3dc948e88d1c80c0bf2a27522456d38d02 100644 (file)
@@ -5433,11 +5433,28 @@ get_query_def(Query *query, StringInfo buf, List *parentnamespace,
 {
        deparse_context context;
        deparse_namespace dpns;
+       int                     rtable_size;
 
        /* Guard against excessively long or deeply-nested queries */
        CHECK_FOR_INTERRUPTS();
        check_stack_depth();
 
+       rtable_size = query->hasGroupRTE ?
+               list_length(query->rtable) - 1 :
+               list_length(query->rtable);
+
+       /*
+        * Replace any Vars in the query's targetlist and havingQual that
+        * reference GROUP outputs with the underlying grouping expressions.
+        */
+       if (query->hasGroupRTE)
+       {
+               query->targetList = (List *)
+                       flatten_group_exprs(NULL, query, (Node *) query->targetList);
+               query->havingQual =
+                       flatten_group_exprs(NULL, query, query->havingQual);
+       }
+
        /*
         * Before we begin to examine the query, acquire locks on referenced
         * relations, and fix up deleted columns in JOIN RTEs.  This ensures
@@ -5455,7 +5472,7 @@ get_query_def(Query *query, StringInfo buf, List *parentnamespace,
        context.targetList = NIL;
        context.windowClause = NIL;
        context.varprefix = (parentnamespace != NIL ||
-                                                list_length(query->rtable) != 1);
+                                                rtable_size != 1);
        context.prettyFlags = prettyFlags;
        context.wrapColumn = wrapColumn;
        context.indentLevel = startIndent;
@@ -8115,6 +8132,14 @@ get_name_for_var_field(Var *var, int fieldno,
                                }
                        }
                        break;
+               case RTE_GROUP:
+
+                       /*
+                        * We couldn't get here: any Vars that reference the RTE_GROUP RTE
+                        * should have been replaced with the underlying grouping
+                        * expressions.
+                        */
+                       break;
        }
 
        /*
index be6815593b29063d68b18dab14804e0f29a063f8..d2a512b61c5c15b5128053a1b7b0581a1f6094ec 100644 (file)
@@ -57,6 +57,6 @@
  */
 
 /*                                                     yyyymmddN */
-#define CATALOG_VERSION_NO     202409041
+#define CATALOG_VERSION_NO     202409101
 
 #endif
index 9b8b351d9a218afb1792a13c61848f54fafd1ae5..3ab0aae78f73fa573ba89cd47b6ed75ea38d9d93 100644 (file)
@@ -67,6 +67,8 @@ typedef struct ExplainState
        List       *deparse_cxt;        /* context list for deparsing expressions */
        Bitmapset  *printed_subplans;   /* ids of SubPlans we've printed */
        bool            hide_workers;   /* set if we find an invisible Gather */
+       int                     rtable_size;    /* length of rtable excluding the RTE_GROUP
+                                                                * entry */
        /* state related to the current plan node */
        ExplainWorkersState *workers_state; /* needed if parallel plan */
 } ExplainState;
index eaba59bed834ba08cc85ca0155a4115176ae92c4..caefc39f6a2645adb241a88f600630874d6412e8 100644 (file)
@@ -31,6 +31,8 @@ struct PlanState;                             /* avoid including execnodes.h too */
 #define QTW_DONT_COPY_QUERY                    0x40    /* do not copy top Query */
 #define QTW_EXAMINE_SORTGROUP          0x80    /* include SortGroupClause lists */
 
+#define QTW_IGNORE_GROUPEXPRS          0x100   /* GROUP expressions list */
+
 /* callback function for check_functions_in_node */
 typedef bool (*check_function_callback) (Oid func_id, void *context);
 
index 124d853e499d7032553b6bc69ae2eeea1c885ab7..d6f7e795fe18aa9017ff2c01133c5e4b8428dbb8 100644 (file)
@@ -160,6 +160,8 @@ typedef struct Query
        bool            hasForUpdate pg_node_attr(query_jumble_ignore);
        /* rewriter has applied some RLS policy */
        bool            hasRowSecurity pg_node_attr(query_jumble_ignore);
+       /* parser has added an RTE_GROUP RTE */
+       bool            hasGroupRTE pg_node_attr(query_jumble_ignore);
        /* is a RETURN statement */
        bool            isReturn pg_node_attr(query_jumble_ignore);
 
@@ -1023,6 +1025,7 @@ typedef enum RTEKind
        RTE_RESULT,                                     /* RTE represents an empty FROM clause; such
                                                                 * RTEs are added by the planner, they're not
                                                                 * present during parsing or rewriting */
+       RTE_GROUP,                                      /* the grouping step */
 } RTEKind;
 
 typedef struct RangeTblEntry
@@ -1229,6 +1232,12 @@ typedef struct RangeTblEntry
        /* estimated or actual from caller */
        Cardinality enrtuples pg_node_attr(query_jumble_ignore);
 
+       /*
+        * Fields valid for a GROUP RTE (else NIL):
+        */
+       /* list of grouping expressions */
+       List       *groupexprs pg_node_attr(query_jumble_ignore);
+
        /*
         * Fields valid in all RTEs:
         */
index 540d021592e68c71fc0c0ce13f96bf562f8570ce..07e2415398e89ce8b0896f424ee4bf17622de8dc 100644 (file)
@@ -509,6 +509,12 @@ struct PlannerInfo
        /* true if planning a recursive WITH item */
        bool            hasRecursion;
 
+       /*
+        * The rangetable index for the RTE_GROUP RTE, or 0 if there is no
+        * RTE_GROUP RTE.
+        */
+       int                     group_rtindex;
+
        /*
         * Information about aggregates. Filled by preprocess_aggrefs().
         */
index 7b63c5cf718dbe8c5a4cfe020268b396c2896b36..93e3dc719dab23ace751a7a2ce5b3d72d69ad2dc 100644 (file)
@@ -201,5 +201,6 @@ extern bool contain_vars_of_level(Node *node, int levelsup);
 extern int     locate_var_of_level(Node *node, int levelsup);
 extern List *pull_var_clause(Node *node, int flags);
 extern Node *flatten_join_alias_vars(PlannerInfo *root, Query *query, Node *node);
+extern Node *flatten_group_exprs(PlannerInfo *root, Query *query, Node *node);
 
 #endif                                                 /* OPTIMIZER_H */
index 5b781d87a9d24c816115b8d8fc035aff2cb3d33e..543df568147a2e9a2e2c3d1320695ef8c065d67f 100644 (file)
@@ -151,6 +151,8 @@ typedef Node *(*CoerceParamHook) (ParseState *pstate, Param *param,
  *
  * p_target_nsitem: target relation's ParseNamespaceItem.
  *
+ * p_grouping_nsitem: the ParseNamespaceItem that represents the grouping step.
+ *
  * p_is_insert: true to process assignment expressions like INSERT, false
  * to process them like UPDATE.  (Note this can change intra-statement, for
  * cases like INSERT ON CONFLICT UPDATE.)
@@ -206,6 +208,7 @@ struct ParseState
        CommonTableExpr *p_parent_cte;  /* this query's containing CTE */
        Relation        p_target_relation;      /* INSERT/UPDATE/DELETE/MERGE target rel */
        ParseNamespaceItem *p_target_nsitem;    /* target rel's NSItem, or NULL */
+       ParseNamespaceItem *p_grouping_nsitem;  /* NSItem for grouping, or NULL */
        bool            p_is_insert;    /* process assignment like INSERT not UPDATE */
        List       *p_windowdefs;       /* raw representations of window clauses */
        ParseExprKind p_expr_kind;      /* what kind of expression we're parsing */
index bea2da549617a6e58cf168be3a1ce91ab832a420..91fd8e243b51355ae4f576f98240a75176c9884f 100644 (file)
@@ -100,6 +100,8 @@ extern ParseNamespaceItem *addRangeTableEntryForCTE(ParseState *pstate,
 extern ParseNamespaceItem *addRangeTableEntryForENR(ParseState *pstate,
                                                                                                        RangeVar *rv,
                                                                                                        bool inFromCl);
+extern ParseNamespaceItem *addRangeTableEntryForGroup(ParseState *pstate,
+                                                                                                         List *groupClauses);
 extern RTEPermissionInfo *addRTEPermissionInfo(List **rteperminfos,
                                                                                           RangeTblEntry *rte);
 extern RTEPermissionInfo *getRTEPermissionInfo(List *rteperminfos,
index e1f06608104e9ab08c4930bb2ee20e05938a76f1..c860eab1c60a19fac228a77567ff6033503ada7e 100644 (file)
@@ -2150,4 +2150,142 @@ select (select grouping(v1)) from (values ((select 1))) v(v1) group by v1;
         0
 (1 row)
 
+-- test handling of subqueries in grouping sets
+create temp table gstest5(id integer primary key, v integer);
+insert into gstest5 select i, i from generate_series(1,5)i;
+explain (verbose, costs off)
+select grouping((select t1.v from gstest5 t2 where id = t1.id)),
+       (select t1.v from gstest5 t2 where id = t1.id) as s
+from gstest5 t1
+group by grouping sets(v, s)
+order by case when grouping((select t1.v from gstest5 t2 where id = t1.id)) = 0
+              then (select t1.v from gstest5 t2 where id = t1.id)
+              else null end
+         nulls first;
+                                                                 QUERY PLAN                                                                  
+---------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+   Output: (GROUPING((SubPlan 1))), ((SubPlan 3)), (CASE WHEN (GROUPING((SubPlan 2)) = 0) THEN ((SubPlan 3)) ELSE NULL::integer END), t1.v
+   Sort Key: (CASE WHEN (GROUPING((SubPlan 2)) = 0) THEN ((SubPlan 3)) ELSE NULL::integer END) NULLS FIRST
+   ->  HashAggregate
+         Output: GROUPING((SubPlan 1)), ((SubPlan 3)), CASE WHEN (GROUPING((SubPlan 2)) = 0) THEN ((SubPlan 3)) ELSE NULL::integer END, t1.v
+         Hash Key: t1.v
+         Hash Key: (SubPlan 3)
+         ->  Seq Scan on pg_temp.gstest5 t1
+               Output: (SubPlan 3), t1.v, t1.id
+               SubPlan 3
+                 ->  Bitmap Heap Scan on pg_temp.gstest5 t2
+                       Output: t1.v
+                       Recheck Cond: (t2.id = t1.id)
+                       ->  Bitmap Index Scan on gstest5_pkey
+                             Index Cond: (t2.id = t1.id)
+(15 rows)
+
+select grouping((select t1.v from gstest5 t2 where id = t1.id)),
+       (select t1.v from gstest5 t2 where id = t1.id) as s
+from gstest5 t1
+group by grouping sets(v, s)
+order by case when grouping((select t1.v from gstest5 t2 where id = t1.id)) = 0
+              then (select t1.v from gstest5 t2 where id = t1.id)
+              else null end
+         nulls first;
+ grouping | s 
+----------+---
+        1 |  
+        1 |  
+        1 |  
+        1 |  
+        1 |  
+        0 | 1
+        0 | 2
+        0 | 3
+        0 | 4
+        0 | 5
+(10 rows)
+
+explain (verbose, costs off)
+select grouping((select t1.v from gstest5 t2 where id = t1.id)),
+       (select t1.v from gstest5 t2 where id = t1.id) as s,
+       case when grouping((select t1.v from gstest5 t2 where id = t1.id)) = 0
+            then (select t1.v from gstest5 t2 where id = t1.id)
+            else null end as o
+from gstest5 t1
+group by grouping sets(v, s)
+order by o nulls first;
+                                                                 QUERY PLAN                                                                  
+---------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+   Output: (GROUPING((SubPlan 1))), ((SubPlan 3)), (CASE WHEN (GROUPING((SubPlan 2)) = 0) THEN ((SubPlan 3)) ELSE NULL::integer END), t1.v
+   Sort Key: (CASE WHEN (GROUPING((SubPlan 2)) = 0) THEN ((SubPlan 3)) ELSE NULL::integer END) NULLS FIRST
+   ->  HashAggregate
+         Output: GROUPING((SubPlan 1)), ((SubPlan 3)), CASE WHEN (GROUPING((SubPlan 2)) = 0) THEN ((SubPlan 3)) ELSE NULL::integer END, t1.v
+         Hash Key: t1.v
+         Hash Key: (SubPlan 3)
+         ->  Seq Scan on pg_temp.gstest5 t1
+               Output: (SubPlan 3), t1.v, t1.id
+               SubPlan 3
+                 ->  Bitmap Heap Scan on pg_temp.gstest5 t2
+                       Output: t1.v
+                       Recheck Cond: (t2.id = t1.id)
+                       ->  Bitmap Index Scan on gstest5_pkey
+                             Index Cond: (t2.id = t1.id)
+(15 rows)
+
+select grouping((select t1.v from gstest5 t2 where id = t1.id)),
+       (select t1.v from gstest5 t2 where id = t1.id) as s,
+       case when grouping((select t1.v from gstest5 t2 where id = t1.id)) = 0
+            then (select t1.v from gstest5 t2 where id = t1.id)
+            else null end as o
+from gstest5 t1
+group by grouping sets(v, s)
+order by o nulls first;
+ grouping | s | o 
+----------+---+---
+        1 |   |  
+        1 |   |  
+        1 |   |  
+        1 |   |  
+        1 |   |  
+        0 | 1 | 1
+        0 | 2 | 2
+        0 | 3 | 3
+        0 | 4 | 4
+        0 | 5 | 5
+(10 rows)
+
+-- test handling of expressions that should match lower target items
+explain (costs off)
+select a < b and b < 3 from (values (1, 2)) t(a, b) group by rollup(a < b and b < 3) having a < b and b < 3;
+            QUERY PLAN             
+-----------------------------------
+ MixedAggregate
+   Hash Key: ((1 < 2) AND (2 < 3))
+   Group Key: ()
+   Filter: (((1 < 2) AND (2 < 3)))
+   ->  Result
+(5 rows)
+
+select a < b and b < 3 from (values (1, 2)) t(a, b) group by rollup(a < b and b < 3) having a < b and b < 3;
+ ?column? 
+----------
+ t
+(1 row)
+
+explain (costs off)
+select not a from (values(true)) t(a) group by rollup(not a) having not not a;
+          QUERY PLAN          
+------------------------------
+ MixedAggregate
+   Hash Key: (NOT true)
+   Group Key: ()
+   Filter: (NOT ((NOT true)))
+   ->  Result
+(5 rows)
+
+select not a from (values(true)) t(a) group by rollup(not a) having not not a;
+ ?column? 
+----------
+ f
+(1 row)
+
 -- end
index 90ba27257a9ee12ab5a14551fea227fc50bc009d..add76ac4a3a3ebb4718d92013234b49a38dbbb34 100644 (file)
@@ -589,4 +589,55 @@ explain (costs off)
 select (select grouping(v1)) from (values ((select 1))) v(v1) group by v1;
 select (select grouping(v1)) from (values ((select 1))) v(v1) group by v1;
 
+-- test handling of subqueries in grouping sets
+create temp table gstest5(id integer primary key, v integer);
+insert into gstest5 select i, i from generate_series(1,5)i;
+
+explain (verbose, costs off)
+select grouping((select t1.v from gstest5 t2 where id = t1.id)),
+       (select t1.v from gstest5 t2 where id = t1.id) as s
+from gstest5 t1
+group by grouping sets(v, s)
+order by case when grouping((select t1.v from gstest5 t2 where id = t1.id)) = 0
+              then (select t1.v from gstest5 t2 where id = t1.id)
+              else null end
+         nulls first;
+
+select grouping((select t1.v from gstest5 t2 where id = t1.id)),
+       (select t1.v from gstest5 t2 where id = t1.id) as s
+from gstest5 t1
+group by grouping sets(v, s)
+order by case when grouping((select t1.v from gstest5 t2 where id = t1.id)) = 0
+              then (select t1.v from gstest5 t2 where id = t1.id)
+              else null end
+         nulls first;
+
+explain (verbose, costs off)
+select grouping((select t1.v from gstest5 t2 where id = t1.id)),
+       (select t1.v from gstest5 t2 where id = t1.id) as s,
+       case when grouping((select t1.v from gstest5 t2 where id = t1.id)) = 0
+            then (select t1.v from gstest5 t2 where id = t1.id)
+            else null end as o
+from gstest5 t1
+group by grouping sets(v, s)
+order by o nulls first;
+
+select grouping((select t1.v from gstest5 t2 where id = t1.id)),
+       (select t1.v from gstest5 t2 where id = t1.id) as s,
+       case when grouping((select t1.v from gstest5 t2 where id = t1.id)) = 0
+            then (select t1.v from gstest5 t2 where id = t1.id)
+            else null end as o
+from gstest5 t1
+group by grouping sets(v, s)
+order by o nulls first;
+
+-- test handling of expressions that should match lower target items
+explain (costs off)
+select a < b and b < 3 from (values (1, 2)) t(a, b) group by rollup(a < b and b < 3) having a < b and b < 3;
+select a < b and b < 3 from (values (1, 2)) t(a, b) group by rollup(a < b and b < 3) having a < b and b < 3;
+
+explain (costs off)
+select not a from (values(true)) t(a) group by rollup(not a) having not not a;
+select not a from (values(true)) t(a) group by rollup(not a) having not not a;
+
 -- end
index df3f336bec0b77315d734a8634af0200cc6626a9..e9ebddde24d511e25aa0d7bccc8074eb3c3b69e0 100644 (file)
@@ -3359,7 +3359,6 @@ check_function_callback
 check_network_data
 check_object_relabel_type
 check_password_hook_type
-check_ungrouped_columns_context
 child_process_kind
 chr
 cmpEntriesArg
@@ -3947,6 +3946,7 @@ stream_stop_callback
 string
 substitute_actual_parameters_context
 substitute_actual_srf_parameters_context
+substitute_grouped_columns_context
 substitute_phv_relids_context
 subxids_array_status
 symbol