From ff47a5356ee9490a36216f33c55c44bb6507d828 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <pj@illuminatedcomputing.com>
Date: Thu, 3 Sep 2026 12:54:34 -0700
Subject: [PATCH v1] Report the user's row, not a leftover, for FOR PORTION OF
 check options

If UPDATE FOR PORTION OF violates a view's CHECK OPTION *both* from a leftover
and from the top-level update, we show a message complaining about the leftover.
It would be clearer to complain about the top-level update.

This commit does the check before inserting leftovers, and then skips it in
ExecUpdateEpilogue.

Reported-by: Noah Misch <noah@leadboat.com>
Author: Paul A. Jungwirth <pj@illuminatedcomputing.com>
Backpatch-through: 19
---
 src/backend/executor/nodeModifyTable.c       | 16 ++++++-
 src/test/regress/expected/for_portion_of.out | 46 ++++++++++++++++++++
 src/test/regress/sql/for_portion_of.sql      | 40 +++++++++++++++++
 3 files changed, 101 insertions(+), 1 deletion(-)

diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 5681505d31c..bfe2259acd0 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -2629,7 +2629,18 @@ ExecUpdateEpilogue(ModifyTableContext *context, UpdateContext *updateCxt,
 
 	/* Compute temporal leftovers in FOR PORTION OF */
 	if (((ModifyTable *) context->mtstate->ps.plan)->forPortionOf)
+	{
+		/*
+		 * Enforce WITH CHECK OPTION on views early.  If an update violates the
+		 * condition *and* a temporal leftover violates it, we want to complain
+		 * about the top-level update.
+		 */
+		if (resultRelInfo->ri_WithCheckOptions != NIL)
+			ExecWithCheckOptions(WCO_VIEW_CHECK, resultRelInfo,
+								 slot, context->estate);
+
 		ExecForPortionOfLeftovers(context, context->estate, resultRelInfo, tupleid);
+	}
 
 	/* AFTER ROW UPDATE Triggers */
 	ExecARUpdateTriggers(context->estate, resultRelInfo,
@@ -2651,8 +2662,11 @@ ExecUpdateEpilogue(ModifyTableContext *context, UpdateContext *updateCxt,
 	 *
 	 * ExecWithCheckOptions() will skip any WCOs which are not of the kind we
 	 * are looking for at this point.
+	 *
+	 * With FOR PORTION OF, we already did this just above.
 	 */
-	if (resultRelInfo->ri_WithCheckOptions != NIL)
+	if (resultRelInfo->ri_WithCheckOptions != NIL &&
+		!((ModifyTable *) context->mtstate->ps.plan)->forPortionOf)
 		ExecWithCheckOptions(WCO_VIEW_CHECK, resultRelInfo,
 							 slot, context->estate);
 }
diff --git a/src/test/regress/expected/for_portion_of.out b/src/test/regress/expected/for_portion_of.out
index 64789d1777b..73e8fa166b0 100644
--- a/src/test/regress/expected/for_portion_of.out
+++ b/src/test/regress/expected/for_portion_of.out
@@ -2793,4 +2793,50 @@ SELECT * FROM fpo_rls ORDER BY valid_at;
 
 DROP TABLE fpo_rls;
 DROP ROLE regress_fpo_rls;
+--
+-- WITH CHECK OPTION on a view
+--
+-- The row the statement produced is checked first, so a violation names the
+-- row the user asked for and not an untouched leftover.
+--
+CREATE TABLE fpo_wco (
+  id int,
+  valid_at daterange,
+  name text
+);
+INSERT INTO fpo_wco VALUES
+  (1, daterange('2000-01-01', '2010-01-01'), 'keepme');
+CREATE VIEW fpo_wco_v AS
+  SELECT * FROM fpo_wco WHERE valid_at @> '2005-01-01'::date
+  WITH CHECK OPTION;
+-- Both the updated row and the leftovers fall outside the view.  The updated
+-- row is the one the user wrote, so that is the one to report.
+UPDATE fpo_wco_v FOR PORTION OF valid_at FROM '2002-01-01' TO '2003-01-01'
+  SET name = 'changed';
+ERROR:  new row violates check option for view "fpo_wco_v"
+DETAIL:  Failing row contains (1, [2002-01-01,2003-01-01), changed).
+-- Here the updated row still satisfies the view and only a leftover does not,
+-- so the leftover is correctly the one reported.
+UPDATE fpo_wco_v FOR PORTION OF valid_at FROM '2004-01-01' TO '2006-01-01'
+  SET name = 'changed';
+ERROR:  new row violates check option for view "fpo_wco_v"
+DETAIL:  Failing row contains (1, [2000-01-01,2004-01-01), keepme).
+-- Nothing was written by either statement.
+SELECT * FROM fpo_wco ORDER BY valid_at;
+ id |        valid_at         |  name  
+----+-------------------------+--------
+  1 | [2000-01-01,2010-01-01) | keepme
+(1 row)
+
+-- A portion covering the whole row leaves no leftovers and is accepted.
+UPDATE fpo_wco_v FOR PORTION OF valid_at FROM '2000-01-01' TO '2010-01-01'
+  SET name = 'changed';
+SELECT * FROM fpo_wco ORDER BY valid_at;
+ id |        valid_at         |  name   
+----+-------------------------+---------
+  1 | [2000-01-01,2010-01-01) | changed
+(1 row)
+
+DROP VIEW fpo_wco_v;
+DROP TABLE fpo_wco;
 RESET datestyle;
diff --git a/src/test/regress/sql/for_portion_of.sql b/src/test/regress/sql/for_portion_of.sql
index b61fe10478e..8c9f903c224 100644
--- a/src/test/regress/sql/for_portion_of.sql
+++ b/src/test/regress/sql/for_portion_of.sql
@@ -1849,4 +1849,44 @@ SELECT * FROM fpo_rls ORDER BY valid_at;
 DROP TABLE fpo_rls;
 DROP ROLE regress_fpo_rls;
 
+--
+-- WITH CHECK OPTION on a view
+--
+-- The row the statement produced is checked first, so a violation names the
+-- row the user asked for and not an untouched leftover.
+--
+
+CREATE TABLE fpo_wco (
+  id int,
+  valid_at daterange,
+  name text
+);
+INSERT INTO fpo_wco VALUES
+  (1, daterange('2000-01-01', '2010-01-01'), 'keepme');
+
+CREATE VIEW fpo_wco_v AS
+  SELECT * FROM fpo_wco WHERE valid_at @> '2005-01-01'::date
+  WITH CHECK OPTION;
+
+-- Both the updated row and the leftovers fall outside the view.  The updated
+-- row is the one the user wrote, so that is the one to report.
+UPDATE fpo_wco_v FOR PORTION OF valid_at FROM '2002-01-01' TO '2003-01-01'
+  SET name = 'changed';
+
+-- Here the updated row still satisfies the view and only a leftover does not,
+-- so the leftover is correctly the one reported.
+UPDATE fpo_wco_v FOR PORTION OF valid_at FROM '2004-01-01' TO '2006-01-01'
+  SET name = 'changed';
+
+-- Nothing was written by either statement.
+SELECT * FROM fpo_wco ORDER BY valid_at;
+
+-- A portion covering the whole row leaves no leftovers and is accepted.
+UPDATE fpo_wco_v FOR PORTION OF valid_at FROM '2000-01-01' TO '2010-01-01'
+  SET name = 'changed';
+SELECT * FROM fpo_wco ORDER BY valid_at;
+
+DROP VIEW fpo_wco_v;
+DROP TABLE fpo_wco;
+
 RESET datestyle;
-- 
2.47.3

