Allow ALTER COLUMN SET EXPRESSION on virtual columns with CHECK constraints

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
This commit is contained in:
Peter Eisentraut 2026-02-24 10:30:50 +01:00
parent 462fe0ff62
commit f80bedd52b
4 changed files with 56 additions and 27 deletions

View 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>

View 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

View 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

View 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