From 19a58bbf964690ba3e9fae1207e2f4be0887752a Mon Sep 17 00:00:00 2001 From: Marko Grujic Date: Wed, 10 Jun 2026 11:06:59 +0200 Subject: [PATCH v1] [BUG #19516] Skip whole-row projection shortcut for OLD/NEW returning type Otherwise we lose varreturningtype, default to VAR_RETURNING_DEFAULT, and return the wrong values: (old).col yields the NEW value on INSERT/UPDATE, and (new).col yields the OLD value on DELETE. Author: Marko Grujic Bug: #19516 --- src/backend/parser/parse_func.c | 8 +++++++- src/test/regress/expected/returning.out | 26 +++++++++++++++++++++++++ src/test/regress/sql/returning.sql | 12 ++++++++++++ 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c index e07e7911f87..16f0733dbfe 100644 --- a/src/backend/parser/parse_func.c +++ b/src/backend/parser/parse_func.c @@ -2050,9 +2050,15 @@ ParseComplexProjection(ParseState *pstate, const char *funcname, Node *first_arg * * This case could be handled by expandRecordVariable, but it's more * efficient to do it this way when possible. + * + * Skip OLD/NEW whole-row Vars: in a RETURNING list multiple namespace + * items share (varno, varlevelsup), so the shortcut would drop the + * OLD/NEW marker and conflate both with the default-returning entry. */ if (IsA(first_arg, Var) && - ((Var *) first_arg)->varattno == InvalidAttrNumber) + ((Var *) first_arg)->varattno == InvalidAttrNumber && + ((Var *) first_arg)->varreturningtype != VAR_RETURNING_OLD && + ((Var *) first_arg)->varreturningtype != VAR_RETURNING_NEW) { ParseNamespaceItem *nsitem; diff --git a/src/test/regress/expected/returning.out b/src/test/regress/expected/returning.out index 196829e94fa..f04509c5753 100644 --- a/src/test/regress/expected/returning.out +++ b/src/test/regress/expected/returning.out @@ -542,6 +542,32 @@ DELETE FROM foo WHERE f1 = 5 foo | (0,7) | 5 | ok | 42 | 100 | | | | | | | 5 | ok | 42 | 100 (1 row) +-- Parenthesized OLD/NEW matches the unparenthesized dot form +INSERT INTO foo VALUES (6, 'paren-test', 60, 600); +UPDATE foo SET f4 = 700 WHERE f1 = 6 + RETURNING old.f4 AS old_dot, (old).f4 AS old_paren, + new.f4 AS new_dot, (new).f4 AS new_paren; + old_dot | old_paren | new_dot | new_paren +---------+-----------+---------+----------- + 600 | 600 | 700 | 700 +(1 row) + +INSERT INTO foo VALUES (7, 'paren-insert') + RETURNING (old).f1 AS old_paren_f1, (old).f2 AS old_paren_f2, + (new).f1 AS new_paren_f1, (new).f2 AS new_paren_f2; + old_paren_f1 | old_paren_f2 | new_paren_f1 | new_paren_f2 +--------------+--------------+--------------+-------------- + | | 7 | paren-insert +(1 row) + +DELETE FROM foo WHERE f1 = 7 + RETURNING (old).f2 AS old_paren_f2, (new).f2 AS new_paren_f2; + old_paren_f2 | new_paren_f2 +--------------+-------------- + paren-insert | +(1 row) + +DELETE FROM foo WHERE f1 = 6; -- RETURNING OLD and NEW from subquery EXPLAIN (verbose, costs off) INSERT INTO foo VALUES (5, 'subquery test') diff --git a/src/test/regress/sql/returning.sql b/src/test/regress/sql/returning.sql index b3c8c5df550..8f271f05e7a 100644 --- a/src/test/regress/sql/returning.sql +++ b/src/test/regress/sql/returning.sql @@ -243,6 +243,18 @@ DELETE FROM foo WHERE f1 = 5 RETURNING old.tableoid::regclass, old.ctid, old.*, new.tableoid::regclass, new.ctid, new.*, *; +-- Parenthesized OLD/NEW matches the unparenthesized dot form +INSERT INTO foo VALUES (6, 'paren-test', 60, 600); +UPDATE foo SET f4 = 700 WHERE f1 = 6 + RETURNING old.f4 AS old_dot, (old).f4 AS old_paren, + new.f4 AS new_dot, (new).f4 AS new_paren; +INSERT INTO foo VALUES (7, 'paren-insert') + RETURNING (old).f1 AS old_paren_f1, (old).f2 AS old_paren_f2, + (new).f1 AS new_paren_f1, (new).f2 AS new_paren_f2; +DELETE FROM foo WHERE f1 = 7 + RETURNING (old).f2 AS old_paren_f2, (new).f2 AS new_paren_f2; +DELETE FROM foo WHERE f1 = 6; + -- RETURNING OLD and NEW from subquery EXPLAIN (verbose, costs off) INSERT INTO foo VALUES (5, 'subquery test') base-commit: e18b0cb7344cb4bd28468f6c0aeeb9b9241d30aa -- 2.39.5 (Apple Git-154)