Calculate agglevelsup correctly when Aggref contains a CTE.
authorTom Lane <[email protected]>
Wed, 17 Sep 2025 20:32:57 +0000 (16:32 -0400)
committerTom Lane <[email protected]>
Wed, 17 Sep 2025 20:32:57 +0000 (16:32 -0400)
If an aggregate function call contains a sub-select that has
an RTE referencing a CTE outside the aggregate, we must treat
that reference like a Var referencing the CTE's query level
for purposes of determining the aggregate's level.  Otherwise
we might reach the nonsensical conclusion that the aggregate
should be evaluated at some query level higher than the CTE,
ending in a planner error or a broken plan tree that causes
executor failures.

Bug: #19055
Reported-by: BugForge <[email protected]>
Author: Tom Lane <[email protected]>
Discussion: https://postgr.es/m/19055-6970cfa8556a394d@postgresql.org
Backpatch-through: 13

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

index be88909da222d95d4177ae24a06cf39122f60e48..cf449efdd3b5ba9b4fd31c09fa0b482acea206b5 100644 (file)
@@ -786,6 +786,32 @@ check_agg_arguments_walker(Node *node,
                     parser_errposition(context->pstate,
                                        ((WindowFunc *) node)->location)));
    }
+
+   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)
+       {
+           int         ctelevelsup = rte->ctelevelsup;
+
+           /* convert levelsup to frame of reference of original query */
+           ctelevelsup -= context->sublevels_up;
+           /* ignore local CTEs of subqueries */
+           if (ctelevelsup >= 0)
+           {
+               if (context->min_varlevel < 0 ||
+                   context->min_varlevel > ctelevelsup)
+                   context->min_varlevel = ctelevelsup;
+           }
+       }
+       return false;           /* allow range_table_walker to continue */
+   }
    if (IsA(node, Query))
    {
        /* Recurse into subselects */
@@ -795,7 +821,7 @@ check_agg_arguments_walker(Node *node,
        result = query_tree_walker((Query *) node,
                                   check_agg_arguments_walker,
                                   (void *) context,
-                                  0);
+                                  QTW_EXAMINE_RTES_BEFORE);
        context->sublevels_up--;
        return result;
    }
index 0a8b48d5e93b0ee9ef56529df2f74e2deed6ed20..2c55fa120a7ce9469da4ca8aadb3d11c6da1ad6b 100644 (file)
@@ -2276,6 +2276,40 @@ from int4_tbl;
  -2147483647
 (5 rows)
 
+--
+-- 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)
+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
+     ->  Aggregate
+           Output: count((InitPlan 1).col1)
+           InitPlan 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)
+
 --
 -- test for nested-recursive-WITH bug
 --
index 6d55c7731a9f4950ff9311025fc18f0cdcd87b6c..1a1dfa7b80036e4caf29919916d4404b17d4fa11 100644 (file)
@@ -1083,6 +1083,20 @@ select ( with cte(foo) as ( values(f1) )
           values((select foo from cte)) )
 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)
+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 nested-recursive-WITH bug
 --