agora inbox for pgsql-hackers@postgresql.orghelp / color / mirror / Atom feed
[PATCH v8a 10/14] ci: windows: Check for errors, cmd/powershell don't have set -e behavior 149+ messages / 2 participants [nested] [flat]
* [PATCH v8a 10/14] ci: windows: Check for errors, cmd/powershell don't have set -e behavior @ 2026-06-03 06:24 Andres Freund <andres@anarazel.de> 0 siblings, 0 replies; 149+ messages in thread From: Andres Freund @ 2026-06-03 06:24 UTC (permalink / raw) --- .github/workflows/pg-ci.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pg-ci.yml b/.github/workflows/pg-ci.yml index f2543689fe5..ad9bbf745fb 100644 --- a/.github/workflows/pg-ci.yml +++ b/.github/workflows/pg-ci.yml @@ -307,7 +307,7 @@ jobs: ${{env.ADDITIONAL_SETUP}} echo ::group::test_setup - meson test ${{env.MTEST_ARGS}} --suite setup --logbase setup + meson test ${{env.MTEST_ARGS}} --suite setup --logbase setup || exit 1 echo ::endgroup:: meson test ${{env.MTEST_ARGS}} --num-processes ${{env.TEST_JOBS}} ${{env.MTEST_TARGET}} @@ -781,6 +781,7 @@ jobs: run: | # meson + ninja aren't preinstalled on windows-2022. Install via pip python -m pip install --upgrade meson ninja + if (!$?) { throw 'cmdfail' } # Install IPC::Run. @@ -793,7 +794,9 @@ jobs: # the thread at # https://postgr.es/m/CAN55FZ06xanSbJdHe-CurjX_qNuBWZDEvS1kAk36L38YCtZXnw%40mail.gmail.com "o conf recommends_policy 0`no conf commit`nnotest install NJM/IPC-Run-20250809.0.tar.gz" | cpan + if (!$?) { throw 'cmdfail' } perl -mIPC::Run -e 1 + if (!$?) { throw 'cmdfail' } - name: Setup hosts file shell: pwsh @@ -821,7 +824,7 @@ jobs: - name: Build run: | call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" x64 - ninja -C build ${{env.MBUILD_TARGET}} + ninja -C build ${{env.MBUILD_TARGET}} || exit 1 ninja -C build -t missingdeps - name: Test world -- 2.54.0.380.gc69baaf57b --vphnza2cz5zw5t4a Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="v8a-0011-ci-windows-ninja-is-already-installed.patch" ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
* [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions @ 2026-08-03 14:46 Alberto Piai <alberto.piai@gmail.com> 0 siblings, 0 replies; 149+ messages in thread From: Alberto Piai @ 2026-08-03 14:46 UTC (permalink / raw) When called on a partition / child table, ATPostAlterTypeCleanup doesn't enqueue rebuild commands for inherited constraint, with the assumption that they will be rebuilt later when the function is called on the parent table. When an ALTER TABLE command which causes a rewrite is called directly on the partition / child table though, ATPostAlterTypeCleanup is never called on the parent table, so the constraint is never recreated. The attached test case reproduces this using SET EXPRESSION. This is not meant for code-review but just to illustrate the idea I'm exploring: what if when we RememberConstraintForRebuilding() a constraint with conislocal=false, we also RememberConstraintForRebuilding() its corresponding parent constraint? The code is incomplete, I haven't fully considered all the implications and I'm only thinking about CHECK and NN constraints. It's just to illustrate the idea and get a discussion started. Reported-by: Jian He <jian.universality@gmail.com> --- src/backend/commands/tablecmds.c | 105 +++++++++++++++++++++- src/test/regress/expected/alter_table.out | 17 ++++ src/test/regress/sql/alter_table.sql | 8 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2fa534413ea..af3a2c95c6b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15561,6 +15561,84 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, return address; } +static Oid +get_parent_constraint(HeapTuple constraintTup) +{ + AttrMap *attmap; + Relation pg_constraints; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tuple; + Relation parentRel; + Relation conRel; + AttrNumber child_attno; + Oid result = InvalidOid; + Oid parentRelid = InvalidOid; + Form_pg_constraint child_con = GETSTRUCT(constraintTup); + + child_attno = child_con->contype == CONSTRAINT_NOTNULL ? extractNotNullColumn(constraintTup) : 0; + + parentRelid = get_partition_parent(child_con->conrelid, false); + + /* scan the parent constraints, matching by name for CHECK and by attno for NN */ + pg_constraints = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parentRelid)); + ScanKeyInit(&key[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + if (child_con->contype == CONSTRAINT_CHECK) + ScanKeyInit(&key[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(NameStr(child_con->conname))); + + scan = systable_beginscan(pg_constraints, + ConstraintRelidTypidNameIndexId, true, NULL, + child_con->contype == CONSTRAINT_CHECK ? 3 : 2, key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = GETSTRUCT(tuple); + + if (con->contype != child_con->contype) + continue; + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + { + /* Got a match by name */ + result = con->oid; + break; + } + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(tuple); + + parentRel = RelationIdGetRelation(parentRelid); + conRel = RelationIdGetRelation(child_con->conrelid); + attmap = build_attrmap_by_name(RelationGetDescr(parentRel), + RelationGetDescr(conRel), + true); + RelationClose(conRel); + RelationClose(parentRel); + + /* Got a match by attno */ + if (parent_attno == attmap->attnums[child_attno - 1]) + result = con->oid; + break; + } + } + + systable_endscan(scan); + table_close(pg_constraints, AccessShareLock); + + return result; +} + /* * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything * that depends on the column (constraints, indexes, etc), and record enough @@ -15634,9 +15712,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, } case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; + { + Oid parentConstraintId = InvalidOid; + HeapTuple tuple; + Form_pg_constraint con; + + Assert(foundObject.objectSubId == 0); + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(foundObject.objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + foundObject.objectId); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (!con->conislocal && OidIsValid(parentConstraintId = get_parent_constraint(tuple))) + { + RememberConstraintForRebuilding(parentConstraintId, tab); + } + + ReleaseSysCache(tuple); + + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + } case ProcedureRelationId: diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..60f5302d02e 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -4876,3 +4876,20 @@ drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; NOTICE: drop cascades to table alter2.t1 +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; + conname | contype +----------------+--------- + tab_b_not_null | n +(1 row) + +drop table tab; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index f5f13bbd3e7..da37d591d73 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -3159,3 +3159,11 @@ alter table alter1.t1 set schema alter2; drop publication pub1; drop schema alter1 cascade; drop schema alter2 cascade; + +-- Check that non-recursive SET EXPRESSION restores the constraints correctly +create table tab (a int, b int not null generated always as (a+1) stored) partition by list (a); +create table part partition of tab for values in (1); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +alter table part alter b set expression as (a); +select conname, contype from pg_constraint where conrelid = 'part'::regclass; +drop table tab; base-commit: fd2b89854d93d70fe8c9a69d5b8fafd5b9302cfc -- 2.47.0 --mb655kqrxwulpyvr-- ^ permalink raw reply [nested|flat] 149+ messages in thread
end of thread, other threads:[~2026-08-03 14:46 UTC | newest] Thread overview: 149+ messages (download: mbox mbox.gz follow: Atom feed) -- links below jump to the message on this page -- 2026-06-03 06:24 [PATCH v8a 10/14] ci: windows: Check for errors, cmd/powershell don't have set -e behavior Andres Freund <andres@anarazel.de> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com> 2026-08-03 14:46 [PATCH v1] WIP: fix ATPostAlterTypeCleanup dropping constraints on partitions Alberto Piai <alberto.piai@gmail.com>
This inbox is served by agora; see mirroring instructions for how to clone and mirror all data and code used for this inbox