Whole-row fixes for ALTER COLUMN SET EXPRESSION

When changing the expression of a generated column via ALTER TABLE
ALTER COLUMN SET EXPRESSION, objects that depend on the column via
indirect whole-row references (such as CHECK constraints, indexes)
must be handled specially, because technically pg_depend does not
contain such dependencies, see
recordDependencyOnSingleRelExpr->find_expr_references_walker.

This is a fix for commit f80bedd52, "Allow ALTER COLUMN SET EXPRESSION
on virtual columns with CHECK constraints".

Author: jian he <jian.universality@gmail.com>
Co-authored-by: Peter Eisentraut <peter@eisentraut.org>
Reported-by: Ayush Tiwari <ayushtiwari.slg01@gmail.com>
Reviewed-by: Ayush Tiwari <ayushtiwari.slg01@gmail.com>
Reviewed-by: solai v <solai.cdac@gmail.com>
Reviewed-by: Zsolt Parragi <zsolt.parragi@percona.com>
Discussion: https://www.postgresql.org/message-id/flat/CAJTYsWXOkyeDVbzymWc9sKrq7Y_MUv6XJXN4H9GfsBOPd3NJ+w@mail.gmail.com
This commit is contained in:
Peter Eisentraut 2026-07-08 18:44:54 +02:00
parent 165dc09b28
commit 9530898940
5 changed files with 214 additions and 0 deletions

View file

@ -693,6 +693,7 @@ static ObjectAddress ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
AlterTableCmd *cmd, LOCKMODE lockmode);
static void RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype,
Relation rel, AttrNumber attnum, const char *colName);
static void RememberWholeRowDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, Relation rel);
static void RememberConstraintForRebuilding(Oid conoid, AlteredTableInfo *tab);
static void RememberIndexForRebuilding(Oid indoid, AlteredTableInfo *tab);
static void RememberStatisticsForRebuilding(Oid stxoid, AlteredTableInfo *tab);
@ -8773,6 +8774,13 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
*/
RememberAllDependentForRebuilding(tab, AT_SetExpression, rel, attnum, colName);
/*
* Find whole-row referenced objects that depend on the column
* (constraints, indexes, etc.), and record enough information to let us
* recreate the objects.
*/
RememberWholeRowDependentForRebuilding(tab, AT_SetExpression, rel);
/*
* Drop the dependency records of the GENERATED expression, in particular
* its INTERNAL dependency on the column, which would otherwise cause
@ -15735,6 +15743,174 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype,
table_close(depRel, NoLock);
}
/*
* Record information about dependencies between objects with whole-row Var
* references (indexes, check constraints, etc.) and the relation.
*
* See also RememberAllDependentForRebuilding, which handles non-whole-row Var
* references.
*
* This function currently applies only to ALTER COLUMN SET EXPRESSION.
*/
static void
RememberWholeRowDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, Relation rel)
{
ScanKeyData skey;
Relation pg_constraint;
Relation pg_index;
SysScanDesc conscan;
SysScanDesc indscan;
HeapTuple constrTuple;
HeapTuple indexTuple;
bool isnull;
Assert(subtype == AT_SetExpression);
/*
* Check CHECK constraints with whole-row references first.
*/
if (RelationGetDescr(rel)->constr &&
RelationGetDescr(rel)->constr->num_check > 0)
{
pg_constraint = table_open(ConstraintRelationId, AccessShareLock);
ScanKeyInit(&skey,
Anum_pg_constraint_conrelid,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(RelationGetRelid(rel)));
conscan = systable_beginscan(pg_constraint,
ConstraintRelidTypidNameIndexId,
true,
NULL,
1,
&skey);
while (HeapTupleIsValid(constrTuple = systable_getnext(conscan)))
{
Form_pg_constraint conform = (Form_pg_constraint) GETSTRUCT(constrTuple);
Datum exprDatum;
if (conform->contype != CONSTRAINT_CHECK)
continue;
exprDatum = heap_getattr(constrTuple,
Anum_pg_constraint_conbin,
RelationGetDescr(pg_constraint),
&isnull);
if (isnull)
elog(ERROR, "null conbin for relation \"%s\"",
RelationGetRelationName(rel));
else
{
char *exprString;
Node *expr;
Bitmapset *expr_attrs = NULL;
exprString = TextDatumGetCString(exprDatum);
expr = stringToNode(exprString);
pfree(exprString);
/* Find all attributes referenced */
pull_varattnos(expr, 1, &expr_attrs);
/*
* If the CHECK constraint contains whole-row reference then
* remember it.
*/
if (bms_is_member(InvalidAttrNumber - FirstLowInvalidHeapAttributeNumber, expr_attrs))
{
RememberConstraintForRebuilding(conform->oid, tab);
}
}
}
systable_endscan(conscan);
table_close(pg_constraint, AccessShareLock);
}
/*
* Now check indexes with whole-row references. Prepare to scan pg_index
* for entries having indrelid matching this relation.
*/
ScanKeyInit(&skey,
Anum_pg_index_indrelid,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(RelationGetRelid(rel)));
pg_index = table_open(IndexRelationId, AccessShareLock);
indscan = systable_beginscan(pg_index,
IndexIndrelidIndexId,
true,
NULL,
1,
&skey);
while (HeapTupleIsValid(indexTuple = systable_getnext(indscan)))
{
Form_pg_index index = (Form_pg_index) GETSTRUCT(indexTuple);
Datum exprDatum;
exprDatum = heap_getattr(indexTuple,
Anum_pg_index_indexprs,
RelationGetDescr(pg_index),
&isnull);
if (!isnull)
{
char *exprString;
Node *expr;
Bitmapset *expr_attrs = NULL;
exprString = TextDatumGetCString(exprDatum);
expr = stringToNode(exprString);
pfree(exprString);
/* Find all attributes referenced */
pull_varattnos(expr, 1, &expr_attrs);
/*
* If the index expression contains a whole-row reference then
* remember it.
*/
if (bms_is_member(InvalidAttrNumber - FirstLowInvalidHeapAttributeNumber, expr_attrs))
{
RememberIndexForRebuilding(index->indexrelid, tab);
continue;
}
}
exprDatum = heap_getattr(indexTuple,
Anum_pg_index_indpred,
RelationGetDescr(pg_index),
&isnull);
if (!isnull)
{
char *exprString;
Node *expr;
Bitmapset *expr_attrs = NULL;
exprString = TextDatumGetCString(exprDatum);
expr = stringToNode(exprString);
pfree(exprString);
/* Find all attributes referenced */
pull_varattnos(expr, 1, &expr_attrs);
/*
* If the index predicate expression contains a whole-row
* reference then remember it.
*/
if (bms_is_member(InvalidAttrNumber - FirstLowInvalidHeapAttributeNumber, expr_attrs))
{
RememberIndexForRebuilding(index->indexrelid, tab);
}
}
}
systable_endscan(indscan);
table_close(pg_index, AccessShareLock);
}
/*
* Subroutine for ATExecAlterColumnType: remember that a replica identity
* needs to be reset.

View file

@ -688,6 +688,15 @@ INSERT INTO gtest20c VALUES (1); -- ok
INSERT INTO gtest20c VALUES (NULL); -- fails
ERROR: new row for relation "gtest20c" violates check constraint "whole_row_check"
DETAIL: Failing row contains (null, null).
ALTER TABLE gtest20c ALTER COLUMN b SET EXPRESSION AS (NULL::int); -- violates constraint
ERROR: check constraint "whole_row_check" of relation "gtest20c" is violated by some row
-- index with whole-row reference needs rebuild
CREATE TABLE gtest20d (a int, b int GENERATED ALWAYS AS (a * 2) STORED);
INSERT INTO gtest20d VALUES (1), (1);
CREATE INDEX gtest20d_idx1 ON gtest20d (a) WHERE gtest20d = ROW (1, 2);
ALTER TABLE gtest20d ALTER COLUMN b SET EXPRESSION AS (a * 2::bigint); -- index rebuild
CREATE INDEX gtest20d_idx2 ON gtest20d ((gtest20d = ROW (1, 2)));
ALTER TABLE gtest20d ALTER COLUMN b SET EXPRESSION AS (a * 3); -- index rebuild
-- not-null constraints
CREATE TABLE gtest21a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (nullif(a, 0)) STORED NOT NULL);
INSERT INTO gtest21a (a) VALUES (1); -- ok

View file

@ -694,6 +694,15 @@ INSERT INTO gtest20c VALUES (1); -- ok
INSERT INTO gtest20c VALUES (NULL); -- fails
ERROR: new row for relation "gtest20c" violates check constraint "whole_row_check"
DETAIL: Failing row contains (null, virtual).
ALTER TABLE gtest20c ALTER COLUMN b SET EXPRESSION AS (NULL::int); -- violates constraint
ERROR: check constraint "whole_row_check" of relation "gtest20c" is violated by some row
-- index with whole-row reference needs rebuild
CREATE TABLE gtest20d (a int, b int GENERATED ALWAYS AS (a * 2) VIRTUAL);
INSERT INTO gtest20d VALUES (1), (1);
CREATE INDEX gtest20d_idx1 ON gtest20d (a) WHERE gtest20d = ROW (1, 2);
ALTER TABLE gtest20d ALTER COLUMN b SET EXPRESSION AS (a * 2::bigint); -- index rebuild
CREATE INDEX gtest20d_idx2 ON gtest20d ((gtest20d = ROW (1, 2)));
ALTER TABLE gtest20d ALTER COLUMN b SET EXPRESSION AS (a * 3); -- index rebuild
-- not-null constraints
CREATE TABLE gtest21a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (nullif(a, 0)) VIRTUAL NOT NULL);
INSERT INTO gtest21a (a) VALUES (1); -- ok

View file

@ -341,6 +341,16 @@ CREATE TABLE gtest20c (a int, b int GENERATED ALWAYS AS (a * 2) STORED);
ALTER TABLE gtest20c ADD CONSTRAINT whole_row_check CHECK (gtest20c IS NOT NULL);
INSERT INTO gtest20c VALUES (1); -- ok
INSERT INTO gtest20c VALUES (NULL); -- fails
ALTER TABLE gtest20c ALTER COLUMN b SET EXPRESSION AS (NULL::int); -- violates constraint
-- index with whole-row reference needs rebuild
CREATE TABLE gtest20d (a int, b int GENERATED ALWAYS AS (a * 2) STORED);
INSERT INTO gtest20d VALUES (1), (1);
CREATE INDEX gtest20d_idx1 ON gtest20d (a) WHERE gtest20d = ROW (1, 2);
ALTER TABLE gtest20d ALTER COLUMN b SET EXPRESSION AS (a * 2::bigint); -- index rebuild
CREATE INDEX gtest20d_idx2 ON gtest20d ((gtest20d = ROW (1, 2)));
ALTER TABLE gtest20d ALTER COLUMN b SET EXPRESSION AS (a * 3); -- index rebuild
-- not-null constraints
CREATE TABLE gtest21a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (nullif(a, 0)) STORED NOT NULL);

View file

@ -347,6 +347,16 @@ CREATE TABLE gtest20c (a int, b int GENERATED ALWAYS AS (a * 2) VIRTUAL);
ALTER TABLE gtest20c ADD CONSTRAINT whole_row_check CHECK (gtest20c IS NOT NULL);
INSERT INTO gtest20c VALUES (1); -- ok
INSERT INTO gtest20c VALUES (NULL); -- fails
ALTER TABLE gtest20c ALTER COLUMN b SET EXPRESSION AS (NULL::int); -- violates constraint
-- index with whole-row reference needs rebuild
CREATE TABLE gtest20d (a int, b int GENERATED ALWAYS AS (a * 2) VIRTUAL);
INSERT INTO gtest20d VALUES (1), (1);
CREATE INDEX gtest20d_idx1 ON gtest20d (a) WHERE gtest20d = ROW (1, 2);
ALTER TABLE gtest20d ALTER COLUMN b SET EXPRESSION AS (a * 2::bigint); -- index rebuild
CREATE INDEX gtest20d_idx2 ON gtest20d ((gtest20d = ROW (1, 2)));
ALTER TABLE gtest20d ALTER COLUMN b SET EXPRESSION AS (a * 3); -- index rebuild
-- not-null constraints
CREATE TABLE gtest21a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (nullif(a, 0)) VIRTUAL NOT NULL);