]> git.ipfire.org Git - thirdparty/postgresql.git/commitdiff
Allow ALTER COLUMN SET EXPRESSION on virtual columns with CHECK constraints
authorPeter Eisentraut <peter@eisentraut.org>
Tue, 24 Feb 2026 09:30:50 +0000 (10:30 +0100)
committerPeter Eisentraut <peter@eisentraut.org>
Tue, 24 Feb 2026 09:32:05 +0000 (10:32 +0100)
Previously, changing the generation expression of a virtual column was
prohibited if the column was referenced by a CHECK constraint.  This
lifts that restriction.

RememberAllDependentForRebuilding within ATExecSetExpression will
rebuild all the dependent constraints, later ATPostAlterTypeCleanup
queues the required AlterTableStmt operations for ALTER TABLE Phase 3
execution.

Overall, ALTER COLUMN SET EXPRESSION on virtual columns may require
scanning the table to re-verify any associated CHECK constraints, but
it does not require a table rewrite in ALTER TABLE Phase 3.

Author: jian he <jian.universality@gmail.com>
Reviewed-by: Matheus Alcantara <matheusssilv97@gmail.com>
Discussion: https://postgr.es/m/CACJufxH3VETr7orF5rW29GnDk3n1wWbOE3WdkHYd3iPGrQ9E_A@mail.gmail.com

doc/src/sgml/ref/alter_table.sgml
src/backend/commands/tablecmds.c
src/test/regress/expected/generated_virtual.out
src/test/regress/sql/generated_virtual.sql

index 1bd479c917a433979d909d30602ef1006cf43e99..aab2c6eb19ff168e0448200aa7ceed1bd0cd2565 100644 (file)
@@ -278,6 +278,9 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
       This form replaces the expression of a generated column.  Existing data
       in a stored generated column is rewritten and all the future changes
       will apply the new generation expression.
+      Replacing the expression of a virtual generated column do not require a
+      table rewrite, but if the column is used in a constraint, the table will
+      be scanned to check that existing rows meet the constraint.
      </para>
 
      <para>
index df1ba112b35bc3bfdd514c655320230bd77ef073..b04b0dbd2a0664b964104e4f30ed290e1319d3de 100644 (file)
@@ -8675,18 +8675,6 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
                                 errmsg("column \"%s\" of relation \"%s\" is not a generated column",
                                                colName, RelationGetRelationName(rel))));
 
-       /*
-        * TODO: This could be done, just need to recheck any constraints
-        * afterwards.
-        */
-       if (attgenerated == ATTRIBUTE_GENERATED_VIRTUAL &&
-               rel->rd_att->constr && rel->rd_att->constr->num_check > 0)
-               ereport(ERROR,
-                               (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-                                errmsg("ALTER TABLE / SET EXPRESSION is not supported for virtual generated columns in tables with check constraints"),
-                                errdetail("Column \"%s\" of relation \"%s\" is a virtual generated column.",
-                                                  colName, RelationGetRelationName(rel))));
-
        if (attgenerated == ATTRIBUTE_GENERATED_VIRTUAL && attTup->attnotnull)
                tab->verify_new_notnull = true;
 
@@ -8719,15 +8707,14 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
 
                /* make sure we don't conflict with later attribute modifications */
                CommandCounterIncrement();
-
-               /*
-                * Find everything that depends on the column (constraints, indexes,
-                * etc), and record enough information to let us recreate the objects
-                * after rewrite.
-                */
-               RememberAllDependentForRebuilding(tab, AT_SetExpression, rel, attnum, colName);
        }
 
+       /*
+        * Find everything that depends on the column (constraints, indexes, etc),
+        * and record enough information to let us recreate the objects.
+        */
+       RememberAllDependentForRebuilding(tab, AT_SetExpression, rel, attnum, colName);
+
        /*
         * Drop the dependency records of the GENERATED expression, in particular
         * its INTERNAL dependency on the column, which would otherwise cause
index 249e68be654d79e5d635423a8798e330b1c2b9c9..6dab60c937b56691ea38c40090d38d00bf4b983f 100644 (file)
@@ -639,12 +639,30 @@ INSERT INTO gtest20 (a) VALUES (10);  -- ok
 INSERT INTO gtest20 (a) VALUES (30);  -- violates constraint
 ERROR:  new row for relation "gtest20" violates check constraint "gtest20_b_check"
 DETAIL:  Failing row contains (30, virtual).
-ALTER TABLE gtest20 ALTER COLUMN b SET EXPRESSION AS (a * 100);  -- violates constraint (currently not supported)
-ERROR:  ALTER TABLE / SET EXPRESSION is not supported for virtual generated columns in tables with check constraints
-DETAIL:  Column "b" of relation "gtest20" is a virtual generated column.
-ALTER TABLE gtest20 ALTER COLUMN b SET EXPRESSION AS (a * 3);  -- ok (currently not supported)
-ERROR:  ALTER TABLE / SET EXPRESSION is not supported for virtual generated columns in tables with check constraints
-DETAIL:  Column "b" of relation "gtest20" is a virtual generated column.
+ALTER TABLE gtest20 ALTER COLUMN b SET EXPRESSION AS (a * 100);  -- violates constraint
+ERROR:  check constraint "gtest20_b_check" of relation "gtest20" is violated by some row
+ALTER TABLE gtest20 ALTER COLUMN b SET EXPRESSION AS (a * 3);  -- ok
+-- table rewrite should not happen
+SELECT pg_relation_filenode('gtest20') AS gtest20_filenode \gset
+ALTER TABLE gtest20 ALTER COLUMN b SET EXPRESSION AS (a * 4), ADD COLUMN c INT DEFAULT 11;
+SELECT pg_relation_filenode('gtest20') = :gtest20_filenode AS is_same_file;
+ is_same_file 
+--------------
+ t
+(1 row)
+
+\d gtest20
+                Table "generated_virtual_tests.gtest20"
+ Column |  Type   | Collation | Nullable |           Default           
+--------+---------+-----------+----------+-----------------------------
+ a      | integer |           | not null | 
+ b      | integer |           |          | generated always as (a * 4)
+ c      | integer |           |          | 11
+Indexes:
+    "gtest20_pkey" PRIMARY KEY, btree (a)
+Check constraints:
+    "gtest20_b_check" CHECK (b < 50)
+
 CREATE TABLE gtest20a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) VIRTUAL);
 INSERT INTO gtest20a (a) VALUES (10);
 INSERT INTO gtest20a (a) VALUES (30);
@@ -988,6 +1006,15 @@ SELECT tableoid::regclass, * FROM gtest_parent ORDER BY 1, 2, 3;
  gtest_child3 | 09-13-2016 |  1 |  4
 (3 rows)
 
+-- check constraint was validated based on each partitions's generation expression
+ALTER TABLE gtest_parent ADD CONSTRAINT cc1 CHECK (f3 < 19); -- error
+ERROR:  check constraint "cc1" of relation "gtest_child" is violated by some row
+ALTER TABLE gtest_parent ADD CONSTRAINT cc1 CHECK (f3 < 66); -- error
+ERROR:  check constraint "cc1" of relation "gtest_child2" is violated by some row
+ALTER TABLE gtest_parent ADD CONSTRAINT cc1 CHECK (f3 <> 33); -- error
+ERROR:  check constraint "cc1" of relation "gtest_child3" is violated by some row
+ALTER TABLE gtest_parent ADD CONSTRAINT cc CHECK (f3 < 67); -- ok
+ALTER TABLE gtest_parent DROP CONSTRAINT cc;
 -- alter generation expression of parent and all its children altogether
 ALTER TABLE gtest_parent ALTER COLUMN f3 SET EXPRESSION AS (f2 * 2);
 \d gtest_parent
index 81152b39a7986993dea1177d597fc2c89cdda02e..e750866d2d82e45c977483aedc72b2d5a15415f0 100644 (file)
@@ -317,8 +317,13 @@ CREATE TABLE gtest20 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) VIRTU
 INSERT INTO gtest20 (a) VALUES (10);  -- ok
 INSERT INTO gtest20 (a) VALUES (30);  -- violates constraint
 
-ALTER TABLE gtest20 ALTER COLUMN b SET EXPRESSION AS (a * 100);  -- violates constraint (currently not supported)
-ALTER TABLE gtest20 ALTER COLUMN b SET EXPRESSION AS (a * 3);  -- ok (currently not supported)
+ALTER TABLE gtest20 ALTER COLUMN b SET EXPRESSION AS (a * 100);  -- violates constraint
+ALTER TABLE gtest20 ALTER COLUMN b SET EXPRESSION AS (a * 3);  -- ok
+-- table rewrite should not happen
+SELECT pg_relation_filenode('gtest20') AS gtest20_filenode \gset
+ALTER TABLE gtest20 ALTER COLUMN b SET EXPRESSION AS (a * 4), ADD COLUMN c INT DEFAULT 11;
+SELECT pg_relation_filenode('gtest20') = :gtest20_filenode AS is_same_file;
+\d gtest20
 
 CREATE TABLE gtest20a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) VIRTUAL);
 INSERT INTO gtest20a (a) VALUES (10);
@@ -536,6 +541,13 @@ ALTER TABLE gtest_child ALTER COLUMN f3 SET EXPRESSION AS (f2 * 10);
 \d gtest_child3
 SELECT tableoid::regclass, * FROM gtest_parent ORDER BY 1, 2, 3;
 
+-- check constraint was validated based on each partitions's generation expression
+ALTER TABLE gtest_parent ADD CONSTRAINT cc1 CHECK (f3 < 19); -- error
+ALTER TABLE gtest_parent ADD CONSTRAINT cc1 CHECK (f3 < 66); -- error
+ALTER TABLE gtest_parent ADD CONSTRAINT cc1 CHECK (f3 <> 33); -- error
+ALTER TABLE gtest_parent ADD CONSTRAINT cc CHECK (f3 < 67); -- ok
+ALTER TABLE gtest_parent DROP CONSTRAINT cc;
+
 -- alter generation expression of parent and all its children altogether
 ALTER TABLE gtest_parent ALTER COLUMN f3 SET EXPRESSION AS (f2 * 2);
 \d gtest_parent