]> git.ipfire.org Git - thirdparty/postgresql.git/commitdiff
Avoid RETURNING side effects for FOR PORTION OF leftovers.
authorDean Rasheed <dean.a.rasheed@gmail.com>
Tue, 28 Jul 2026 08:44:23 +0000 (09:44 +0100)
committerDean Rasheed <dean.a.rasheed@gmail.com>
Tue, 28 Jul 2026 08:44:23 +0000 (09:44 +0100)
UPDATE/DELETE ... FOR PORTION OF inserts leftover rows for the
untouched parts of the original row. These hidden inserts should not
affect the command tag or ROW_COUNT, so they call ExecInsert() with
canSetTag set to false.

However, ExecInsert() still processed the RETURNING list whenever the
target ResultRelInfo had ri_projectReturning set. That caused
RETURNING expressions to be evaluated for leftover rows even though
their results were discarded. As a result, expressions with side
effects and information-leaking functions could be executed on the
leftover rows, in addition to the visibly updated or deleted row.

Fix by having ExecInsert() skip RETURNING processing when it is
handling an internal FOR PORTION OF leftover insert. Use both the
presence of a FOR PORTION OF clause and mtstate->operation ==
CMD_INSERT for this check, so that the auxiliary INSERT of a
cross-partition UPDATE with a FOR PORTION OF clause still processes
RETURNING normally.

Back-patch to v19, where support for FOR PORTION OF was added.

Author: Chao Li <lic@highgo.com>
Reviewed-by: Dean Rasheed <dean.a.rasheed@gmail.com>
Reviewed-by: Paul A Jungwirth <pj@illuminatedcomputing.com>
Discussion: https://postgr.es/m/07C125E5-F6ED-460C-A394-E6503DAE18FB@gmail.com
Backpatch-through: 19

src/backend/executor/nodeModifyTable.c
src/test/regress/expected/for_portion_of.out
src/test/regress/sql/for_portion_of.sql

index 1dbf0ffff9ed668bafee3d4f546ef8e370104769..9a1c0992bfea80de240efec963633d59fac955df 100644 (file)
@@ -1335,8 +1335,18 @@ ExecInsert(ModifyTableContext *context,
        if (resultRelInfo->ri_WithCheckOptions != NIL)
                ExecWithCheckOptions(WCO_VIEW_CHECK, resultRelInfo, slot, estate);
 
-       /* Process RETURNING if present */
-       if (resultRelInfo->ri_projectReturning)
+       /*
+        * Process RETURNING if present.
+        *
+        * If this is an UPDATE/DELETE ... FOR PORTION OF, we do not return the
+        * leftover rows inserted by ExecForPortionOfLeftovers().  Note that we
+        * must check mtstate->operation here, because we *do* want to process the
+        * newly inserted row of a cross-partition UPDATE with a FOR PORTION OF
+        * clause (ExecCrossPartitionUpdate() leaves mtstate->operation set to
+        * CMD_UPDATE, whereas ExecForPortionOfLeftovers() sets it to CMD_INSERT).
+        */
+       if (resultRelInfo->ri_projectReturning &&
+               !(node->forPortionOf && mtstate->operation == CMD_INSERT))
        {
                TupleTableSlot *oldSlot = NULL;
 
index 3c592f90e70ce9970c35b2ac359d99d7f604a66d..a6cb1ba8380ce8c10194c50793c6425f52282af2 100644 (file)
@@ -920,14 +920,23 @@ SELECT * FROM for_portion_of_test ORDER BY id, valid_at;
 \set QUIET true
 -- UPDATE ... RETURNING returns only the updated values
 -- (not the inserted side values, which are added by a separate "statement"):
+CREATE FUNCTION fpo_returning_row(text)
+RETURNS text LANGUAGE plpgsql AS
+$$
+BEGIN
+  RAISE NOTICE 'RETURNING %', $1;
+  RETURN $1;
+END;
+$$;
 UPDATE for_portion_of_test
   FOR PORTION OF valid_at FROM '2018-02-01' TO '2018-02-15'
   SET name = 'three^3'
   WHERE id = '[3,4)'
-  RETURNING *;
-  id   |        valid_at         |  name   
--------+-------------------------+---------
- [3,4) | [2018-02-01,2018-02-15) | three^3
+  RETURNING *, fpo_returning_row(for_portion_of_test::text);
+NOTICE:  RETURNING ("[3,4)","[2018-02-01,2018-02-15)",three^3)
+  id   |        valid_at         |  name   |              fpo_returning_row              
+-------+-------------------------+---------+---------------------------------------------
+ [3,4) | [2018-02-01,2018-02-15) | three^3 | ("[3,4)","[2018-02-01,2018-02-15)",three^3)
 (1 row)
 
 -- UPDATE ... RETURNING supports NEW and OLD valid_at
@@ -975,10 +984,11 @@ DELETE FROM for_portion_of_test WHERE id = '[99,100)';
 DELETE FROM for_portion_of_test
   FOR PORTION OF valid_at FROM '2018-02-02' TO '2018-02-03'
   WHERE id = '[3,4)'
-  RETURNING *;
-  id   |        valid_at         |  name   
--------+-------------------------+---------
- [3,4) | [2018-02-01,2018-02-10) | three^3
+  RETURNING *, fpo_returning_row(for_portion_of_test::text);
+NOTICE:  RETURNING ("[3,4)","[2018-02-01,2018-02-10)",three^3)
+  id   |        valid_at         |  name   |              fpo_returning_row              
+-------+-------------------------+---------+---------------------------------------------
+ [3,4) | [2018-02-01,2018-02-10) | three^3 | ("[3,4)","[2018-02-01,2018-02-10)",three^3)
 (1 row)
 
 -- DELETE FOR PORTION OF in a PL/pgSQL function
@@ -2137,7 +2147,14 @@ UPDATE temporal_partitioned FOR PORTION OF valid_at FROM '2000-03-01' TO '2000-0
 UPDATE temporal_partitioned FOR PORTION OF valid_at FROM '2000-06-01' TO '2000-07-01'
   SET name = 'one^2',
       id = '[4,5)'
-  WHERE id = '[1,2)';
+  WHERE id = '[1,2)'
+  RETURNING id, valid_at, name, fpo_returning_row(temporal_partitioned::text);
+NOTICE:  RETURNING ("[4,5)","[2000-06-01,2000-07-01)",one^2,30)
+  id   |        valid_at         | name  |              fpo_returning_row               
+-------+-------------------------+-------+----------------------------------------------
+ [4,5) | [2000-06-01,2000-07-01) | one^2 | ("[4,5)","[2000-06-01,2000-07-01)",one^2,30)
+(1 row)
+
 -- Move from partition 3 to partition 1
 UPDATE temporal_partitioned FOR PORTION OF valid_at FROM '2000-06-01' TO '2000-07-01'
   SET name = 'three^2',
@@ -2199,6 +2216,7 @@ SELECT * FROM temporal_partitioned_5 ORDER BY id, valid_at;
  five   | [2000-07-01,2010-01-01) | [5,6) |      3471
 (4 rows)
 
+DROP FUNCTION fpo_returning_row;
 DROP TABLE temporal_partitioned;
 -- UPDATE/DELETE FOR PORTION OF with RULEs
 CREATE TABLE fpo_rule (f1 bigint, f2 int4range);
index 5f7b04fdf6b40846a84c33168f684693fd75d088..955cf666d6613ec465e32fd211aaebd9f1984709 100644 (file)
@@ -590,11 +590,19 @@ SELECT * FROM for_portion_of_test ORDER BY id, valid_at;
 
 -- UPDATE ... RETURNING returns only the updated values
 -- (not the inserted side values, which are added by a separate "statement"):
+CREATE FUNCTION fpo_returning_row(text)
+RETURNS text LANGUAGE plpgsql AS
+$$
+BEGIN
+  RAISE NOTICE 'RETURNING %', $1;
+  RETURN $1;
+END;
+$$;
 UPDATE for_portion_of_test
   FOR PORTION OF valid_at FROM '2018-02-01' TO '2018-02-15'
   SET name = 'three^3'
   WHERE id = '[3,4)'
-  RETURNING *;
+  RETURNING *, fpo_returning_row(for_portion_of_test::text);
 
 -- UPDATE ... RETURNING supports NEW and OLD valid_at
 UPDATE for_portion_of_test
@@ -629,7 +637,7 @@ DELETE FROM for_portion_of_test WHERE id = '[99,100)';
 DELETE FROM for_portion_of_test
   FOR PORTION OF valid_at FROM '2018-02-02' TO '2018-02-03'
   WHERE id = '[3,4)'
-  RETURNING *;
+  RETURNING *, fpo_returning_row(for_portion_of_test::text);
 
 -- DELETE FOR PORTION OF in a PL/pgSQL function
 INSERT INTO for_portion_of_test (id, valid_at, name) VALUES
@@ -1439,7 +1447,8 @@ UPDATE temporal_partitioned FOR PORTION OF valid_at FROM '2000-03-01' TO '2000-0
 UPDATE temporal_partitioned FOR PORTION OF valid_at FROM '2000-06-01' TO '2000-07-01'
   SET name = 'one^2',
       id = '[4,5)'
-  WHERE id = '[1,2)';
+  WHERE id = '[1,2)'
+  RETURNING id, valid_at, name, fpo_returning_row(temporal_partitioned::text);
 
 -- Move from partition 3 to partition 1
 UPDATE temporal_partitioned FOR PORTION OF valid_at FROM '2000-06-01' TO '2000-07-01'
@@ -1460,6 +1469,7 @@ SELECT * FROM temporal_partitioned_1 ORDER BY id, valid_at;
 SELECT * FROM temporal_partitioned_3 ORDER BY id, valid_at;
 SELECT * FROM temporal_partitioned_5 ORDER BY id, valid_at;
 
+DROP FUNCTION fpo_returning_row;
 DROP TABLE temporal_partitioned;
 
 -- UPDATE/DELETE FOR PORTION OF with RULEs