Fix EXPLAIN of SEARCH BREADTH FIRST with a constant initial value.
authorTom Lane <tgl@sss.pgh.pa.us>
Sun, 16 Oct 2022 23:18:08 +0000 (19:18 -0400)
committerTom Lane <tgl@sss.pgh.pa.us>
Sun, 16 Oct 2022 23:18:08 +0000 (19:18 -0400)
If the non-recursive term of a SEARCH BREADTH FIRST recursive
query has only constants in its target list, the planner will
fold the starting RowExpr added by rewrite into a simple Const
of type RECORD.  The executor doesn't have any problem with
that --- but EXPLAIN VERBOSE will encounter the Const as the
ultimate source of truth about what the field names of the
SET column are, and it didn't know what to do with that.
Fortunately, we can pull the identifying typmod out of the
Const, in much the same way that record_out would.

For reasons that remain a bit obscure to me, this only fails
with SEARCH BREADTH FIRST, not SEARCH DEPTH FIRST or CYCLE.
But I added regression test cases for both of those options
too, just to make sure we don't break it in future.

Per bug #17644 from Matthijs van der Vleuten.  Back-patch
to v14 where these constructs were added.

Discussion: https://postgr.es/m/17644-3bd1f3036d6d7a16@postgresql.org

src/backend/utils/adt/ruleutils.c
src/backend/utils/fmgr/funcapi.c
src/test/regress/expected/with.out
src/test/regress/sql/with.sql

index 67a8728d1baf5d41927e17aeccb0e528980f5927..d583b8e6f15945a1f73ea4bd3f50d830093d9c6f 100644 (file)
@@ -7505,7 +7505,8 @@ get_name_for_var_field(Var *var, int fieldno,
 
    /*
     * If it's a RowExpr that was expanded from a whole-row Var, use the
-    * column names attached to it.
+    * column names attached to it.  (We could let get_expr_result_tupdesc()
+    * handle this, but it's much cheaper to just pull out the name we need.)
     */
    if (IsA(var, RowExpr))
    {
index 9197b0f1e266cb5c609d719784753993fc51c3cc..e82a6d50654a84fbb13ab6d0d8e0b2fdd6160301 100644 (file)
@@ -339,6 +339,40 @@ get_expr_result_type(Node *expr,
            *resultTupleDesc = BlessTupleDesc(tupdesc);
        return TYPEFUNC_COMPOSITE;
    }
+   else if (expr && IsA(expr, Const) &&
+            ((Const *) expr)->consttype == RECORDOID &&
+            !((Const *) expr)->constisnull)
+   {
+       /*
+        * When EXPLAIN'ing some queries with SEARCH/CYCLE clauses, we may
+        * need to resolve field names of a RECORD-type Const.  The datum
+        * should contain a typmod that will tell us that.
+        */
+       HeapTupleHeader rec;
+       Oid         tupType;
+       int32       tupTypmod;
+
+       rec = DatumGetHeapTupleHeader(((Const *) expr)->constvalue);
+       tupType = HeapTupleHeaderGetTypeId(rec);
+       tupTypmod = HeapTupleHeaderGetTypMod(rec);
+       if (resultTypeId)
+           *resultTypeId = tupType;
+       if (tupType != RECORDOID || tupTypmod >= 0)
+       {
+           /* Should be able to look it up */
+           if (resultTupleDesc)
+               *resultTupleDesc = lookup_rowtype_tupdesc_copy(tupType,
+                                                              tupTypmod);
+           return TYPEFUNC_COMPOSITE;
+       }
+       else
+       {
+           /* This shouldn't really happen ... */
+           if (resultTupleDesc)
+               *resultTupleDesc = NULL;
+           return TYPEFUNC_RECORD;
+       }
+   }
    else
    {
        /* handle as a generic expression; no chance to resolve RECORD */
index 30dd900e1141e4519b4ad377cf8c943942487fc4..9c075cf0c7ad64430f1c94f85ba8f7f958c6bca5 100644 (file)
@@ -790,6 +790,83 @@ select * from search_graph order by seq;
  4 | 5 | arc 4 -> 5 | (1,4,5)
 (7 rows)
 
+-- a constant initial value causes issues for EXPLAIN
+explain (verbose, costs off)
+with recursive test as (
+  select 1 as x
+  union all
+  select x + 1
+  from test
+) search depth first by x set y
+select * from test limit 5;
+                                       QUERY PLAN                                        
+-----------------------------------------------------------------------------------------
+ Limit
+   Output: test.x, test.y
+   CTE test
+     ->  Recursive Union
+           ->  Result
+                 Output: 1, '{(1)}'::record[]
+           ->  WorkTable Scan on test test_1
+                 Output: (test_1.x + 1), array_cat(test_1.y, ARRAY[ROW((test_1.x + 1))])
+   ->  CTE Scan on test
+         Output: test.x, test.y
+(10 rows)
+
+with recursive test as (
+  select 1 as x
+  union all
+  select x + 1
+  from test
+) search depth first by x set y
+select * from test limit 5;
+ x |           y           
+---+-----------------------
+ 1 | {(1)}
+ 2 | {(1),(2)}
+ 3 | {(1),(2),(3)}
+ 4 | {(1),(2),(3),(4)}
+ 5 | {(1),(2),(3),(4),(5)}
+(5 rows)
+
+explain (verbose, costs off)
+with recursive test as (
+  select 1 as x
+  union all
+  select x + 1
+  from test
+) search breadth first by x set y
+select * from test limit 5;
+                                         QUERY PLAN                                         
+--------------------------------------------------------------------------------------------
+ Limit
+   Output: test.x, test.y
+   CTE test
+     ->  Recursive Union
+           ->  Result
+                 Output: 1, '(0,1)'::record
+           ->  WorkTable Scan on test test_1
+                 Output: (test_1.x + 1), ROW(int8inc((test_1.y)."*DEPTH*"), (test_1.x + 1))
+   ->  CTE Scan on test
+         Output: test.x, test.y
+(10 rows)
+
+with recursive test as (
+  select 1 as x
+  union all
+  select x + 1
+  from test
+) search breadth first by x set y
+select * from test limit 5;
+ x |   y   
+---+-------
+ 1 | (0,1)
+ 2 | (1,2)
+ 3 | (2,3)
+ 4 | (3,4)
+ 5 | (4,5)
+(5 rows)
+
 -- various syntax errors
 with recursive search_graph(f, t, label) as (
    select * from graph0 g
@@ -1132,6 +1209,49 @@ select * from search_graph;
  2 | 3 | arc 2 -> 3 | N        | {"(1,4)","(4,5)","(5,1)","(1,2)","(2,3)"}
 (25 rows)
 
+explain (verbose, costs off)
+with recursive test as (
+  select 0 as x
+  union all
+  select (x + 1) % 10
+  from test
+) cycle x set is_cycle using path
+select * from test;
+                                                                                          QUERY PLAN                                                                                           
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CTE Scan on test
+   Output: test.x, test.is_cycle, test.path
+   CTE test
+     ->  Recursive Union
+           ->  Result
+                 Output: 0, false, '{(0)}'::record[]
+           ->  WorkTable Scan on test test_1
+                 Output: ((test_1.x + 1) % 10), CASE WHEN (ROW(((test_1.x + 1) % 10)) = ANY (test_1.path)) THEN true ELSE false END, array_cat(test_1.path, ARRAY[ROW(((test_1.x + 1) % 10))])
+                 Filter: (NOT test_1.is_cycle)
+(9 rows)
+
+with recursive test as (
+  select 0 as x
+  union all
+  select (x + 1) % 10
+  from test
+) cycle x set is_cycle using path
+select * from test;
+ x | is_cycle |                     path                      
+---+----------+-----------------------------------------------
+ 0 | f        | {(0)}
+ 1 | f        | {(0),(1)}
+ 2 | f        | {(0),(1),(2)}
+ 3 | f        | {(0),(1),(2),(3)}
+ 4 | f        | {(0),(1),(2),(3),(4)}
+ 5 | f        | {(0),(1),(2),(3),(4),(5)}
+ 6 | f        | {(0),(1),(2),(3),(4),(5),(6)}
+ 7 | f        | {(0),(1),(2),(3),(4),(5),(6),(7)}
+ 8 | f        | {(0),(1),(2),(3),(4),(5),(6),(7),(8)}
+ 9 | f        | {(0),(1),(2),(3),(4),(5),(6),(7),(8),(9)}
+ 0 | t        | {(0),(1),(2),(3),(4),(5),(6),(7),(8),(9),(0)}
+(11 rows)
+
 -- multiple CTEs
 with recursive
 graph(f, t, label) as (
index 5c52561a8aa4295a6b59f270d318f707a2d98c37..87e78541fc701c5990fd6d85c8f92e6e6e204cee 100644 (file)
@@ -414,6 +414,41 @@ with recursive search_graph(f, t, label) as (
 ) search breadth first by f, t set seq
 select * from search_graph order by seq;
 
+-- a constant initial value causes issues for EXPLAIN
+explain (verbose, costs off)
+with recursive test as (
+  select 1 as x
+  union all
+  select x + 1
+  from test
+) search depth first by x set y
+select * from test limit 5;
+
+with recursive test as (
+  select 1 as x
+  union all
+  select x + 1
+  from test
+) search depth first by x set y
+select * from test limit 5;
+
+explain (verbose, costs off)
+with recursive test as (
+  select 1 as x
+  union all
+  select x + 1
+  from test
+) search breadth first by x set y
+select * from test limit 5;
+
+with recursive test as (
+  select 1 as x
+  union all
+  select x + 1
+  from test
+) search breadth first by x set y
+select * from test limit 5;
+
 -- various syntax errors
 with recursive search_graph(f, t, label) as (
    select * from graph0 g
@@ -561,6 +596,23 @@ with recursive search_graph(f, t, label) as (
 ) cycle f, t set is_cycle to 'Y' default 'N' using path
 select * from search_graph;
 
+explain (verbose, costs off)
+with recursive test as (
+  select 0 as x
+  union all
+  select (x + 1) % 10
+  from test
+) cycle x set is_cycle using path
+select * from test;
+
+with recursive test as (
+  select 0 as x
+  union all
+  select (x + 1) % 10
+  from test
+) cycle x set is_cycle using path
+select * from test;
+
 -- multiple CTEs
 with recursive
 graph(f, t, label) as (