0001-Consistent-trigger-behavior-between-multiple-tempora.patch
application/octet-stream
Filename: 0001-Consistent-trigger-behavior-between-multiple-tempora.patch
Type: application/octet-stream
Part: 0
Patch
Same data as JSON:
GET /api/v1/attachments/:id/patch
the parsed metadata as JSON — format, series position, per-file stats; never the diff bytes.
API reference →
Format: format-patch
Series: patch 0001
Subject: Consistent trigger behavior between multiple temporal leftovers
| File | + | − |
|---|---|---|
| src/backend/executor/nodeModifyTable.c | 13 | 0 |
| src/test/regress/expected/for_portion_of.out | 37 | 0 |
| src/test/regress/sql/for_portion_of.sql | 37 | 0 |
From 19d9e8114e7439e7c784a72d64dc07bb85ae2415 Mon Sep 17 00:00:00 2001
From: Sergei Patiakin <sergei.patiakin@enterprisedb.com>
Date: Wed, 8 Apr 2026 14:46:09 +0200
Subject: [PATCH] Consistent trigger behavior between multiple temporal
leftovers
Avoid leaking modifications from one temporal leftover's
BEFORE INSERT ROW trigger to another temporal leftover.
---
src/backend/executor/nodeModifyTable.c | 13 +++++++
src/test/regress/expected/for_portion_of.out | 37 ++++++++++++++++++++
src/test/regress/sql/for_portion_of.sql | 37 ++++++++++++++++++++
3 files changed, 87 insertions(+)
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index ef2a6bc6e9d..7d77d0a511c 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -1584,6 +1584,19 @@ ExecForPortionOfLeftovers(ModifyTableContext *context,
didInit = true;
}
+ else
+ {
+ /*
+ * Re-copy the original row into leftoverSlot because
+ * ExecInsert may pass leftoverSlot to BEFORE INSERT
+ * triggers, which can modify the slot contents.
+ */
+ if (map != NULL)
+ execute_attr_map_slot(map->attrMap, oldtupleSlot,
+ leftoverSlot);
+ else
+ ExecForceStoreHeapTuple(oldtuple, leftoverSlot, false);
+ }
leftoverSlot->tts_values[forPortionOf->rangeVar->varattno - 1] = leftover;
leftoverSlot->tts_isnull[forPortionOf->rangeVar->varattno - 1] = false;
diff --git a/src/test/regress/expected/for_portion_of.out b/src/test/regress/expected/for_portion_of.out
index 31f772c723d..0d21944181c 100644
--- a/src/test/regress/expected/for_portion_of.out
+++ b/src/test/regress/expected/for_portion_of.out
@@ -1793,6 +1793,43 @@ SELECT * FROM for_portion_of_test WHERE id = '[4,5)' ORDER BY id, valid_at;
(3 rows)
DROP TRIGGER fpo_after_delete_row ON for_portion_of_test;
+-- Test that a tuple-modifying BEFORE INSERT ROW trigger acts
+-- consistently on both temporal leftovers.
+-- When FOR PORTION OF splits a row into two leftovers,
+-- both triggers should get the original row's values.
+DROP TABLE for_portion_of_test;
+CREATE TABLE for_portion_of_test (
+ id int,
+ valid_at daterange,
+ name text
+);
+CREATE FUNCTION fpo_append_name_suffix()
+RETURNS TRIGGER LANGUAGE plpgsql AS
+$$
+BEGIN
+ NEW.name := NEW.name || '+insert';
+ RETURN NEW;
+END;
+$$;
+CREATE TRIGGER fpo_before_insert_row
+ BEFORE INSERT ON for_portion_of_test
+ FOR EACH ROW EXECUTE PROCEDURE fpo_append_name_suffix();
+INSERT INTO for_portion_of_test VALUES (1, '[2020-01-01,2020-12-31)', 'foo');
+UPDATE for_portion_of_test
+ FOR PORTION OF valid_at FROM '2020-04-01' TO '2020-08-01'
+ SET name = 'bar'
+ WHERE id = 1;
+-- Both leftovers should have the same name: 'foo+insert+insert'.
+SELECT * FROM for_portion_of_test ORDER BY valid_at;
+ id | valid_at | name
+----+-------------------------+-------------------
+ 1 | [2020-01-01,2020-04-01) | foo+insert+insert
+ 1 | [2020-04-01,2020-08-01) | bar
+ 1 | [2020-08-01,2020-12-31) | foo+insert+insert
+(3 rows)
+
+DROP FUNCTION fpo_append_name_suffix CASCADE;
+NOTICE: drop cascades to trigger fpo_before_insert_row on table for_portion_of_test
-- Test with multiranges
CREATE TABLE for_portion_of_test2 (
id int4range NOT NULL,
diff --git a/src/test/regress/sql/for_portion_of.sql b/src/test/regress/sql/for_portion_of.sql
index d4062acf1d1..a7dec60e364 100644
--- a/src/test/regress/sql/for_portion_of.sql
+++ b/src/test/regress/sql/for_portion_of.sql
@@ -1169,6 +1169,43 @@ SELECT * FROM for_portion_of_test WHERE id = '[4,5)' ORDER BY id, valid_at;
DROP TRIGGER fpo_after_delete_row ON for_portion_of_test;
+-- Test that a tuple-modifying BEFORE INSERT ROW trigger acts
+-- consistently on both temporal leftovers.
+-- When FOR PORTION OF splits a row into two leftovers,
+-- both triggers should get the original row's values.
+
+DROP TABLE for_portion_of_test;
+CREATE TABLE for_portion_of_test (
+ id int,
+ valid_at daterange,
+ name text
+);
+
+CREATE FUNCTION fpo_append_name_suffix()
+RETURNS TRIGGER LANGUAGE plpgsql AS
+$$
+BEGIN
+ NEW.name := NEW.name || '+insert';
+ RETURN NEW;
+END;
+$$;
+
+CREATE TRIGGER fpo_before_insert_row
+ BEFORE INSERT ON for_portion_of_test
+ FOR EACH ROW EXECUTE PROCEDURE fpo_append_name_suffix();
+
+INSERT INTO for_portion_of_test VALUES (1, '[2020-01-01,2020-12-31)', 'foo');
+
+UPDATE for_portion_of_test
+ FOR PORTION OF valid_at FROM '2020-04-01' TO '2020-08-01'
+ SET name = 'bar'
+ WHERE id = 1;
+
+-- Both leftovers should have the same name: 'foo+insert+insert'.
+SELECT * FROM for_portion_of_test ORDER BY valid_at;
+
+DROP FUNCTION fpo_append_name_suffix CASCADE;
+
-- Test with multiranges
CREATE TABLE for_portion_of_test2 (
--
2.47.3