From 45415b5ec7ab1ffb3f3b9ccc2de0239eab4e7eba Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <pj@illuminatedcomputing.com>
Date: Thu, 14 May 2026 09:56:24 -0700
Subject: [PATCH v3] Forbid GENERATED columns in FOR PORTION OF

With VIRTUAL columns there is no column to assign to, and we shouldn't
assign directly to STORED columns either. (Once we have PERIODs, we will
allow a STORED column here, but we will assign to its start/end inputs.)

It is important to do this check after parse analysis, so the column
can't change after we validate it. For one thing, a BEGIN ATOMIC SQL
function parses the statement far in advance of executing it.

Discussion: https://www.postgresql.org/message-id/agOOykf2HV26yVfU%40nathan
---
 src/backend/executor/nodeModifyTable.c       | 17 ++++++
 src/test/regress/expected/for_portion_of.out | 56 ++++++++++++++++++++
 src/test/regress/sql/for_portion_of.sql      | 42 +++++++++++++++
 3 files changed, 115 insertions(+)

diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 33a6735f08d..1e39ba0aada 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -5584,6 +5584,23 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 		tupDesc = rootRelInfo->ri_RelationDesc->rd_att;
 		forPortionOf = (ForPortionOfExpr *) node->forPortionOf;
 
+		/*
+		 * Reject generated columns. We can't write to a virtual generated column,
+		 * and a stored generated column should be written by its own expression.
+		 *
+		 * For virtual columns, forPortionOf->rangeVar gets replaced by the planner,
+		 * so it's not actually a Var anymore!
+		 *
+		 * XXX: We plan to implement PERIODs as stored generated columns, so later
+		 * we will loosen this restriction if the column belongs to a PERIOD.
+		 */
+		if (!IsA(forPortionOf->rangeVar, Var) ||
+			TupleDescAttr(tupDesc, forPortionOf->rangeVar->varattno - 1)->attgenerated)
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("cannot use generated column \"%s\" in FOR PORTION OF",
+							forPortionOf->range_name)));
+
 		/* Eval the FOR PORTION OF target */
 		if (mtstate->ps.ps_ExprContext == NULL)
 			ExecAssignExprContext(estate, &mtstate->ps);
diff --git a/src/test/regress/expected/for_portion_of.out b/src/test/regress/expected/for_portion_of.out
index 43408972117..c75b3b51489 100644
--- a/src/test/regress/expected/for_portion_of.out
+++ b/src/test/regress/expected/for_portion_of.out
@@ -2208,6 +2208,62 @@ SELECT * FROM fpo_rule ORDER BY f1;
 (2 rows)
 
 DROP TABLE fpo_rule;
+-- UPDATE/DELETE FOR PORTION OF on a GENERATED VIRTUAL range column:
+CREATE TABLE fpo_gen_virtual (
+  a int,
+  b int4range GENERATED ALWAYS AS (int4range(a, a + 1)) VIRTUAL
+);
+INSERT INTO fpo_gen_virtual VALUES (1);
+DELETE FROM fpo_gen_virtual FOR PORTION OF b FROM 1 TO 2; -- fails
+ERROR:  cannot use generated column "b" in FOR PORTION OF
+UPDATE fpo_gen_virtual FOR PORTION OF b FROM 1 TO 2 SET a = 5; -- fails
+ERROR:  column "b" can only be updated to DEFAULT
+DETAIL:  Column "b" is a generated column.
+DROP TABLE fpo_gen_virtual;
+-- UPDATE/DELETE FOR PORTION OF on a GENERATED STORED range column:
+CREATE TABLE fpo_gen_stored (
+  a int,
+  b int4range GENERATED ALWAYS AS (int4range(a, a + 1)) STORED
+);
+INSERT INTO fpo_gen_stored VALUES (1);
+DELETE FROM fpo_gen_stored FOR PORTION OF b FROM 1 TO 2; -- fails
+ERROR:  cannot use generated column "b" in FOR PORTION OF
+UPDATE fpo_gen_stored FOR PORTION OF b FROM 1 TO 2 SET a = 5; -- fails
+ERROR:  column "b" can only be updated to DEFAULT
+DETAIL:  Column "b" is a generated column.
+DROP TABLE fpo_gen_stored;
+-- A new-style SQL function is parsed at CREATE FUNCTION time, but our
+-- generated-column check is in the executor, so it sees the column's
+-- current attgenerated when the function actually runs.
+CREATE TABLE fpo_func_test (
+  a int,
+  b int4range GENERATED ALWAYS AS (int4range(a, a + 1)) STORED
+);
+INSERT INTO fpo_func_test VALUES (1);
+-- Definition succeeds even though b is a generated column today.
+CREATE FUNCTION fpo_delete() RETURNS void
+  LANGUAGE SQL
+  BEGIN ATOMIC
+    DELETE FROM fpo_func_test FOR PORTION OF b FROM 1 TO 2;
+  END;
+SELECT fpo_delete(); -- fails: b is generated
+ERROR:  cannot use generated column "b" in FOR PORTION OF
+CONTEXT:  SQL function "fpo_delete" statement 1
+-- Drop the generation expression and the same function now succeeds.
+ALTER TABLE fpo_func_test ALTER COLUMN b DROP EXPRESSION;
+SELECT fpo_delete();
+ fpo_delete 
+------------
+ 
+(1 row)
+
+TABLE fpo_func_test ORDER BY a, b;
+ a | b 
+---+---
+(0 rows)
+
+DROP FUNCTION fpo_delete();
+DROP TABLE fpo_func_test;
 -- UPDATE/DELETE FOR PORTION OF with table inheritance
 -- Leftover rows must stay in the child table, preserving child-specific columns.
 CREATE TABLE fpo_inh_parent (
diff --git a/src/test/regress/sql/for_portion_of.sql b/src/test/regress/sql/for_portion_of.sql
index 7b08f8cf45e..a3d44cf8166 100644
--- a/src/test/regress/sql/for_portion_of.sql
+++ b/src/test/regress/sql/for_portion_of.sql
@@ -1448,6 +1448,48 @@ SELECT * FROM fpo_rule ORDER BY f1;
 
 DROP TABLE fpo_rule;
 
+-- UPDATE/DELETE FOR PORTION OF on a GENERATED VIRTUAL range column:
+CREATE TABLE fpo_gen_virtual (
+  a int,
+  b int4range GENERATED ALWAYS AS (int4range(a, a + 1)) VIRTUAL
+);
+INSERT INTO fpo_gen_virtual VALUES (1);
+DELETE FROM fpo_gen_virtual FOR PORTION OF b FROM 1 TO 2; -- fails
+UPDATE fpo_gen_virtual FOR PORTION OF b FROM 1 TO 2 SET a = 5; -- fails
+DROP TABLE fpo_gen_virtual;
+
+-- UPDATE/DELETE FOR PORTION OF on a GENERATED STORED range column:
+CREATE TABLE fpo_gen_stored (
+  a int,
+  b int4range GENERATED ALWAYS AS (int4range(a, a + 1)) STORED
+);
+INSERT INTO fpo_gen_stored VALUES (1);
+DELETE FROM fpo_gen_stored FOR PORTION OF b FROM 1 TO 2; -- fails
+UPDATE fpo_gen_stored FOR PORTION OF b FROM 1 TO 2 SET a = 5; -- fails
+DROP TABLE fpo_gen_stored;
+
+-- A new-style SQL function is parsed at CREATE FUNCTION time, but our
+-- generated-column check is in the executor, so it sees the column's
+-- current attgenerated when the function actually runs.
+CREATE TABLE fpo_func_test (
+  a int,
+  b int4range GENERATED ALWAYS AS (int4range(a, a + 1)) STORED
+);
+INSERT INTO fpo_func_test VALUES (1);
+-- Definition succeeds even though b is a generated column today.
+CREATE FUNCTION fpo_delete() RETURNS void
+  LANGUAGE SQL
+  BEGIN ATOMIC
+    DELETE FROM fpo_func_test FOR PORTION OF b FROM 1 TO 2;
+  END;
+SELECT fpo_delete(); -- fails: b is generated
+-- Drop the generation expression and the same function now succeeds.
+ALTER TABLE fpo_func_test ALTER COLUMN b DROP EXPRESSION;
+SELECT fpo_delete();
+TABLE fpo_func_test ORDER BY a, b;
+DROP FUNCTION fpo_delete();
+DROP TABLE fpo_func_test;
+
 -- UPDATE/DELETE FOR PORTION OF with table inheritance
 -- Leftover rows must stay in the child table, preserving child-specific columns.
 CREATE TABLE fpo_inh_parent (
-- 
2.47.3

