From 2064b015962932c30b23fa4d22f8e0f42839e755 Mon Sep 17 00:00:00 2001
From: Zsolt Parragi <zsolt.parragi@percona.com>
Date: Wed, 22 Jul 2026 07:30:51 +0000
Subject: [PATCH] Enforce WITH CHECK OPTION on DELETE FOR PORTION OF leftovers

DELETE FOR PORTION OF inserts temporal leftovers through ExecInsert(),
but the rewriter only attached view WCOs for INSERT/UPDATE.  Leftover
rows could therefore silently escape a WITH CHECK OPTION view, while
the equivalent UPDATE correctly raised an error.

This commit fixes it by attaching the WCOs for FOR PORTION OF deletes
too.
---
 src/backend/rewrite/rewriteHandler.c          |  7 ++-
 src/test/regress/expected/updatable_views.out | 48 +++++++++++++++++++
 src/test/regress/sql/updatable_views.sql      | 27 +++++++++++
 3 files changed, 81 insertions(+), 1 deletion(-)

diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index 38f54b57eec..1d24cc3ebc1 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -3953,8 +3953,13 @@ rewriteTargetView(Query *parsetree, Relation view)
 	 * the WITH CHECK OPTION, or any parent view specified WITH CASCADED CHECK
 	 * OPTION, add the quals from the view to the query's withCheckOptions
 	 * list.
+	 *
+	 * DELETE FOR PORTION OF needs this too: it inserts temporal leftovers to
+	 * preserve the untouched parts of the deleted row, and those must not
+	 * escape the view any more than an UPDATE's leftovers may.
 	 */
-	if (insert_or_update)
+	if (insert_or_update ||
+		(parsetree->commandType == CMD_DELETE && parsetree->forPortionOf != NULL))
 	{
 		bool		has_wco = RelationHasCheckOption(view);
 		bool		cascaded = RelationHasCascadedCheckOption(view);
diff --git a/src/test/regress/expected/updatable_views.out b/src/test/regress/expected/updatable_views.out
index 9c6bb2219f9..c5fc85df1b0 100644
--- a/src/test/regress/expected/updatable_views.out
+++ b/src/test/regress/expected/updatable_views.out
@@ -3768,6 +3768,54 @@ delete from uv_fpo_view_nonupd for portion of valid_at from 1 to 10;
 ERROR:  cannot delete from view "uv_fpo_view_nonupd" using FOR PORTION OF "valid_at"
 DETAIL:  View columns that are not columns of their base relation are not updatable.
 drop view uv_fpo_view_nonupd;
+-- WITH CHECK OPTION must be enforced on temporal leftovers, i.e. the rows
+-- FOR PORTION OF inserts to preserve the untouched parts of the target row.
+-- This applies to DELETE as well as UPDATE.
+create table uv_fpo_wco_tab (id int4range, valid_at daterange, b int);
+insert into uv_fpo_wco_tab values ('[1,1]', '[2020-01-01,2030-01-01)', 0);
+create view uv_fpo_wco_view as
+  select * from uv_fpo_wco_tab
+  where valid_at && daterange('2024-01-01', '2025-01-01')
+  with check option;
+-- The leftovers fall outside the view, so both commands fail:
+update uv_fpo_wco_view for portion of valid_at from '2024-01-01' to '2025-01-01' set b = 1;
+ERROR:  new row violates check option for view "uv_fpo_wco_view"
+DETAIL:  Failing row contains ([1,2), [01-01-2020,01-01-2024), 0).
+delete from uv_fpo_wco_view for portion of valid_at from '2024-01-01' to '2025-01-01';
+ERROR:  new row violates check option for view "uv_fpo_wco_view"
+DETAIL:  Failing row contains ([1,2), [01-01-2020,01-01-2024), 0).
+-- The base table is unchanged:
+select * from uv_fpo_wco_tab order by valid_at;
+  id   |        valid_at         | b 
+-------+-------------------------+---
+ [1,2) | [01-01-2020,01-01-2030) | 0
+(1 row)
+
+-- Leftovers that still satisfy the view are allowed:
+delete from uv_fpo_wco_view for portion of valid_at from '2024-03-01' to '2024-06-01';
+select * from uv_fpo_wco_tab order by valid_at;
+  id   |        valid_at         | b 
+-------+-------------------------+---
+ [1,2) | [01-01-2020,03-01-2024) | 0
+ [1,2) | [06-01-2024,01-01-2030) | 0
+(2 rows)
+
+-- Without WITH CHECK OPTION the leftovers may leave the view:
+create view uv_fpo_nowco_view as
+  select * from uv_fpo_wco_tab
+  where valid_at && daterange('2024-01-01', '2025-01-01');
+delete from uv_fpo_nowco_view for portion of valid_at from '2024-01-01' to '2024-02-01';
+select * from uv_fpo_wco_tab order by valid_at;
+  id   |        valid_at         | b 
+-------+-------------------------+---
+ [1,2) | [01-01-2020,01-01-2024) | 0
+ [1,2) | [02-01-2024,03-01-2024) | 0
+ [1,2) | [06-01-2024,01-01-2030) | 0
+(3 rows)
+
+drop view uv_fpo_wco_view;
+drop view uv_fpo_nowco_view;
+drop table uv_fpo_wco_tab;
 -- Test whole-row references to the view
 create table uv_iocu_tab (a int unique, b text);
 create view uv_iocu_view as
diff --git a/src/test/regress/sql/updatable_views.sql b/src/test/regress/sql/updatable_views.sql
index 2ef9aa32f36..b2c575675b8 100644
--- a/src/test/regress/sql/updatable_views.sql
+++ b/src/test/regress/sql/updatable_views.sql
@@ -1914,6 +1914,33 @@ update uv_fpo_view_nonupd for portion of valid_at from 1 to 10 set b = 2;
 delete from uv_fpo_view_nonupd for portion of valid_at from 1 to 10;
 drop view uv_fpo_view_nonupd;
 
+-- WITH CHECK OPTION must be enforced on temporal leftovers, i.e. the rows
+-- FOR PORTION OF inserts to preserve the untouched parts of the target row.
+-- This applies to DELETE as well as UPDATE.
+create table uv_fpo_wco_tab (id int4range, valid_at daterange, b int);
+insert into uv_fpo_wco_tab values ('[1,1]', '[2020-01-01,2030-01-01)', 0);
+create view uv_fpo_wco_view as
+  select * from uv_fpo_wco_tab
+  where valid_at && daterange('2024-01-01', '2025-01-01')
+  with check option;
+-- The leftovers fall outside the view, so both commands fail:
+update uv_fpo_wco_view for portion of valid_at from '2024-01-01' to '2025-01-01' set b = 1;
+delete from uv_fpo_wco_view for portion of valid_at from '2024-01-01' to '2025-01-01';
+-- The base table is unchanged:
+select * from uv_fpo_wco_tab order by valid_at;
+-- Leftovers that still satisfy the view are allowed:
+delete from uv_fpo_wco_view for portion of valid_at from '2024-03-01' to '2024-06-01';
+select * from uv_fpo_wco_tab order by valid_at;
+-- Without WITH CHECK OPTION the leftovers may leave the view:
+create view uv_fpo_nowco_view as
+  select * from uv_fpo_wco_tab
+  where valid_at && daterange('2024-01-01', '2025-01-01');
+delete from uv_fpo_nowco_view for portion of valid_at from '2024-01-01' to '2024-02-01';
+select * from uv_fpo_wco_tab order by valid_at;
+drop view uv_fpo_wco_view;
+drop view uv_fpo_nowco_view;
+drop table uv_fpo_wco_tab;
+
 -- Test whole-row references to the view
 create table uv_iocu_tab (a int unique, b text);
 create view uv_iocu_view as
-- 
2.54.0

