From f37b8d41c211d8f05b3fe7b96a0bf618890e7170 Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
Date: Tue, 14 Apr 2026 12:10:09 +0800
Subject: [PATCH v10 1/1] reject instead of view with DELETE/UPDATE FOR PORTION
 OF

Views with INSTEAD OF triggers cannot rewrite the query to the base relation.
Views do not have physical rows.  FOR PORTION OF requires access to the physical
row to compute temporal leftovers, so we must disallow it here.

discussion: https://postgr.es/m/CAHg%2BQDd74fnd4obCRMqVS0AVWf%3DcSFH%3DCv7trTJWgm%2B_bhTK6w%40mail.gmail.com
commitfest entry: https://commitfest.postgresql.org/patch/
---
 src/backend/rewrite/rewriteHandler.c         | 28 ++++++++++++++++++++
 src/test/regress/expected/for_portion_of.out | 21 +++++++++++++++
 src/test/regress/sql/for_portion_of.sql      | 23 ++++++++++++++++
 3 files changed, 72 insertions(+)

diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index 021c73f1b67..dbfbfcaf34a 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -4166,6 +4166,20 @@ RewriteQuery(Query *parsetree, List *rewrite_events, int orig_rt_length,
 						parsetree->targetList = lappend(parsetree->targetList, tle);
 					}
 				}
+				else if (view_has_instead_trigger(rt_entry_relation, event, NIL))
+				{
+					/*
+					 * Views with INSTEAD OF triggers cannot rewrite the query
+					 * to the base relation. Views do not have physical rows.
+					 * FOR PORTION OF requires access to the physical row to
+					 * compute temporal leftovers, so we must disallow it
+					 * here.
+					 */
+					if (parsetree->forPortionOf)
+						ereport(ERROR,
+								errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+								errmsg("UPDATE FOR PORTION OF is not supported for views with INSTEAD OF triggers"));
+				}
 			}
 
 			parsetree->targetList =
@@ -4231,6 +4245,20 @@ RewriteQuery(Query *parsetree, List *rewrite_events, int orig_rt_length,
 					 */
 					AddQual(parsetree, parsetree->forPortionOf->overlapsExpr);
 				}
+				else if (view_has_instead_trigger(rt_entry_relation, event, NIL))
+				{
+					/*
+					 * Views with INSTEAD OF triggers cannot rewrite the query
+					 * to the base relation. Views do not have physical rows.
+					 * FOR PORTION OF requires access to the physical row to
+					 * compute temporal leftovers, so we must disallow it
+					 * here.
+					 */
+					if (parsetree->forPortionOf)
+						ereport(ERROR,
+								errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+								errmsg("DELETE FOR PORTION OF is not supported for views with INSTEAD OF triggers"));
+				}
 			}
 		}
 		else
diff --git a/src/test/regress/expected/for_portion_of.out b/src/test/regress/expected/for_portion_of.out
index 31f772c723d..9e3c337e0e0 100644
--- a/src/test/regress/expected/for_portion_of.out
+++ b/src/test/regress/expected/for_portion_of.out
@@ -2097,4 +2097,25 @@ SELECT * FROM temporal_partitioned_5 ORDER BY id, valid_at;
 (4 rows)
 
 DROP TABLE temporal_partitioned;
+-- Test: FOR PORTION OF should be rejected on views with INSTEAD OF triggers
+CREATE TABLE fpo_instead_base (id int, valid_at daterange, val int);
+INSERT INTO fpo_instead_base VALUES (1, '[2024-01-01,2024-12-31)', 100);
+CREATE VIEW fpo_instead_view AS SELECT * FROM fpo_instead_base;
+CREATE FUNCTION fpo_instead_trig_fn() RETURNS trigger LANGUAGE plpgsql AS $$
+BEGIN
+    RETURN NEW;
+END;
+$$;
+CREATE TRIGGER fpo_instead_trig INSTEAD OF UPDATE ON fpo_instead_view
+  FOR EACH ROW EXECUTE FUNCTION fpo_instead_trig_fn();
+CREATE TRIGGER fpo_instead_del_trig INSTEAD OF DELETE ON fpo_instead_view
+  FOR EACH ROW EXECUTE FUNCTION fpo_instead_trig_fn();
+UPDATE fpo_instead_view FOR PORTION OF valid_at FROM '2024-04-01' TO '2024-08-01'
+    SET val = 999 WHERE id = 1;  -- error
+ERROR:  UPDATE FOR PORTION OF is not supported for views with INSTEAD OF triggers
+DELETE FROM fpo_instead_view FOR PORTION OF valid_at FROM '2024-04-01' TO '2024-08-01'
+    WHERE id = 1;  -- error
+ERROR:  DELETE FOR PORTION OF is not supported for views with INSTEAD OF triggers
+DROP VIEW fpo_instead_view;
+DROP TABLE fpo_instead_base;
 RESET datestyle;
diff --git a/src/test/regress/sql/for_portion_of.sql b/src/test/regress/sql/for_portion_of.sql
index d4062acf1d1..d4602171c90 100644
--- a/src/test/regress/sql/for_portion_of.sql
+++ b/src/test/regress/sql/for_portion_of.sql
@@ -1365,4 +1365,27 @@ SELECT * FROM temporal_partitioned_5 ORDER BY id, valid_at;
 
 DROP TABLE temporal_partitioned;
 
+-- Test: FOR PORTION OF should be rejected on views with INSTEAD OF triggers
+CREATE TABLE fpo_instead_base (id int, valid_at daterange, val int);
+INSERT INTO fpo_instead_base VALUES (1, '[2024-01-01,2024-12-31)', 100);
+CREATE VIEW fpo_instead_view AS SELECT * FROM fpo_instead_base;
+CREATE FUNCTION fpo_instead_trig_fn() RETURNS trigger LANGUAGE plpgsql AS $$
+BEGIN
+    RETURN NEW;
+END;
+$$;
+CREATE TRIGGER fpo_instead_trig INSTEAD OF UPDATE ON fpo_instead_view
+  FOR EACH ROW EXECUTE FUNCTION fpo_instead_trig_fn();
+CREATE TRIGGER fpo_instead_del_trig INSTEAD OF DELETE ON fpo_instead_view
+  FOR EACH ROW EXECUTE FUNCTION fpo_instead_trig_fn();
+
+UPDATE fpo_instead_view FOR PORTION OF valid_at FROM '2024-04-01' TO '2024-08-01'
+    SET val = 999 WHERE id = 1;  -- error
+
+DELETE FROM fpo_instead_view FOR PORTION OF valid_at FROM '2024-04-01' TO '2024-08-01'
+    WHERE id = 1;  -- error
+
+DROP VIEW fpo_instead_view;
+DROP TABLE fpo_instead_base;
+
 RESET datestyle;
-- 
2.34.1

