From 1b6c411bacfe88ba913382ea5b88c305ce509e8d Mon Sep 17 00:00:00 2001 From: Nik Samokhvalov Date: Sun, 13 Sep 2026 03:41:54 -0700 Subject: [PATCH] Validate inherited CHECK constraints when enabling enforcement A local enforced CHECK can merge into an inherited NOT ENFORCED constraint without checking existing rows. Preserve NOT VALID and queue validation when required, including descendants whose enforcement state changes. Retain ADD CONSTRAINT permissions and ONLY restrictions. Reuse the CHECK enforceability traversal without changing inheritance counts. Add regression coverage for direct and descendant merges, validation state, multiple parents, ONLY, and descendant ownership. This PG19 patch replaces the direct-case change in PR #81. PG18 needs a separate adaptation because it lacks this enforceability traversal. --- src/backend/catalog/heap.c | 10 +- src/backend/commands/tablecmds.c | 162 ++++++++++++++++--- src/test/regress/expected/inherit.out | 217 +++++++++++++++++++++++++- src/test/regress/sql/inherit.sql | 119 +++++++++++++- 4 files changed, 484 insertions(+), 24 deletions(-) diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c index c85d6ef9979..65f1d57d317 100644 --- a/src/backend/catalog/heap.c +++ b/src/backend/catalog/heap.c @@ -2896,12 +2896,20 @@ MergeWithExistingConstraint(Relation rel, const char *ccname, Node *expr, * constraint is not, this should be allowed by marking the child * constraint as enforced. In the reverse case, an error would have * already been thrown before reaching this point. + * + * Since the constraint was not enforced before, its existing rows + * have never been checked against it, so it must not become validated + * as a side effect of the merge. Mark it validated only if the new + * constraint says to be initially valid, in which case it is the + * caller's responsibility to verify the existing rows + * (ATAddCheckNNConstraint queues that work); otherwise it remains NOT + * VALID until ALTER TABLE ... VALIDATE CONSTRAINT is run. */ if (is_enforced && !con->conenforced) { Assert(is_local); con->conenforced = true; - con->convalidated = true; + con->convalidated = is_initially_valid; } CatalogTupleUpdate(conDesc, &tup->t_self, tup); diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 67829288ed4..e861bb7e263 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -410,6 +410,9 @@ static bool ATExecAlterCheckConstrEnforceability(List **wqueue, ATAlterConstrain Relation conrel, HeapTuple contuple, bool recurse, bool recursing, List *changing_conids, + bool skip_validation, + bool force_recurse, + bool force_validation, LOCKMODE lockmode); static bool ATExecAlterConstrDeferrability(List **wqueue, ATAlterConstraint *cmdcon, Relation conrel, Relation tgrel, Relation rel, @@ -433,13 +436,16 @@ static void AlterCheckConstrEnforceabilityRecurse(List **wqueue, ATAlterConstrai Relation conrel, Oid conrelid, bool recurse, bool recursing, List *changing_conids, + bool skip_validation, + bool force_validation, LOCKMODE lockmode); static void AlterConstrDeferrabilityRecurse(List **wqueue, ATAlterConstraint *cmdcon, Relation conrel, Relation tgrel, Relation rel, HeapTuple contuple, bool recurse, List **otherrelids, LOCKMODE lockmode); static void AlterConstrUpdateConstraintEntry(ATAlterConstraint *cmdcon, Relation conrel, - HeapTuple contuple); + HeapTuple contuple, + bool skip_validation); static bool ATCheckCheckConstrHasEnforcedParent(Relation conrel, Relation rel, HeapTuple contuple, List *changing_conids, @@ -9951,6 +9957,7 @@ ATAddCheckNNConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, ListCell *lcon; List *children; ListCell *child; + Oid enforcing_conoid = InvalidOid; ObjectAddress address = InvalidObjectAddress; /* Guard against stack overflow due to overly deep inheritance tree. */ @@ -9961,15 +9968,50 @@ ATAddCheckNNConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, ATSimplePermissions(AT_AddConstraint, rel, ATT_TABLE | ATT_PARTITIONED_TABLE | ATT_FOREIGN_TABLE); + /* + * If the command names a check constraint and an identically-named NOT + * ENFORCED check constraint already exists on this relation, then (if + * things go well) AddRelationNewConstraints will merge the new constraint + * into the existing one, additionally marking it enforced. Existing rows + * have never been checked against a NOT ENFORCED constraint, so unlike + * ordinary merges this one requires the existing rows to be verified when + * the new constraint is to be valid. Take note of the pre-merge state, + * so that we can queue that work below. + */ + if (constr->contype == CONSTR_CHECK && + constr->conname != NULL && + constr->is_enforced) + { + Oid conoid; + + conoid = get_relation_constraint_oid(RelationGetRelid(rel), + constr->conname, true); + if (OidIsValid(conoid)) + { + HeapTuple contup; + Form_pg_constraint conform; + + contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(conoid)); + if (!HeapTupleIsValid(contup)) + elog(ERROR, "cache lookup failed for constraint %u", conoid); + conform = (Form_pg_constraint) GETSTRUCT(contup); + if (conform->contype == CONSTRAINT_CHECK && !conform->conenforced) + enforcing_conoid = conoid; + ReleaseSysCache(contup); + } + } + /* * Call AddRelationNewConstraints to do the work, making sure it works on * a copy of the Constraint so transformExpr can't modify the original. It * returns a list of cooked constraints. * * If the constraint ends up getting merged with a pre-existing one, it's - * omitted from the returned list, which is what we want: we do not need - * to do any validation work. That can only happen at child tables, - * though, since we disallow merging at the top level. + * omitted from the returned list. Normally there is then no validation + * work to do, but if the merge marked a previously NOT ENFORCED + * constraint as enforced, existing rows must be verified; that case is + * handled below. Merging can only happen at child tables, though, since + * we disallow merging at the top level. */ newcons = AddRelationNewConstraints(rel, NIL, list_make1(copyObject(constr)), @@ -10022,6 +10064,64 @@ ATAddCheckNNConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, /* Advance command counter in case same table is visited multiple times */ CommandCounterIncrement(); + /* + * If the new constraint was merged into a pre-existing NOT ENFORCED + * constraint, propagate its new enforceability to descendants and queue + * verification of every relation that was not already known valid. The + * normal add-constraint recursion cannot do this, since a merge returns + * no CookedConstraint and must not increment descendants' coninhcount. + */ + if (newcons == NIL && OidIsValid(enforcing_conoid)) + { + ATAlterConstraint altercon = {0}; + Relation conrel; + HeapTuple contup; + + /* + * The enforceability traversal does not perform the checks made by + * normal ADD CONSTRAINT recursion. Preserve those checks here, + * including ONLY's restriction on changing inheritable constraints. + */ + children = find_all_inheritors(RelationGetRelid(rel), lockmode, NULL); + foreach_oid(childoid, children) + { + Relation childrel; + + if (childoid == RelationGetRelid(rel)) + continue; + if (!recurse) + ereport(ERROR, + (errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("constraint must be added to child tables too"))); + + /* find_all_inheritors already got lock */ + childrel = table_open(childoid, NoLock); + CheckAlterTableIsSafe(childrel); + ATSimplePermissions(AT_AddConstraint, childrel, + ATT_TABLE | ATT_PARTITIONED_TABLE | ATT_FOREIGN_TABLE); + table_close(childrel, NoLock); + } + list_free(children); + + conrel = table_open(ConstraintRelationId, RowExclusiveLock); + contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(enforcing_conoid)); + if (!HeapTupleIsValid(contup)) + elog(ERROR, "cache lookup failed for constraint %u", + enforcing_conoid); + + altercon.conname = constr->conname; + altercon.alterEnforceability = true; + altercon.is_enforced = true; + ATExecAlterCheckConstrEnforceability(wqueue, &altercon, conrel, + contup, recurse, false, NIL, + constr->skip_validation, + true, + constr->initially_valid, + lockmode); + ReleaseSysCache(contup); + table_close(conrel, RowExclusiveLock); + } + /* * If the constraint got merged with an existing constraint, we're done. * We mustn't recurse to child tables in this case, because they've @@ -12414,7 +12514,8 @@ ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon, else if (currcon->contype == CONSTRAINT_CHECK) changed = ATExecAlterCheckConstrEnforceability(wqueue, cmdcon, conrel, contuple, recurse, false, - NIL, lockmode); + NIL, false, false, false, + lockmode); } else if (cmdcon->alterDeferrability && ATExecAlterConstrDeferrability(wqueue, cmdcon, conrel, tgrel, rel, @@ -12486,7 +12587,7 @@ ATExecAlterFKConstrEnforceability(List **wqueue, ATAlterConstraint *cmdcon, if (currcon->conenforced != cmdcon->is_enforced) { - AlterConstrUpdateConstraintEntry(cmdcon, conrel, contuple); + AlterConstrUpdateConstraintEntry(cmdcon, conrel, contuple, false); changed = true; } @@ -12596,13 +12697,21 @@ ATExecAlterFKConstrEnforceability(List **wqueue, ATAlterConstraint *cmdcon, } /* - * Returns true if the CHECK constraint's enforceability is altered. + * Returns true if the CHECK constraint's enforcement or validation state changes. + * + * ADD CONSTRAINT uses force_recurse after merging into an inherited constraint: + * the merge has already updated the named relation, but its descendants still + * need processing. force_validation also validates descendants that were + * already enforced but not yet valid. skip_validation supports NOT VALID. */ static bool ATExecAlterCheckConstrEnforceability(List **wqueue, ATAlterConstraint *cmdcon, Relation conrel, HeapTuple contuple, bool recurse, bool recursing, List *changing_conids, + bool skip_validation, + bool force_recurse, + bool force_validation, LOCKMODE lockmode) { Form_pg_constraint currcon; @@ -12664,12 +12773,14 @@ ATExecAlterCheckConstrEnforceability(List **wqueue, ATAlterConstraint *cmdcon, * requested enforceability when another matching parent constraint * remains enforced. */ - if (currcon->conenforced != target_enforced) + if (currcon->conenforced != target_enforced || + (force_validation && target_enforced && !currcon->convalidated)) { ATAlterConstraint updatecon = *cmdcon; updatecon.is_enforced = target_enforced; - AlterConstrUpdateConstraintEntry(&updatecon, conrel, contuple); + AlterConstrUpdateConstraintEntry(&updatecon, conrel, contuple, + skip_validation); changed = true; } @@ -12680,9 +12791,10 @@ ATExecAlterCheckConstrEnforceability(List **wqueue, ATAlterConstraint *cmdcon, * unless they still inherit an enforced constraint from another parent. * Conversely, we should do nothing if a constraint is being set to * enforced and is already enforced, as descendant constraints cannot be - * different in that case. + * different in that case. An ADD CONSTRAINT merge has already updated + * the root constraint, so force_recurse overrides that shortcut. */ - if (!cmdcon->is_enforced || changed) + if (!cmdcon->is_enforced || changed || force_recurse) { /* * If we're recursing, the parent has already done this, so skip it. @@ -12750,17 +12862,22 @@ ATExecAlterCheckConstrEnforceability(List **wqueue, ATAlterConstraint *cmdcon, AlterCheckConstrEnforceabilityRecurse(wqueue, cmdcon, conrel, childoid, false, true, changing_conids, + skip_validation, + force_validation, lockmode); } } /* * Tell Phase 3 to check that the constraint is satisfied by existing - * rows. We only need do this when altering the constraint from NOT - * ENFORCED to ENFORCED. + * rows. Besides newly enforced constraints, an ADD CONSTRAINT merge + * requires validation of its already-updated root and of descendants that + * were already enforced but not yet valid. */ if (rel->rd_rel->relkind == RELKIND_RELATION && - !currcon->conenforced && + (!currcon->conenforced || force_recurse || + (force_validation && !currcon->convalidated)) && + !skip_validation && target_enforced) { AlteredTableInfo *tab; @@ -12802,6 +12919,8 @@ AlterCheckConstrEnforceabilityRecurse(List **wqueue, ATAlterConstraint *cmdcon, Relation conrel, Oid conrelid, bool recurse, bool recursing, List *changing_conids, + bool skip_validation, + bool force_validation, LOCKMODE lockmode) { SysScanDesc pscan; @@ -12832,6 +12951,8 @@ AlterCheckConstrEnforceabilityRecurse(List **wqueue, ATAlterConstraint *cmdcon, ATExecAlterCheckConstrEnforceability(wqueue, cmdcon, conrel, childtup, recurse, recursing, changing_conids, + skip_validation, false, + force_validation, lockmode); systable_endscan(pscan); @@ -13001,7 +13122,7 @@ ATExecAlterConstrDeferrability(List **wqueue, ATAlterConstraint *cmdcon, if (currcon->condeferrable != cmdcon->deferrable || currcon->condeferred != cmdcon->initdeferred) { - AlterConstrUpdateConstraintEntry(cmdcon, conrel, contuple); + AlterConstrUpdateConstraintEntry(cmdcon, conrel, contuple, false); changed = true; /* @@ -13054,7 +13175,7 @@ ATExecAlterConstrInheritability(List **wqueue, ATAlterConstraint *cmdcon, if (cmdcon->noinherit == currcon->connoinherit) return false; - AlterConstrUpdateConstraintEntry(cmdcon, conrel, contuple); + AlterConstrUpdateConstraintEntry(cmdcon, conrel, contuple, false); CommandCounterIncrement(); /* Fetch the column number and name */ @@ -13271,7 +13392,7 @@ AlterConstrDeferrabilityRecurse(List **wqueue, ATAlterConstraint *cmdcon, */ static void AlterConstrUpdateConstraintEntry(ATAlterConstraint *cmdcon, Relation conrel, - HeapTuple contuple) + HeapTuple contuple, bool skip_validation) { HeapTuple copyTuple; Form_pg_constraint copy_con; @@ -13290,10 +13411,11 @@ AlterConstrUpdateConstraintEntry(ATAlterConstraint *cmdcon, Relation conrel, * NB: The convalidated status is irrelevant when the constraint is * set to NOT ENFORCED, but for consistency, it should still be set * appropriately. Similarly, if the constraint is later changed to - * ENFORCED, validation will be performed during phase 3, so it makes - * sense to mark it as valid in that case. + * ENFORCED, validation will normally be performed during phase 3. ADD + * CONSTRAINT NOT VALID can instead request that we skip that + * validation and leave the constraint unvalidated. */ - copy_con->convalidated = cmdcon->is_enforced; + copy_con->convalidated = cmdcon->is_enforced && !skip_validation; } if (cmdcon->alterDeferrability) { diff --git a/src/test/regress/expected/inherit.out b/src/test/regress/expected/inherit.out index 3c2ff55d3ba..7ceff010f6d 100644 --- a/src/test/regress/expected/inherit.out +++ b/src/test/regress/expected/inherit.out @@ -1371,7 +1371,8 @@ NOTICE: merging constraint "inh_check_constraint6" with inherited definition alter table p1_c1 add constraint inh_check_constraint9 check (f1 < 10) not valid enforced; alter table p1 add constraint inh_check_constraint9 check (f1 < 10) not enforced; NOTICE: merging constraint "inh_check_constraint9" with inherited definition --- the not-valid state of the child constraint will be ignored here. +-- the not-valid state of the child constraint is preserved here, so the +-- merged constraint becomes enforced but remains NOT VALID. alter table p1 add constraint inh_check_constraint10 check (f1 < 10) not enforced; alter table p1_c1 add constraint inh_check_constraint10 check (f1 < 10) not valid enforced; NOTICE: merging constraint "inh_check_constraint10" with inherited definition @@ -1411,7 +1412,7 @@ order by 1, 2; p1 | inh_check_constraint8 | t | 0 | t | t p1 | inh_check_constraint9 | t | 0 | f | f p1_c1 | inh_check_constraint1 | t | 1 | t | t - p1_c1 | inh_check_constraint10 | t | 1 | t | t + p1_c1 | inh_check_constraint10 | t | 1 | t | f p1_c1 | inh_check_constraint2 | t | 1 | t | t p1_c1 | inh_check_constraint3 | t | 1 | f | f p1_c1 | inh_check_constraint4 | t | 1 | f | f @@ -1479,6 +1480,218 @@ NOTICE: drop cascades to 3 other objects DETAIL: drop cascades to table p1_c1 drop cascades to table p1_c2 drop cascades to table p1_c3 +-- Existing rows must be verified when merging a local ENFORCED constraint +-- into an inherited NOT ENFORCED one +create table p1(f1 int); +create table p1_c1() inherits(p1); +alter table p1 add constraint inh_check_constraint check (f1 > 0) not enforced; +insert into p1_c1 values(-1); +alter table p1_c1 add constraint inh_check_constraint check (f1 > 0); --error +NOTICE: merging constraint "inh_check_constraint" with inherited definition +ERROR: check constraint "inh_check_constraint" of relation "p1_c1" is violated by some row +-- adding it as NOT VALID skips the verification, and the merged constraint +-- must not be marked validated +alter table p1_c1 add constraint inh_check_constraint check (f1 > 0) not valid; --ok +NOTICE: merging constraint "inh_check_constraint" with inherited definition +select conrelid::regclass, conenforced, convalidated +from pg_constraint where conname = 'inh_check_constraint' +order by conrelid::regclass::text collate "C"; + conrelid | conenforced | convalidated +----------+-------------+-------------- + p1 | f | f + p1_c1 | t | f +(2 rows) + +alter table p1_c1 validate constraint inh_check_constraint; --error +ERROR: check constraint "inh_check_constraint" of relation "p1_c1" is violated by some row +delete from p1_c1 where f1 = -1; +alter table p1_c1 validate constraint inh_check_constraint; --ok +drop table p1 cascade; +NOTICE: drop cascades to table p1_c1 +-- with no violating rows the merge succeeds, and the verification allows the +-- merged constraint to be marked validated. ONLY is allowed without children. +create table p1(f1 int); +create table p1_c1() inherits(p1); +alter table p1 add constraint inh_check_constraint check (f1 > 0) not enforced; +insert into p1_c1 values(1); +alter table only p1_c1 add constraint inh_check_constraint check (f1 > 0); --ok +NOTICE: merging constraint "inh_check_constraint" with inherited definition +select conrelid::regclass, conenforced, convalidated +from pg_constraint where conname = 'inh_check_constraint' +order by conrelid::regclass::text collate "C"; + conrelid | conenforced | convalidated +----------+-------------+-------------- + p1 | f | f + p1_c1 | t | t +(2 rows) + +drop table p1 cascade; +NOTICE: drop cascades to table p1_c1 +-- Promotion must cover all descendants, without changing inheritance counts. +create table p1(f1 int); +create table p1_c1() inherits(p1); +create table p1_c2() inherits(p1_c1); +alter table p1 add constraint inh_check_constraint check (f1 > 0) not enforced; +-- ONLY must not silently change descendants, with or without validation. +alter table only p1_c1 add constraint inh_check_constraint check (f1 > 0); --error +NOTICE: merging constraint "inh_check_constraint" with inherited definition +ERROR: constraint must be added to child tables too +alter table only p1_c1 add constraint inh_check_constraint check (f1 > 0) not valid; --error +NOTICE: merging constraint "inh_check_constraint" with inherited definition +ERROR: constraint must be added to child tables too +select conrelid::regclass, conislocal, conenforced, convalidated +from pg_constraint where conname = 'inh_check_constraint' +order by conrelid::regclass::text collate "C"; + conrelid | conislocal | conenforced | convalidated +----------+------------+-------------+-------------- + p1 | t | f | f + p1_c1 | f | f | f + p1_c2 | f | f | f +(3 rows) + +insert into p1_c2 values(-1); +alter table p1_c1 add constraint inh_check_constraint check (f1 > 0); --error +NOTICE: merging constraint "inh_check_constraint" with inherited definition +ERROR: check constraint "inh_check_constraint" of relation "p1_c2" is violated by some row +select conrelid::regclass, coninhcount, conenforced, convalidated +from pg_constraint where conname = 'inh_check_constraint' +order by conrelid::regclass::text collate "C"; + conrelid | coninhcount | conenforced | convalidated +----------+-------------+-------------+-------------- + p1 | 0 | f | f + p1_c1 | 1 | f | f + p1_c2 | 1 | f | f +(3 rows) + +delete from p1_c2; +alter table p1_c1 add constraint inh_check_constraint check (f1 > 0); --ok +NOTICE: merging constraint "inh_check_constraint" with inherited definition +insert into p1_c2 values(-2); --error +ERROR: new row for relation "p1_c2" violates check constraint "inh_check_constraint" +DETAIL: Failing row contains (-2). +select conrelid::regclass, coninhcount, conenforced, convalidated +from pg_constraint where conname = 'inh_check_constraint' +order by conrelid::regclass::text collate "C"; + conrelid | coninhcount | conenforced | convalidated +----------+-------------+-------------+-------------- + p1 | 0 | f | f + p1_c1 | 1 | t | t + p1_c2 | 1 | t | t +(3 rows) + +drop table p1 cascade; +NOTICE: drop cascades to 2 other objects +DETAIL: drop cascades to table p1_c1 +drop cascades to table p1_c2 +-- A merge must retain ADD CONSTRAINT's permission checks on descendants. +create role regress_check_owner; +create table p1(f1 int); +create table p1_c1() inherits(p1); +create table p1_c2() inherits(p1_c1); +alter table p1 add constraint inh_check_constraint check (f1 > 0) not enforced; +alter table p1_c1 owner to regress_check_owner; +set role regress_check_owner; +alter table p1_c1 add constraint inh_check_constraint check (f1 > 0); --error +NOTICE: merging constraint "inh_check_constraint" with inherited definition +ERROR: must be owner of table p1_c2 +reset role; +select conrelid::regclass, conislocal, conenforced, convalidated +from pg_constraint where conname = 'inh_check_constraint' +order by conrelid::regclass::text collate "C"; + conrelid | conislocal | conenforced | convalidated +----------+------------+-------------+-------------- + p1 | t | f | f + p1_c1 | f | f | f + p1_c2 | f | f | f +(3 rows) + +drop table p1 cascade; +NOTICE: drop cascades to 2 other objects +DETAIL: drop cascades to table p1_c1 +drop cascades to table p1_c2 +drop role regress_check_owner; +-- NOT VALID promotion skips the initial scan, but propagates enforcement to +-- all descendants so that new rows are checked everywhere. +create table p1(f1 int); +create table p1_c1() inherits(p1); +create table p1_c2() inherits(p1_c1); +alter table p1 add constraint inh_check_constraint check (f1 > 0) not enforced; +insert into p1_c2 values(-1); +alter table p1_c1 add constraint inh_check_constraint check (f1 > 0) not valid; +NOTICE: merging constraint "inh_check_constraint" with inherited definition +insert into p1_c2 values(-2); --error +ERROR: new row for relation "p1_c2" violates check constraint "inh_check_constraint" +DETAIL: Failing row contains (-2). +select conrelid::regclass, coninhcount, conenforced, convalidated +from pg_constraint where conname = 'inh_check_constraint' +order by conrelid::regclass::text collate "C"; + conrelid | coninhcount | conenforced | convalidated +----------+-------------+-------------+-------------- + p1 | 0 | f | f + p1_c1 | 1 | t | f + p1_c2 | 1 | t | f +(3 rows) + +drop table p1 cascade; +NOTICE: drop cascades to 2 other objects +DETAIL: drop cascades to table p1_c1 +drop cascades to table p1_c2 +-- A descendant already promoted with NOT VALID must still be scanned when an +-- ancestor is later promoted as valid. +create table p1(f1 int); +create table p1_c1() inherits(p1); +create table p1_c2() inherits(p1_c1); +alter table p1 add constraint inh_check_constraint check (f1 > 0) not enforced; +insert into p1_c2 values(-1); +alter table p1_c2 add constraint inh_check_constraint check (f1 > 0) not valid; +NOTICE: merging constraint "inh_check_constraint" with inherited definition +alter table p1_c1 add constraint inh_check_constraint check (f1 > 0); --error +NOTICE: merging constraint "inh_check_constraint" with inherited definition +ERROR: check constraint "inh_check_constraint" of relation "p1_c2" is violated by some row +delete from p1_c2; +alter table p1_c1 add constraint inh_check_constraint check (f1 > 0); --ok +NOTICE: merging constraint "inh_check_constraint" with inherited definition +select conrelid::regclass, coninhcount, conenforced, convalidated +from pg_constraint where conname = 'inh_check_constraint' +order by conrelid::regclass::text collate "C"; + conrelid | coninhcount | conenforced | convalidated +----------+-------------+-------------+-------------- + p1 | 0 | f | f + p1_c1 | 1 | t | t + p1_c2 | 1 | t | t +(3 rows) + +drop table p1 cascade; +NOTICE: drop cascades to 2 other objects +DETAIL: drop cascades to table p1_c1 +drop cascades to table p1_c2 +-- Multiple inherited definitions remain merged while a local promotion scans +-- the relation. +create table p1(f1 int); +create table p1_other(f1 int); +alter table p1 add constraint inh_check_constraint check (f1 > 0) not enforced; +alter table p1_other add constraint inh_check_constraint check (f1 > 0) not enforced; +create table p1_c1() inherits(p1, p1_other); +NOTICE: merging multiple inherited definitions of column "f1" +insert into p1_c1 values(-1); +alter table p1_c1 add constraint inh_check_constraint check (f1 > 0); --error +NOTICE: merging constraint "inh_check_constraint" with inherited definition +ERROR: check constraint "inh_check_constraint" of relation "p1_c1" is violated by some row +delete from p1_c1; +alter table p1_c1 add constraint inh_check_constraint check (f1 > 0); --ok +NOTICE: merging constraint "inh_check_constraint" with inherited definition +select conrelid::regclass, coninhcount, conenforced, convalidated +from pg_constraint where conname = 'inh_check_constraint' +order by conrelid::regclass::text collate "C"; + conrelid | coninhcount | conenforced | convalidated +----------+-------------+-------------+-------------- + p1 | 0 | f | f + p1_c1 | 2 | t | t + p1_other | 0 | f | f +(3 rows) + +drop table p1, p1_other cascade; +NOTICE: drop cascades to table p1_c1 -- an inherited CHECK constraint cannot be NOT ENFORCED under an ENFORCED parent create table p1(f1 int constraint p1_a_check check (f1 > 0) enforced); create table p1_c1() inherits(p1); diff --git a/src/test/regress/sql/inherit.sql b/src/test/regress/sql/inherit.sql index 072fca13c13..62d99794adc 100644 --- a/src/test/regress/sql/inherit.sql +++ b/src/test/regress/sql/inherit.sql @@ -485,7 +485,8 @@ alter table p1_c1 add constraint inh_check_constraint6 check (f1 < 10) enforced; alter table p1_c1 add constraint inh_check_constraint9 check (f1 < 10) not valid enforced; alter table p1 add constraint inh_check_constraint9 check (f1 < 10) not enforced; --- the not-valid state of the child constraint will be ignored here. +-- the not-valid state of the child constraint is preserved here, so the +-- merged constraint becomes enforced but remains NOT VALID. alter table p1 add constraint inh_check_constraint10 check (f1 < 10) not enforced; alter table p1_c1 add constraint inh_check_constraint10 check (f1 < 10) not valid enforced; @@ -535,6 +536,122 @@ where conname = 'inh_check_constraint3' and contype = 'c' order by conrelid::regclass::text collate "C"; drop table p1 cascade; +-- Existing rows must be verified when merging a local ENFORCED constraint +-- into an inherited NOT ENFORCED one +create table p1(f1 int); +create table p1_c1() inherits(p1); +alter table p1 add constraint inh_check_constraint check (f1 > 0) not enforced; +insert into p1_c1 values(-1); +alter table p1_c1 add constraint inh_check_constraint check (f1 > 0); --error +-- adding it as NOT VALID skips the verification, and the merged constraint +-- must not be marked validated +alter table p1_c1 add constraint inh_check_constraint check (f1 > 0) not valid; --ok +select conrelid::regclass, conenforced, convalidated +from pg_constraint where conname = 'inh_check_constraint' +order by conrelid::regclass::text collate "C"; +alter table p1_c1 validate constraint inh_check_constraint; --error +delete from p1_c1 where f1 = -1; +alter table p1_c1 validate constraint inh_check_constraint; --ok +drop table p1 cascade; + +-- with no violating rows the merge succeeds, and the verification allows the +-- merged constraint to be marked validated. ONLY is allowed without children. +create table p1(f1 int); +create table p1_c1() inherits(p1); +alter table p1 add constraint inh_check_constraint check (f1 > 0) not enforced; +insert into p1_c1 values(1); +alter table only p1_c1 add constraint inh_check_constraint check (f1 > 0); --ok +select conrelid::regclass, conenforced, convalidated +from pg_constraint where conname = 'inh_check_constraint' +order by conrelid::regclass::text collate "C"; +drop table p1 cascade; + +-- Promotion must cover all descendants, without changing inheritance counts. +create table p1(f1 int); +create table p1_c1() inherits(p1); +create table p1_c2() inherits(p1_c1); +alter table p1 add constraint inh_check_constraint check (f1 > 0) not enforced; +-- ONLY must not silently change descendants, with or without validation. +alter table only p1_c1 add constraint inh_check_constraint check (f1 > 0); --error +alter table only p1_c1 add constraint inh_check_constraint check (f1 > 0) not valid; --error +select conrelid::regclass, conislocal, conenforced, convalidated +from pg_constraint where conname = 'inh_check_constraint' +order by conrelid::regclass::text collate "C"; +insert into p1_c2 values(-1); +alter table p1_c1 add constraint inh_check_constraint check (f1 > 0); --error +select conrelid::regclass, coninhcount, conenforced, convalidated +from pg_constraint where conname = 'inh_check_constraint' +order by conrelid::regclass::text collate "C"; +delete from p1_c2; +alter table p1_c1 add constraint inh_check_constraint check (f1 > 0); --ok +insert into p1_c2 values(-2); --error +select conrelid::regclass, coninhcount, conenforced, convalidated +from pg_constraint where conname = 'inh_check_constraint' +order by conrelid::regclass::text collate "C"; +drop table p1 cascade; + +-- A merge must retain ADD CONSTRAINT's permission checks on descendants. +create role regress_check_owner; +create table p1(f1 int); +create table p1_c1() inherits(p1); +create table p1_c2() inherits(p1_c1); +alter table p1 add constraint inh_check_constraint check (f1 > 0) not enforced; +alter table p1_c1 owner to regress_check_owner; +set role regress_check_owner; +alter table p1_c1 add constraint inh_check_constraint check (f1 > 0); --error +reset role; +select conrelid::regclass, conislocal, conenforced, convalidated +from pg_constraint where conname = 'inh_check_constraint' +order by conrelid::regclass::text collate "C"; +drop table p1 cascade; +drop role regress_check_owner; + +-- NOT VALID promotion skips the initial scan, but propagates enforcement to +-- all descendants so that new rows are checked everywhere. +create table p1(f1 int); +create table p1_c1() inherits(p1); +create table p1_c2() inherits(p1_c1); +alter table p1 add constraint inh_check_constraint check (f1 > 0) not enforced; +insert into p1_c2 values(-1); +alter table p1_c1 add constraint inh_check_constraint check (f1 > 0) not valid; +insert into p1_c2 values(-2); --error +select conrelid::regclass, coninhcount, conenforced, convalidated +from pg_constraint where conname = 'inh_check_constraint' +order by conrelid::regclass::text collate "C"; +drop table p1 cascade; + +-- A descendant already promoted with NOT VALID must still be scanned when an +-- ancestor is later promoted as valid. +create table p1(f1 int); +create table p1_c1() inherits(p1); +create table p1_c2() inherits(p1_c1); +alter table p1 add constraint inh_check_constraint check (f1 > 0) not enforced; +insert into p1_c2 values(-1); +alter table p1_c2 add constraint inh_check_constraint check (f1 > 0) not valid; +alter table p1_c1 add constraint inh_check_constraint check (f1 > 0); --error +delete from p1_c2; +alter table p1_c1 add constraint inh_check_constraint check (f1 > 0); --ok +select conrelid::regclass, coninhcount, conenforced, convalidated +from pg_constraint where conname = 'inh_check_constraint' +order by conrelid::regclass::text collate "C"; +drop table p1 cascade; + +-- Multiple inherited definitions remain merged while a local promotion scans +-- the relation. +create table p1(f1 int); +create table p1_other(f1 int); +alter table p1 add constraint inh_check_constraint check (f1 > 0) not enforced; +alter table p1_other add constraint inh_check_constraint check (f1 > 0) not enforced; +create table p1_c1() inherits(p1, p1_other); +insert into p1_c1 values(-1); +alter table p1_c1 add constraint inh_check_constraint check (f1 > 0); --error +delete from p1_c1; +alter table p1_c1 add constraint inh_check_constraint check (f1 > 0); --ok +select conrelid::regclass, coninhcount, conenforced, convalidated +from pg_constraint where conname = 'inh_check_constraint' +order by conrelid::regclass::text collate "C"; +drop table p1, p1_other cascade; + -- an inherited CHECK constraint cannot be NOT ENFORCED under an ENFORCED parent create table p1(f1 int constraint p1_a_check check (f1 > 0) enforced); create table p1_c1() inherits(p1); base-commit: 6bc236c8a5b198e7ffde80377777167f1d4b78bb -- 2.50.1 (Apple Git-155)