agora inbox for pgsql-hackers@postgresql.orghelp / color / mirror / Atom feed
[PATCH v10 3/7] Row pattern recognition patch (planner). 149+ messages / 2 participants [nested] [flat]
* [PATCH v10 3/7] Row pattern recognition patch (planner). @ 2023-10-22 02:22 Tatsuo Ishii <ishii@postgresql.org> 0 siblings, 0 replies; 149+ messages in thread From: Tatsuo Ishii @ 2023-10-22 02:22 UTC (permalink / raw) --- src/backend/optimizer/plan/createplan.c | 23 ++++++++++++++++++----- src/backend/optimizer/plan/setrefs.c | 23 +++++++++++++++++++++++ src/include/nodes/plannodes.h | 15 +++++++++++++++ 3 files changed, 56 insertions(+), 5 deletions(-) diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c index 34ca6d4ac2..469fcd156b 100644 --- a/src/backend/optimizer/plan/createplan.c +++ b/src/backend/optimizer/plan/createplan.c @@ -286,9 +286,10 @@ static WindowAgg *make_windowagg(List *tlist, Index winref, int ordNumCols, AttrNumber *ordColIdx, Oid *ordOperators, Oid *ordCollations, int frameOptions, Node *startOffset, Node *endOffset, Oid startInRangeFunc, Oid endInRangeFunc, - Oid inRangeColl, bool inRangeAsc, bool inRangeNullsFirst, - List *runCondition, List *qual, bool topWindow, - Plan *lefttree); + Oid inRangeColl, bool inRangeAsc, bool inRangeNullsFirst, List *runCondition, + RPSkipTo rpSkipTo, List *patternVariable, List *patternRegexp, List *defineClause, + List *defineInitial, + List *qual, bool topWindow, Plan *lefttree); static Group *make_group(List *tlist, List *qual, int numGroupCols, AttrNumber *grpColIdx, Oid *grpOperators, Oid *grpCollations, Plan *lefttree); @@ -2698,6 +2699,11 @@ create_windowagg_plan(PlannerInfo *root, WindowAggPath *best_path) wc->inRangeAsc, wc->inRangeNullsFirst, wc->runCondition, + wc->rpSkipTo, + wc->patternVariable, + wc->patternRegexp, + wc->defineClause, + wc->defineInitial, best_path->qual, best_path->topwindow, subplan); @@ -6601,8 +6607,10 @@ make_windowagg(List *tlist, Index winref, int ordNumCols, AttrNumber *ordColIdx, Oid *ordOperators, Oid *ordCollations, int frameOptions, Node *startOffset, Node *endOffset, Oid startInRangeFunc, Oid endInRangeFunc, - Oid inRangeColl, bool inRangeAsc, bool inRangeNullsFirst, - List *runCondition, List *qual, bool topWindow, Plan *lefttree) + Oid inRangeColl, bool inRangeAsc, bool inRangeNullsFirst, List *runCondition, + RPSkipTo rpSkipTo, List *patternVariable, List *patternRegexp, List *defineClause, + List *defineInitial, + List *qual, bool topWindow, Plan *lefttree) { WindowAgg *node = makeNode(WindowAgg); Plan *plan = &node->plan; @@ -6628,6 +6636,11 @@ make_windowagg(List *tlist, Index winref, node->inRangeAsc = inRangeAsc; node->inRangeNullsFirst = inRangeNullsFirst; node->topWindow = topWindow; + node->rpSkipTo = rpSkipTo, + node->patternVariable = patternVariable; + node->patternRegexp = patternRegexp; + node->defineClause = defineClause; + node->defineInitial = defineInitial; plan->targetlist = tlist; plan->lefttree = lefttree; diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c index 7962200885..20ce399763 100644 --- a/src/backend/optimizer/plan/setrefs.c +++ b/src/backend/optimizer/plan/setrefs.c @@ -2456,6 +2456,29 @@ set_upper_references(PlannerInfo *root, Plan *plan, int rtoffset) NRM_EQUAL, NUM_EXEC_QUAL(plan)); + /* + * Modifies an expression tree in each DEFINE clause so that all Var + * nodes reference outputs of a subplan. + */ + if (IsA(plan, WindowAgg)) + { + WindowAgg *wplan = (WindowAgg *) plan; + + foreach(l, wplan->defineClause) + { + TargetEntry *tle = (TargetEntry *) lfirst(l); + + tle->expr = (Expr *) + fix_upper_expr(root, + (Node *) tle->expr, + subplan_itlist, + OUTER_VAR, + rtoffset, + NRM_EQUAL, + NUM_EXEC_QUAL(plan)); + } + } + pfree(subplan_itlist); } diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h index 8cafbf3f8a..e48b59517d 100644 --- a/src/include/nodes/plannodes.h +++ b/src/include/nodes/plannodes.h @@ -1096,6 +1096,21 @@ typedef struct WindowAgg /* nulls sort first for in_range tests? */ bool inRangeNullsFirst; + /* Row Pattern Recognition AFTER MACH SKIP clause */ + RPSkipTo rpSkipTo; /* Row Pattern Skip To type */ + + /* Row Pattern PATTERN variable name (list of String) */ + List *patternVariable; + + /* Row Pattern RPATTERN regular expression quantifier ('+' or ''. list of String) */ + List *patternRegexp; + + /* Row Pattern DEFINE clause (list of TargetEntry) */ + List *defineClause; + + /* Row Pattern DEFINE variable initial names (list of String) */ + List *defineInitial; + /* * false for all apart from the WindowAgg that's closest to the root of * the plan -- 2.25.1 ----Next_Part(Sun_Oct_22_11_39_20_2023_140)-- Content-Type: Text/X-Patch; charset=us-ascii Content-Transfer-Encoding: 7bit Content-Disposition: inline; filename="v10-0004-Row-pattern-recognition-patch-executor.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 -- 2023-10-22 02:22 [PATCH v10 3/7] Row pattern recognition patch (planner). Tatsuo Ishii <ishii@postgresql.org> 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