0001-Fix-mixup-when-RETURNING-parenthesized-OLD-NEW-from-.patch
application/octet-stream
Filename: 0001-Fix-mixup-when-RETURNING-parenthesized-OLD-NEW-from-.patch
Type: application/octet-stream
Part: 0
Patch
Format: format-patch
Series: patch v2-0001
Subject: [BUG #19516] Fix mixup when RETURNING parenthesized OLD/NEW from DML
| File | + | − |
|---|---|---|
| src/backend/parser/parse_coerce.c | 4 | 5 |
| src/backend/parser/parse_func.c | 1 | 3 |
| src/backend/parser/parse_relation.c | 29 | 0 |
| src/backend/parser/parse_target.c | 1 | 1 |
| src/include/parser/parse_relation.h | 1 | 0 |
| src/test/regress/expected/returning.out | 47 | 0 |
| src/test/regress/sql/returning.sql | 19 | 0 |
From d26a4e776c252ee2f2e01cdc6815b4fdbc8b1616 Mon Sep 17 00:00:00 2001
From: Marko Grujic <markoog@gmail.com>
Date: Wed, 10 Jun 2026 15:27:33 +0200
Subject: [PATCH v2] [BUG #19516] Fix mixup when RETURNING parenthesized
OLD/NEW from DML
When a parenthesized OLD/NEW row reference is used in a RETURNING
clause of a INSERT/UPDATE/DELETE statement, the corresponding parser
namespace is wrongly resolved inside GetNSItemByRangeTablePosn.
This patch introduces GetNSItemByVar, which is explicitly aimed
for Var-shaped inputs, and ensures the returning type matches too.
Also adding tests exercising parenthesized OLD/NEW field selection,
and * expansion.
Author: Marko Grujic <markoog@gmail.com>
Suggested-by: Dean Rasheed <dean.a.rasheed@gmail.com>
Bug: #19516
Discussion: https://postgr.es/m/CAOvwyF2cO_5mAt=w=y-dFnaG5UkZ+3H8nSDoKF_iuWZHsU2ARg@mail.gmail.com
---
src/backend/parser/parse_coerce.c | 9 +++--
src/backend/parser/parse_func.c | 4 +--
src/backend/parser/parse_relation.c | 29 +++++++++++++++
src/backend/parser/parse_target.c | 2 +-
src/include/parser/parse_relation.h | 1 +
src/test/regress/expected/returning.out | 47 +++++++++++++++++++++++++
src/test/regress/sql/returning.sql | 19 ++++++++++
7 files changed, 102 insertions(+), 9 deletions(-)
diff --git a/src/backend/parser/parse_coerce.c b/src/backend/parser/parse_coerce.c
index 9edace67e1d..3b92ae2a920 100644
--- a/src/backend/parser/parse_coerce.c
+++ b/src/backend/parser/parse_coerce.c
@@ -1035,13 +1035,12 @@ coerce_record_to_complex(ParseState *pstate, Node *node,
else if (node && IsA(node, Var) &&
((Var *) node)->varattno == InvalidAttrNumber)
{
- int rtindex = ((Var *) node)->varno;
- int sublevels_up = ((Var *) node)->varlevelsup;
- int vlocation = ((Var *) node)->location;
+ Var *var = (Var *) node;
ParseNamespaceItem *nsitem;
- nsitem = GetNSItemByRangeTablePosn(pstate, rtindex, sublevels_up);
- args = expandNSItemVars(pstate, nsitem, sublevels_up, vlocation, NULL);
+ nsitem = GetNSItemByVar(pstate, var);
+ args = expandNSItemVars(pstate, nsitem, var->varlevelsup,
+ var->location, NULL);
}
else
ereport(ERROR,
diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c
index e07e7911f87..2e4cc1de50d 100644
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -2056,9 +2056,7 @@ ParseComplexProjection(ParseState *pstate, const char *funcname, Node *first_arg
{
ParseNamespaceItem *nsitem;
- nsitem = GetNSItemByRangeTablePosn(pstate,
- ((Var *) first_arg)->varno,
- ((Var *) first_arg)->varlevelsup);
+ nsitem = GetNSItemByVar(pstate, (Var *) first_arg);
/* Return a Var if funcname matches a column, else NULL */
return scanNSItemForColumn(pstate, nsitem,
((Var *) first_arg)->varlevelsup,
diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c
index 43460e4a5a5..03af6d809b5 100644
--- a/src/backend/parser/parse_relation.c
+++ b/src/backend/parser/parse_relation.c
@@ -533,6 +533,35 @@ GetNSItemByRangeTablePosn(ParseState *pstate,
return NULL; /* keep compiler quiet */
}
+/*
+ * Given a Var, find the corresponding ParseNamespaceItem (there must be one).
+ *
+ * Like GetNSItemByRangeTablePosn, but uses the Var's varno, varlevelsup, and
+ * varreturningtype.
+ */
+ParseNamespaceItem *
+GetNSItemByVar(ParseState *pstate, Var *var)
+{
+ int sublevels_up = var->varlevelsup;
+ ListCell *lc;
+
+ while (sublevels_up-- > 0)
+ {
+ pstate = pstate->parentParseState;
+ Assert(pstate != NULL);
+ }
+ foreach(lc, pstate->p_namespace)
+ {
+ ParseNamespaceItem *nsitem = (ParseNamespaceItem *) lfirst(lc);
+
+ if (nsitem->p_rtindex == var->varno &&
+ nsitem->p_returning_type == var->varreturningtype)
+ return nsitem;
+ }
+ elog(ERROR, "nsitem not found (internal error)");
+ return NULL; /* keep compiler quiet */
+}
+
/*
* Given an RT index and nesting depth, find the corresponding RTE.
* (Note that the RTE need not be in the query's namespace.)
diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c
index 541fef5f183..6a862d5a4f9 100644
--- a/src/backend/parser/parse_target.c
+++ b/src/backend/parser/parse_target.c
@@ -1451,7 +1451,7 @@ ExpandRowReference(ParseState *pstate, Node *expr,
Var *var = (Var *) expr;
ParseNamespaceItem *nsitem;
- nsitem = GetNSItemByRangeTablePosn(pstate, var->varno, var->varlevelsup);
+ nsitem = GetNSItemByVar(pstate, var);
return ExpandSingleTable(pstate, nsitem, var->varlevelsup, var->location, make_target_entry);
}
diff --git a/src/include/parser/parse_relation.h b/src/include/parser/parse_relation.h
index 59721a70efb..1bf39922c4c 100644
--- a/src/include/parser/parse_relation.h
+++ b/src/include/parser/parse_relation.h
@@ -32,6 +32,7 @@ extern void checkNameSpaceConflicts(ParseState *pstate, List *namespace1,
extern ParseNamespaceItem *GetNSItemByRangeTablePosn(ParseState *pstate,
int varno,
int sublevels_up);
+extern ParseNamespaceItem *GetNSItemByVar(ParseState *pstate, Var *var);
extern RangeTblEntry *GetRTEByRangeTablePosn(ParseState *pstate,
int varno,
int sublevels_up);
diff --git a/src/test/regress/expected/returning.out b/src/test/regress/expected/returning.out
index 196829e94fa..bede35135e3 100644
--- a/src/test/regress/expected/returning.out
+++ b/src/test/regress/expected/returning.out
@@ -542,6 +542,53 @@ DELETE FROM foo WHERE f1 = 5
foo | (0,7) | 5 | ok | 42 | 100 | | | | | | | 5 | ok | 42 | 100
(1 row)
+-- Parenthesized OLD/NEW field selection matches the unparenthesized form
+INSERT INTO foo VALUES (6, 'paren-test', 60, 600)
+ 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
+(1 row)
+
+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)
+
+DELETE FROM foo 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
+---------+-----------+---------+-----------
+ 700 | 700 | |
+(1 row)
+
+-- Parenthesized OLD/NEW star expansion matches the unparenthesized set
+INSERT INTO foo VALUES (8, 'paren-star', 80, 800)
+ RETURNING old.*, (old).*, new.*, (new).*;
+ f1 | f2 | f3 | f4 | f1 | f2 | f3 | f4 | f1 | f2 | f3 | f4 | f1 | f2 | f3 | f4
+----+----+----+----+----+----+----+----+----+------------+----+-----+----+------------+----+-----
+ | | | | | | | | 8 | paren-star | 80 | 800 | 8 | paren-star | 80 | 800
+(1 row)
+
+UPDATE foo SET f4 = 900 WHERE f1 = 8
+ RETURNING old.*, (old).*, new.*, (new).*;
+ f1 | f2 | f3 | f4 | f1 | f2 | f3 | f4 | f1 | f2 | f3 | f4 | f1 | f2 | f3 | f4
+----+------------+----+-----+----+------------+----+-----+----+------------+----+-----+----+------------+----+-----
+ 8 | paren-star | 80 | 800 | 8 | paren-star | 80 | 800 | 8 | paren-star | 80 | 900 | 8 | paren-star | 80 | 900
+(1 row)
+
+DELETE FROM foo WHERE f1 = 8
+ RETURNING old.*, (old).*, new.*, (new).*;
+ f1 | f2 | f3 | f4 | f1 | f2 | f3 | f4 | f1 | f2 | f3 | f4 | f1 | f2 | f3 | f4
+----+------------+----+-----+----+------------+----+-----+----+----+----+----+----+----+----+----
+ 8 | paren-star | 80 | 900 | 8 | paren-star | 80 | 900 | | | | | | | |
+(1 row)
+
-- 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..cd7c01c4240 100644
--- a/src/test/regress/sql/returning.sql
+++ b/src/test/regress/sql/returning.sql
@@ -243,6 +243,25 @@ DELETE FROM foo WHERE f1 = 5
RETURNING old.tableoid::regclass, old.ctid, old.*,
new.tableoid::regclass, new.ctid, new.*, *;
+-- Parenthesized OLD/NEW field selection matches the unparenthesized form
+INSERT INTO foo VALUES (6, 'paren-test', 60, 600)
+ RETURNING old.f4 AS old_dot, (old).f4 AS old_paren,
+ new.f4 AS new_dot, (new).f4 AS new_paren;
+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;
+DELETE FROM foo WHERE f1 = 6
+ RETURNING old.f4 AS old_dot, (old).f4 AS old_paren,
+ new.f4 AS new_dot, (new).f4 AS new_paren;
+
+-- Parenthesized OLD/NEW star expansion matches the unparenthesized set
+INSERT INTO foo VALUES (8, 'paren-star', 80, 800)
+ RETURNING old.*, (old).*, new.*, (new).*;
+UPDATE foo SET f4 = 900 WHERE f1 = 8
+ RETURNING old.*, (old).*, new.*, (new).*;
+DELETE FROM foo WHERE f1 = 8
+ RETURNING old.*, (old).*, new.*, (new).*;
+
-- 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)