]> git.ipfire.org Git - thirdparty/postgresql.git/commitdiff
Forbid generated columns in FOR PORTION OF
authorPeter Eisentraut <peter@eisentraut.org>
Mon, 6 Jul 2026 07:19:02 +0000 (09:19 +0200)
committerPeter Eisentraut <peter@eisentraut.org>
Mon, 6 Jul 2026 07:32:09 +0000 (09:32 +0200)
With virtual generated columns there is no column to assign to, and we
shouldn't assign directly to stored generated columns either.  (Once
we have PERIODs, we will allow a stored generated column here, but we
will assign to its start/end inputs.)

We can't do this in parse analysis, because views haven't yet been
rewritten, so they mask generated columns.

Author: Paul A. Jungwirth <pj@illuminatedcomputing.com>
Discussion: https://www.postgresql.org/message-id/agOOykf2HV26yVfU%40nathan

doc/src/sgml/ddl.sgml
src/backend/optimizer/plan/planner.c
src/test/regress/expected/for_portion_of.out
src/test/regress/sql/for_portion_of.sql

index 9d36a8b08f34f3968d4b57782320aec40eda3a87..2b08b54edf5786535742d8fc3f30cd760b6aeeb5 100644 (file)
@@ -382,7 +382,9 @@ CREATE TABLE people (
    A generated column cannot be written to directly.  In
    <command>INSERT</command> or <command>UPDATE</command> commands, a value
    cannot be specified for a generated column, but the keyword
-   <literal>DEFAULT</literal> may be specified.
+   <literal>DEFAULT</literal> may be specified.  Also, a generated column
+   cannot be used in the <literal>FOR PORTION OF</literal> clause of an
+   <command>UPDATE</command> or <command>DELETE</command> command.
   </para>
 
   <para>
index cac2ebb86c6f995de6769706228dfa5a2c76a479..3225185d16f8f2551034694cb91a5aa87263d91c 100644 (file)
@@ -848,6 +848,32 @@ subquery_planner(PlannerGlobal *glob, Query *parse, char *plan_name,
         */
        transform_MERGE_to_join(parse);
 
+       /*
+        * Reject FOR PORTION OF on a generated column.  We can't write to a
+        * virtual generated column, and a stored generated column should be
+        * written by its own expression.
+        *
+        * We do this in the planner rather than parse analysis so that updatable
+        * views have been rewritten; otherwise they would mask which columns are
+        * generated.  We need to check before preprocess_relation_rtes(), so that
+        * for virtual generated columns we still have the rangeVar.  After that
+        * it is replaced by the column's expression.
+        *
+        * 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 (parse->forPortionOf)
+       {
+               ForPortionOfExpr *forPortionOf = parse->forPortionOf;
+               RangeTblEntry *rte = rt_fetch(parse->resultRelation, parse->rtable);
+
+               if (get_attgenerated(rte->relid, forPortionOf->rangeVar->varattno))
+                       ereport(ERROR,
+                                       (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+                                        errmsg("cannot use generated column \"%s\" in FOR PORTION OF",
+                                                       forPortionOf->range_name)));
+       }
+
        /*
         * Scan the rangetable for relation RTEs and retrieve the necessary
         * catalog information for each relation.  Using this information, clear
index 207e370627e2daee8c6bee2e12015a388216cd4d..a050fc2dadf0c0c1a99e80468d682b6c1f0262f3 100644 (file)
@@ -2208,6 +2208,79 @@ 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;
+-- FOR PORTION OF a generated column reached through an updatable view.
+-- The view hides that b is generated during parse analysis, so the check
+-- must happen later (in the planner), after the view is rewritten to its
+-- underlying table.
+CREATE TABLE fpo_gen_view (
+  a int,
+  b int4range GENERATED ALWAYS AS (int4range(a, a + 1)) STORED
+);
+INSERT INTO fpo_gen_view VALUES (1);
+CREATE VIEW fpo_gen_view_v AS SELECT * FROM fpo_gen_view;
+DELETE FROM fpo_gen_view_v FOR PORTION OF b FROM 1 TO 2; -- fails
+ERROR:  cannot use generated column "b" in FOR PORTION OF
+UPDATE fpo_gen_view_v 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 VIEW fpo_gen_view_v;
+DROP TABLE fpo_gen_view;
+-- A new-style SQL function is parsed at CREATE FUNCTION time, but our
+-- generated-column check is in the planner, so it sees the column's
+-- current attgenerated when the function's plan is built at run time.
+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 (
index a3c41abf7b7dde308345bd151cbf8eacc21dace7..a1ee1d5e50147324c9684b1e49bf9e61c0b24553 100644 (file)
@@ -1448,6 +1448,63 @@ 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;
+
+-- FOR PORTION OF a generated column reached through an updatable view.
+-- The view hides that b is generated during parse analysis, so the check
+-- must happen later (in the planner), after the view is rewritten to its
+-- underlying table.
+CREATE TABLE fpo_gen_view (
+  a int,
+  b int4range GENERATED ALWAYS AS (int4range(a, a + 1)) STORED
+);
+INSERT INTO fpo_gen_view VALUES (1);
+CREATE VIEW fpo_gen_view_v AS SELECT * FROM fpo_gen_view;
+DELETE FROM fpo_gen_view_v FOR PORTION OF b FROM 1 TO 2; -- fails
+UPDATE fpo_gen_view_v FOR PORTION OF b FROM 1 TO 2 SET a = 5; -- fails
+DROP VIEW fpo_gen_view_v;
+DROP TABLE fpo_gen_view;
+
+-- A new-style SQL function is parsed at CREATE FUNCTION time, but our
+-- generated-column check is in the planner, so it sees the column's
+-- current attgenerated when the function's plan is built at run time.
+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 (