Build whole-row Vars the same way during parsing and planning.
authorTom Lane <[email protected]>
Wed, 12 Mar 2025 15:47:19 +0000 (11:47 -0400)
committerTom Lane <[email protected]>
Wed, 12 Mar 2025 15:47:38 +0000 (11:47 -0400)
makeWholeRowVar() has different rules for constructing a
whole-row Var depending on the kind of RTE it's representing.
This turns out to be problematic because the rewriter and planner
can convert view RTEs and set-returning-function RTEs into
subquery RTEs; so a whole-row Var made during planning might
look different from one made by the parser.  In isolation this
doesn't cause any problem, but if a query contains Vars made
both ways for the same varno, there are cross-checks in the
executor that will complain.  This manifests for UPDATE, DELETE,
and MERGE queries that use whole-row table references.

To fix, we need makeWholeRowVar() to produce the same result
from an inlined RTE as it would have for the original.  For
an inlined view, we can use RangeTblEntry.relid to detect
that this had been a view RTE.  For inlined SRFs, make a
data structure definition change akin to commit 47bb9db75,
and say that we won't clear RangeTblEntry.functions until
the end of planning.  That allows makeWholeRowVar() to
repeat what it would have done with the unmodified RTE.

Reported-by: Duncan Sands <[email protected]>
Reported-by: Dean Rasheed <[email protected]>
Diagnosed-by: Tender Wang <[email protected]>
Author: Tom Lane <[email protected]>
Reviewed-by: Dean Rasheed <[email protected]>
Discussion: https://postgr.es/m/3518c50a-ab18-482f-b916-a37263622501@deepbluecap.com
Backpatch-through: 13

src/backend/nodes/makefuncs.c
src/backend/optimizer/prep/prepjointree.c
src/test/regress/expected/returning.out
src/test/regress/sql/returning.sql

index dbbc2f1e30d1b1bef0794a3de0b59583c06d1523..e2d9e9be41a65f79d7da662b234b3f873e2e290d 100644 (file)
@@ -161,6 +161,53 @@ makeWholeRowVar(RangeTblEntry *rte,
                             varlevelsup);
            break;
 
+       case RTE_SUBQUERY:
+
+           /*
+            * For a standard subquery, the Var should be of RECORD type.
+            * However, if we're looking at a subquery that was expanded from
+            * a view or SRF (only possible during planning), we must use the
+            * appropriate rowtype, so that the resulting Var has the same
+            * type that we would have produced from the original RTE.
+            */
+           if (OidIsValid(rte->relid))
+           {
+               /* Subquery was expanded from a view */
+               toid = get_rel_type_id(rte->relid);
+               if (!OidIsValid(toid))
+                   ereport(ERROR,
+                           (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+                            errmsg("relation \"%s\" does not have a composite type",
+                                   get_rel_name(rte->relid))));
+           }
+           else if (rte->functions)
+           {
+               /*
+                * Subquery was expanded from a set-returning function.  That
+                * would not have happened if there's more than one function
+                * or ordinality was requested.  We also needn't worry about
+                * the allowScalar case, since the planner doesn't use that.
+                * Otherwise this must match the RTE_FUNCTION code below.
+                */
+               Assert(!allowScalar);
+               fexpr = ((RangeTblFunction *) linitial(rte->functions))->funcexpr;
+               toid = exprType(fexpr);
+               if (!type_is_rowtype(toid))
+                   toid = RECORDOID;
+           }
+           else
+           {
+               /* Normal subquery-in-FROM */
+               toid = RECORDOID;
+           }
+           result = makeVar(varno,
+                            InvalidAttrNumber,
+                            toid,
+                            -1,
+                            InvalidOid,
+                            varlevelsup);
+           break;
+
        case RTE_FUNCTION:
 
            /*
@@ -217,8 +264,8 @@ makeWholeRowVar(RangeTblEntry *rte,
        default:
 
            /*
-            * RTE is a join, subselect, tablefunc, or VALUES.  We represent
-            * this as a whole-row Var of RECORD type. (Note that in most
+            * RTE is a join, tablefunc, VALUES, CTE, etc.  We represent these
+            * cases as a whole-row Var of RECORD type.  (Note that in most
             * cases the Var will be expanded to a RowExpr during planning,
             * but that is not our concern here.)
             */
index bcc40dd5a84383028a4f9de9e6f5ef6894556202..a9efc0b23a2290f34f85c355da4a764fca06edb5 100644 (file)
@@ -914,8 +914,14 @@ preprocess_function_rtes(PlannerInfo *root)
                rte->rtekind = RTE_SUBQUERY;
                rte->subquery = funcquery;
                rte->security_barrier = false;
-               /* Clear fields that should not be set in a subquery RTE */
-               rte->functions = NIL;
+
+               /*
+                * Clear fields that should not be set in a subquery RTE.
+                * However, we leave rte->functions filled in for the moment,
+                * in case makeWholeRowVar needs to consult it.  We'll clear
+                * it in setrefs.c (see add_rte_to_flat_rtable) so that this
+                * abuse of the data structure doesn't escape the planner.
+                */
                rte->funcordinality = false;
            }
        }
index d1394c67833eb3d26ae26e3e37d439fc4d6a8bd6..341b689f76655c31ed05d5ae85119dda89660e37 100644 (file)
@@ -286,6 +286,63 @@ SELECT * FROM voo;
  16 | zoo2
 (2 rows)
 
+-- Check use of a whole-row variable for an un-flattenable view
+CREATE TEMP VIEW foo_v AS SELECT * FROM foo OFFSET 0;
+UPDATE foo SET f2 = foo_v.f2 FROM foo_v WHERE foo_v.f1 = foo.f1
+  RETURNING foo_v;
+      foo_v      
+-----------------
+ (2,more,42,141)
+ (16,zoo2,57,99)
+(2 rows)
+
+SELECT * FROM foo;
+ f1 |  f2  | f3 | f4  
+----+------+----+-----
+  2 | more | 42 | 141
+ 16 | zoo2 | 57 |  99
+(2 rows)
+
+-- Check use of a whole-row variable for an inlined set-returning function
+CREATE FUNCTION foo_f() RETURNS SETOF foo AS
+  $$ SELECT * FROM foo OFFSET 0 $$ LANGUAGE sql STABLE;
+UPDATE foo SET f2 = foo_f.f2 FROM foo_f() WHERE foo_f.f1 = foo.f1
+  RETURNING foo_f;
+      foo_f      
+-----------------
+ (2,more,42,141)
+ (16,zoo2,57,99)
+(2 rows)
+
+SELECT * FROM foo;
+ f1 |  f2  | f3 | f4  
+----+------+----+-----
+  2 | more | 42 | 141
+ 16 | zoo2 | 57 |  99
+(2 rows)
+
+DROP FUNCTION foo_f();
+-- As above, but SRF is defined to return a composite type
+CREATE TYPE foo_t AS (f1 int, f2 text, f3 int, f4 int8);
+CREATE FUNCTION foo_f() RETURNS SETOF foo_t AS
+  $$ SELECT * FROM foo OFFSET 0 $$ LANGUAGE sql STABLE;
+UPDATE foo SET f2 = foo_f.f2 FROM foo_f() WHERE foo_f.f1 = foo.f1
+  RETURNING foo_f;
+      foo_f      
+-----------------
+ (2,more,42,141)
+ (16,zoo2,57,99)
+(2 rows)
+
+SELECT * FROM foo;
+ f1 |  f2  | f3 | f4  
+----+------+----+-----
+  2 | more | 42 | 141
+ 16 | zoo2 | 57 |  99
+(2 rows)
+
+DROP FUNCTION foo_f();
+DROP TYPE foo_t;
 -- Try a join case
 CREATE TEMP TABLE joinme (f2j text, other int);
 INSERT INTO joinme VALUES('more', 12345);
@@ -726,8 +783,9 @@ NOTICE:  UPDATE: (3,zoo2,58,99,54321) -> (3,zoo2,59,7,54321)
 
 -- Test wholerow & dropped column handling
 ALTER TABLE foo DROP COLUMN f3 CASCADE;
-NOTICE:  drop cascades to 3 other objects
+NOTICE:  drop cascades to 4 other objects
 DETAIL:  drop cascades to rule voo_i on view voo
+drop cascades to view foo_v
 drop cascades to view joinview
 drop cascades to rule foo_del_rule on table foo
 UPDATE foo SET f4 = f4 + 1 RETURNING old.f3;  -- should fail
index 54caf56244c0fc0f1f81028a0a5fa460c38b16ab..cc99cb53f63c4b87e152af838e62da53e29d9e93 100644 (file)
@@ -132,6 +132,30 @@ DELETE FROM foo WHERE f2 = 'zit' RETURNING *;
 SELECT * FROM foo;
 SELECT * FROM voo;
 
+-- Check use of a whole-row variable for an un-flattenable view
+CREATE TEMP VIEW foo_v AS SELECT * FROM foo OFFSET 0;
+UPDATE foo SET f2 = foo_v.f2 FROM foo_v WHERE foo_v.f1 = foo.f1
+  RETURNING foo_v;
+SELECT * FROM foo;
+
+-- Check use of a whole-row variable for an inlined set-returning function
+CREATE FUNCTION foo_f() RETURNS SETOF foo AS
+  $$ SELECT * FROM foo OFFSET 0 $$ LANGUAGE sql STABLE;
+UPDATE foo SET f2 = foo_f.f2 FROM foo_f() WHERE foo_f.f1 = foo.f1
+  RETURNING foo_f;
+SELECT * FROM foo;
+DROP FUNCTION foo_f();
+
+-- As above, but SRF is defined to return a composite type
+CREATE TYPE foo_t AS (f1 int, f2 text, f3 int, f4 int8);
+CREATE FUNCTION foo_f() RETURNS SETOF foo_t AS
+  $$ SELECT * FROM foo OFFSET 0 $$ LANGUAGE sql STABLE;
+UPDATE foo SET f2 = foo_f.f2 FROM foo_f() WHERE foo_f.f1 = foo.f1
+  RETURNING foo_f;
+SELECT * FROM foo;
+DROP FUNCTION foo_f();
+DROP TYPE foo_t;
+
 -- Try a join case
 
 CREATE TEMP TABLE joinme (f2j text, other int);