From 19a58bbf964690ba3e9fae1207e2f4be0887752a Mon Sep 17 00:00:00 2001
From: Marko Grujic <markoog@gmail.com>
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 <markoog@gmail.com>
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)

