From 8a82a68e808600a38b8350f9d19794e0d0d3159d Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <pj@illuminatedcomputing.com>
Date: Sat, 9 May 2026 10:47:45 -0700
Subject: [PATCH v1] Fix FOR PORTION OF with non-updatable view columns

Both UPDATE and DELETE were failing to test that the application-time
column was updatable. The column is not part of perminfo->updatedCols,
because it should not be checked for permissions. And it needs to be
checked in the DELETE case as well, since we might insert leftovers with
a value for that column.

Discussion: https://www.postgresql.org/message-id/CACJufxFRqg8%3DgbZ-Q6ZS_UQ%2BYdwfZpk%2B9rf7jgWrk8m4RMUm%3DA%40mail.gmail.com
---
 src/backend/rewrite/rewriteHandler.c          | 47 +++++++++++++++++++
 src/test/regress/expected/updatable_views.out | 14 ++++++
 src/test/regress/sql/updatable_views.sql      | 11 +++++
 3 files changed, 72 insertions(+)

diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index 77b2c9bc622..e7ae9cce65f 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -3481,6 +3481,53 @@ rewriteTargetView(Query *parsetree, Relation view)
 		}
 	}
 
+	/*
+	 * Similarly, make sure the FOR PORTION OF column is updateable. This is
+	 * not included in the columns tested above, and we have to test it even
+	 * for DELETEs.
+	 */
+	if (parsetree->forPortionOf)
+	{
+		AttrNumber	rangeAttno;
+		Bitmapset  *fpo_cols;
+		char	   *non_updatable_col;
+		const char *fpo_update_detail;
+
+		rangeAttno = parsetree->forPortionOf->rangeVar->varattno;
+		fpo_cols = bms_make_singleton(rangeAttno - FirstLowInvalidHeapAttributeNumber);
+
+		fpo_update_detail = view_cols_are_auto_updatable(viewquery,
+														 fpo_cols,
+														 NULL,
+														 &non_updatable_col);
+		if (fpo_update_detail)
+		{
+			switch (parsetree->commandType)
+			{
+				case CMD_UPDATE:
+					ereport(ERROR,
+							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+							 errmsg("cannot update column \"%s\" of view \"%s\"",
+									non_updatable_col,
+									RelationGetRelationName(view)),
+							 errdetail_internal("%s", _(fpo_update_detail))));
+					break;
+				case CMD_DELETE:
+					ereport(ERROR,
+							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+							 errmsg("cannot delete from view \"%s\" using FOR PORTION OF \"%s\"",
+									RelationGetRelationName(view),
+									non_updatable_col),
+							 errdetail_internal("%s", _(fpo_update_detail))));
+					break;
+				default:
+					elog(ERROR, "unrecognized CmdType: %d",
+						 (int) parsetree->commandType);
+					break;
+			}
+		}
+	}
+
 	/*
 	 * For MERGE, there must not be any INSTEAD OF triggers on an otherwise
 	 * updatable view.  The caller already checked that there isn't a full set
diff --git a/src/test/regress/expected/updatable_views.out b/src/test/regress/expected/updatable_views.out
index 8852160718f..7b00c742776 100644
--- a/src/test/regress/expected/updatable_views.out
+++ b/src/test/regress/expected/updatable_views.out
@@ -3754,6 +3754,20 @@ select * from uv_fpo_view order by id, valid_at;
  0 | 1 | ["Sat Jan 01 00:00:00 2022","Tue Jan 01 00:00:00 2030") | [1,2) | 2.0
 (3 rows)
 
+-- UPDATE/DELETE FOR PORTION fails if the column is not updatable
+-- (e.g. a computed expression, not a base column):
+create view uv_fpo_view_nonupd as
+  select id, '[1,20]'::int4range as valid_at, b
+  from uv_fpo_tab;
+-- Updating fails:
+update uv_fpo_view_nonupd for portion of valid_at from 1 to 10 set b = 2;
+ERROR:  cannot update column "valid_at" of view "uv_fpo_view_nonupd"
+DETAIL:  View columns that are not columns of their base relation are not updatable.
+-- Deleting fails:
+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;
 -- 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 f7646999bd4..4a60126ec90 100644
--- a/src/test/regress/sql/updatable_views.sql
+++ b/src/test/regress/sql/updatable_views.sql
@@ -1903,6 +1903,17 @@ select * from uv_fpo_view order by id, valid_at;
 delete from uv_fpo_view for portion of valid_at from '2017-01-01' to '2022-01-01' where id = '[1,1]';
 select * from uv_fpo_view order by id, valid_at;
 
+-- UPDATE/DELETE FOR PORTION fails if the column is not updatable
+-- (e.g. a computed expression, not a base column):
+create view uv_fpo_view_nonupd as
+  select id, '[1,20]'::int4range as valid_at, b
+  from uv_fpo_tab;
+-- Updating fails:
+update uv_fpo_view_nonupd for portion of valid_at from 1 to 10 set b = 2;
+-- Deleting fails:
+delete from uv_fpo_view_nonupd for portion of valid_at from 1 to 10;
+drop view uv_fpo_view_nonupd;
+
 -- Test whole-row references to the view
 create table uv_iocu_tab (a int unique, b text);
 create view uv_iocu_view as
-- 
2.47.3

