Don't allow CTEs to determine semantic levels of aggregates.
authorTom Lane <[email protected]>
Tue, 18 Nov 2025 17:56:55 +0000 (12:56 -0500)
committerTom Lane <[email protected]>
Tue, 18 Nov 2025 17:56:55 +0000 (12:56 -0500)
The fix for bug #19055 (commit b0cc0a71e) allowed CTE references in
sub-selects within aggregate functions to affect the semantic levels
assigned to such aggregates.  It turns out this broke some related
cases, leading to assertion failures or strange planner errors such
as "unexpected outer reference in CTE query".  After experimenting
with some alternative rules for assigning the semantic level in
such cases, we've come to the conclusion that changing the level
is more likely to break things than be helpful.

Therefore, this patch undoes what b0cc0a71e changed, and instead
installs logic to throw an error if there is any reference to a
CTE that's below the semantic level that standard SQL rules would
assign to the aggregate based on its contained Var and Aggref nodes.
(The SQL standard disallows sub-selects within aggregate functions,
so it can't reach the troublesome case and hence has no rule for
what to do.)

Perhaps someone will come along with a legitimate query that this
logic rejects, and if so probably the example will help us craft
a level-adjustment rule that works better than what b0cc0a71e did.
I'm not holding my breath for that though, because the previous
logic had been there for a very long time before bug #19055 without
complaints, and that bug report sure looks to have originated from
fuzzing not from real usage.

Like b0cc0a71e, back-patch to all supported branches, though
sadly that no longer includes v13.

Bug: #19106
Reported-by: Kamil Monicz <[email protected]>
Author: Tom Lane <[email protected]>
Discussion: https://postgr.es/m/19106-9dd3668a0734cd72@postgresql.org
Backpatch-through: 14

src/backend/parser/parse_agg.c
src/test/regress/expected/with.out
src/test/regress/sql/with.sql

index d0592a1176c2d86e9b881aca6beffacc09d678a3..7a85651befe54f3eba59511e4e8b0b9bb9594394 100644 (file)
@@ -35,6 +35,8 @@ typedef struct
    ParseState *pstate;
    int         min_varlevel;
    int         min_agglevel;
+   int         min_ctelevel;
+   RangeTblEntry *min_cte;
    int         sublevels_up;
 } check_agg_arguments_context;
 
@@ -54,7 +56,8 @@ typedef struct
 static int check_agg_arguments(ParseState *pstate,
                                List *directargs,
                                List *args,
-                               Expr *filter);
+                               Expr *filter,
+                               int agglocation);
 static bool check_agg_arguments_walker(Node *node,
                                       check_agg_arguments_context *context);
 static void check_ungrouped_columns(Node *node, ParseState *pstate, Query *qry,
@@ -332,7 +335,8 @@ check_agglevels_and_constraints(ParseState *pstate, Node *expr)
    min_varlevel = check_agg_arguments(pstate,
                                       directargs,
                                       args,
-                                      filter);
+                                      filter,
+                                      location);
 
    *p_levelsup = min_varlevel;
 
@@ -626,7 +630,8 @@ static int
 check_agg_arguments(ParseState *pstate,
                    List *directargs,
                    List *args,
-                   Expr *filter)
+                   Expr *filter,
+                   int agglocation)
 {
    int         agglevel;
    check_agg_arguments_context context;
@@ -634,6 +639,8 @@ check_agg_arguments(ParseState *pstate,
    context.pstate = pstate;
    context.min_varlevel = -1;  /* signifies nothing found yet */
    context.min_agglevel = -1;
+   context.min_ctelevel = -1;
+   context.min_cte = NULL;
    context.sublevels_up = 0;
 
    (void) check_agg_arguments_walker((Node *) args, &context);
@@ -671,6 +678,20 @@ check_agg_arguments(ParseState *pstate,
                 parser_errposition(pstate, aggloc)));
    }
 
+   /*
+    * If there's a non-local CTE that's below the aggregate's semantic level,
+    * complain.  It's not quite clear what we should do to fix up such a case
+    * (treating the CTE reference like a Var seems wrong), and it's also
+    * unclear whether there is a real-world use for such cases.
+    */
+   if (context.min_ctelevel >= 0 && context.min_ctelevel < agglevel)
+       ereport(ERROR,
+               (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+                errmsg("outer-level aggregate cannot use a nested CTE"),
+                errdetail("CTE \"%s\" is below the aggregate's semantic level.",
+                          context.min_cte->eref->aliasname),
+                parser_errposition(pstate, agglocation)));
+
    /*
     * Now check for vars/aggs in the direct arguments, and throw error if
     * needed.  Note that we allow a Var of the agg's semantic level, but not
@@ -684,6 +705,7 @@ check_agg_arguments(ParseState *pstate,
    {
        context.min_varlevel = -1;
        context.min_agglevel = -1;
+       context.min_ctelevel = -1;
        (void) check_agg_arguments_walker((Node *) directargs, &context);
        if (context.min_varlevel >= 0 && context.min_varlevel < agglevel)
            ereport(ERROR,
@@ -699,6 +721,13 @@ check_agg_arguments(ParseState *pstate,
                     parser_errposition(pstate,
                                        locate_agg_of_level((Node *) directargs,
                                                            context.min_agglevel))));
+       if (context.min_ctelevel >= 0 && context.min_ctelevel < agglevel)
+           ereport(ERROR,
+                   (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+                    errmsg("outer-level aggregate cannot use a nested CTE"),
+                    errdetail("CTE \"%s\" is below the aggregate's semantic level.",
+                              context.min_cte->eref->aliasname),
+                    parser_errposition(pstate, agglocation)));
    }
    return agglevel;
 }
@@ -779,11 +808,6 @@ check_agg_arguments_walker(Node *node,
 
    if (IsA(node, RangeTblEntry))
    {
-       /*
-        * CTE references act similarly to Vars of the CTE's level.  Without
-        * this we might conclude that the Agg can be evaluated above the CTE,
-        * leading to trouble.
-        */
        RangeTblEntry *rte = (RangeTblEntry *) node;
 
        if (rte->rtekind == RTE_CTE)
@@ -795,9 +819,12 @@ check_agg_arguments_walker(Node *node,
            /* ignore local CTEs of subqueries */
            if (ctelevelsup >= 0)
            {
-               if (context->min_varlevel < 0 ||
-                   context->min_varlevel > ctelevelsup)
-                   context->min_varlevel = ctelevelsup;
+               if (context->min_ctelevel < 0 ||
+                   context->min_ctelevel > ctelevelsup)
+               {
+                   context->min_ctelevel = ctelevelsup;
+                   context->min_cte = rte;
+               }
            }
        }
        return false;           /* allow range_table_walker to continue */
index d7b431a9aa27bdfaed95636692faa5ffd906e815..8ce2b173fd4977c40b9a7efbd270ad97aef54977 100644 (file)
@@ -2195,36 +2195,44 @@ from int4_tbl;
 --
 -- test for bug #19055: interaction of WITH with aggregates
 --
--- The reference to cte1 must determine the aggregate's level,
--- even though it contains no Vars referencing cte1
-explain (verbose, costs off)
+-- For now, we just throw an error if there's a use of a CTE below the
+-- semantic level that the SQL standard assigns to the aggregate.
+-- It's not entirely clear what we could do instead that doesn't risk
+-- breaking more things than it fixes.
 select f1, (with cte1(x,y) as (select 1,2)
             select count((select i4.f1 from cte1))) as ss
 from int4_tbl i4;
-            QUERY PLAN             
------------------------------------
- Seq Scan on public.int4_tbl i4
-   Output: i4.f1, (SubPlan 2)
-   SubPlan 2
+ERROR:  outer-level aggregate cannot use a nested CTE
+LINE 2:             select count((select i4.f1 from cte1))) as ss
+                           ^
+DETAIL:  CTE "cte1" is below the aggregate's semantic level.
+--
+-- test for bug #19106: interaction of WITH with aggregates
+--
+-- the initial fix for #19055 was too aggressive and broke this case
+explain (verbose, costs off)
+with a as ( select id from (values (1), (2)) as v(id) ),
+     b as ( select max((select sum(id) from a)) as agg )
+select agg from b;
+                 QUERY PLAN                 
+--------------------------------------------
+ Aggregate
+   Output: max($0)
+   InitPlan 1 (returns $0)
      ->  Aggregate
-           Output: count($1)
-           InitPlan 1 (returns $1)
-             ->  Result
-                   Output: i4.f1
-           ->  Result
-(9 rows)
-
-select f1, (with cte1(x,y) as (select 1,2)
-            select count((select i4.f1 from cte1))) as ss
-from int4_tbl i4;
-     f1      | ss 
--------------+----
-           0 |  1
-      123456 |  1
-     -123456 |  1
-  2147483647 |  1
- -2147483647 |  1
-(5 rows)
+           Output: sum("*VALUES*".column1)
+           ->  Values Scan on "*VALUES*"
+                 Output: "*VALUES*".column1
+   ->  Result
+(8 rows)
+
+with a as ( select id from (values (1), (2)) as v(id) ),
+     b as ( select max((select sum(id) from a)) as agg )
+select agg from b;
+ agg 
+-----
+   3
+(1 row)
 
 --
 -- test for nested-recursive-WITH bug
index cd6ce45a4342d39eb029518386f911ed796fd2f1..ed1b38c1f3c08b114264d2aa3d5c592ed609aa7b 100644 (file)
@@ -1059,16 +1059,26 @@ from int4_tbl;
 --
 -- test for bug #19055: interaction of WITH with aggregates
 --
--- The reference to cte1 must determine the aggregate's level,
--- even though it contains no Vars referencing cte1
-explain (verbose, costs off)
+-- For now, we just throw an error if there's a use of a CTE below the
+-- semantic level that the SQL standard assigns to the aggregate.
+-- It's not entirely clear what we could do instead that doesn't risk
+-- breaking more things than it fixes.
 select f1, (with cte1(x,y) as (select 1,2)
             select count((select i4.f1 from cte1))) as ss
 from int4_tbl i4;
 
-select f1, (with cte1(x,y) as (select 1,2)
-            select count((select i4.f1 from cte1))) as ss
-from int4_tbl i4;
+--
+-- test for bug #19106: interaction of WITH with aggregates
+--
+-- the initial fix for #19055 was too aggressive and broke this case
+explain (verbose, costs off)
+with a as ( select id from (values (1), (2)) as v(id) ),
+     b as ( select max((select sum(id) from a)) as agg )
+select agg from b;
+
+with a as ( select id from (values (1), (2)) as v(id) ),
+     b as ( select max((select sum(id) from a)) as agg )
+select agg from b;
 
 --
 -- test for nested-recursive-WITH bug