static bool ATExecAlterCheckConstrEnforceability(List **wqueue, ATAlterConstraint *cmdcon,
Relation conrel, HeapTuple contuple,
bool recurse, bool recursing,
+ List *changing_conids,
LOCKMODE lockmode);
static bool ATExecAlterConstrDeferrability(List **wqueue, ATAlterConstraint *cmdcon,
Relation conrel, Relation tgrel, Relation rel,
static void AlterCheckConstrEnforceabilityRecurse(List **wqueue, ATAlterConstraint *cmdcon,
Relation conrel, Oid conrelid,
bool recurse, bool recursing,
+ List *changing_conids,
LOCKMODE lockmode);
static void AlterConstrDeferrabilityRecurse(List **wqueue, ATAlterConstraint *cmdcon,
Relation conrel, Relation tgrel, Relation rel,
List **otherrelids, LOCKMODE lockmode);
static void AlterConstrUpdateConstraintEntry(ATAlterConstraint *cmdcon, Relation conrel,
HeapTuple contuple);
+static bool ATCheckCheckConstrHasEnforcedParent(Relation conrel, Relation rel,
+ HeapTuple contuple,
+ List *changing_conids,
+ Oid *enforced_parentoid);
static ObjectAddress ATExecValidateConstraint(List **wqueue,
Relation rel, char *constrName,
bool recurse, bool recursing, LOCKMODE lockmode);
static void QueueNNConstraintValidation(List **wqueue, Relation conrel, Relation rel,
HeapTuple contuple, bool recurse, bool recursing,
LOCKMODE lockmode);
+static bool constraints_equivalent(HeapTuple a, HeapTuple b, TupleDesc tupleDesc);
static int transformColumnNameList(Oid relId, List *colList,
int16 *attnums, Oid *atttypids, Oid *attcollids);
static int transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid,
else if (currcon->contype == CONSTRAINT_CHECK)
changed = ATExecAlterCheckConstrEnforceability(wqueue, cmdcon, conrel,
contuple, recurse, false,
- lockmode);
+ NIL, lockmode);
}
else if (cmdcon->alterDeferrability &&
ATExecAlterConstrDeferrability(wqueue, cmdcon, conrel, tgrel, rel,
static bool
ATExecAlterCheckConstrEnforceability(List **wqueue, ATAlterConstraint *cmdcon,
Relation conrel, HeapTuple contuple,
- bool recurse, bool recursing, LOCKMODE lockmode)
+ bool recurse, bool recursing,
+ List *changing_conids,
+ LOCKMODE lockmode)
{
Form_pg_constraint currcon;
Relation rel;
bool changed = false;
List *children = NIL;
+ bool target_enforced = cmdcon->is_enforced;
+ Oid enforced_parentoid = InvalidOid;
/* Since this function recurses, it could be driven to stack overflow */
check_stack_depth();
*/
rel = table_open(currcon->conrelid, NoLock);
- if (currcon->conenforced != cmdcon->is_enforced)
+ /*
+ * When setting a constraint to NOT ENFORCED, check whether any matching
+ * parent constraint remains ENFORCED and is not part of this ALTER.
+ *
+ * For a direct ALTER of an inherited constraint, reject the command,
+ * because the child cannot be weakened while its parent remains enforced.
+ *
+ * During recursion, another parent outside this ALTER may still enforce
+ * the same constraint in a regular inheritance hierarchy. In that case,
+ * keep the child constraint ENFORCED so that its merged enforceability
+ * still reflects the remaining enforced parent. Partitions do not need
+ * this recursive parent check because a partition can have only one
+ * direct parent.
+ */
+ if (!cmdcon->is_enforced &&
+ (!recursing || !rel->rd_rel->relispartition) &&
+ ATCheckCheckConstrHasEnforcedParent(conrel, rel, contuple,
+ changing_conids,
+ &enforced_parentoid))
{
- AlterConstrUpdateConstraintEntry(cmdcon, conrel, contuple);
+ if (!recursing)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("cannot mark inherited constraint \"%s\" as %s",
+ NameStr(currcon->conname),
+ "NOT ENFORCED"),
+ errdetail("The matching constraint on parent table \"%s\" is %s.",
+ get_rel_name(enforced_parentoid), "ENFORCED"));
+
+ target_enforced = true;
+ }
+
+ /*
+ * Update to the merged enforceability if needed. This may differ from the
+ * requested enforceability when another matching parent constraint
+ * remains enforced.
+ */
+ if (currcon->conenforced != target_enforced)
+ {
+ ATAlterConstraint updatecon = *cmdcon;
+
+ updatecon.is_enforced = target_enforced;
+ AlterConstrUpdateConstraintEntry(&updatecon, conrel, contuple);
changed = true;
}
/*
* Note that we must recurse even when trying to change a check constraint
* to not enforced if it is already not enforced, in case descendant
- * constraints might be enforced and need to be changed to not enforced.
+ * constraints might be enforced and need to be changed to not enforced,
+ * 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.
* try to look for it in the children.
*/
if (!recursing && !currcon->connoinherit)
+ {
+ Assert(changing_conids == NIL);
+
children = find_all_inheritors(RelationGetRelid(rel),
lockmode, NULL);
+ /*
+ * When setting NOT ENFORCED, build the set of equivalent CHECK
+ * constraints that this command will attempt to change before
+ * visiting descendants. The root itself has already been checked
+ * above.
+ */
+ if (!cmdcon->is_enforced)
+ changing_conids = list_make1_oid(currcon->oid);
+
+ foreach_oid(childoid, children)
+ {
+ if (childoid == RelationGetRelid(rel))
+ continue;
+
+ /*
+ * If we are told not to recurse, there had better not be any
+ * child tables, because we can't change constraint
+ * enforceability on the parent unless we have changed
+ * enforceability for all child.
+ */
+ if (!recurse)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+ errmsg("constraint must be altered on child tables too"),
+ errhint("Do not specify the ONLY keyword."));
+
+ /*
+ * It is sufficient to look up the constraint by name here.
+ * Supported DDL ensures that inheritable CHECK constraints
+ * with the same name have equivalent definitions when they
+ * are propagated to children or when inheritance is
+ * established. All descendants returned by
+ * find_all_inheritors must have this constraint: inherited
+ * CHECK constraints propagate to all children at
+ * inheritance-link creation time and cannot be dropped
+ * independently on child tables.
+ */
+ if (!cmdcon->is_enforced)
+ changing_conids =
+ list_append_unique_oid(changing_conids,
+ get_relation_constraint_oid(childoid,
+ cmdcon->conname,
+ false));
+ }
+ }
+
foreach_oid(childoid, children)
{
if (childoid == RelationGetRelid(rel))
continue;
- /*
- * If we are told not to recurse, there had better not be any
- * child tables, because we can't change constraint enforceability
- * on the parent unless we have changed enforceability for all
- * child.
- */
- if (!recurse)
- ereport(ERROR,
- errcode(ERRCODE_INVALID_TABLE_DEFINITION),
- errmsg("constraint must be altered on child tables too"),
- errhint("Do not specify the ONLY keyword."));
-
AlterCheckConstrEnforceabilityRecurse(wqueue, cmdcon, conrel,
childoid, false, true,
+ changing_conids,
lockmode);
}
}
*/
if (rel->rd_rel->relkind == RELKIND_RELATION &&
!currcon->conenforced &&
- cmdcon->is_enforced)
+ target_enforced)
{
AlteredTableInfo *tab;
NewConstraint *newcon;
AlterCheckConstrEnforceabilityRecurse(List **wqueue, ATAlterConstraint *cmdcon,
Relation conrel, Oid conrelid,
bool recurse, bool recursing,
+ List *changing_conids,
LOCKMODE lockmode)
{
SysScanDesc pscan;
cmdcon->conname, get_rel_name(conrelid)));
ATExecAlterCheckConstrEnforceability(wqueue, cmdcon, conrel, childtup,
- recurse, recursing, lockmode);
+ recurse, recursing, changing_conids,
+ lockmode);
systable_endscan(pscan);
}
+/*
+ * When setting an inherited CHECK constraint to NOT ENFORCED, look for a
+ * matching parent constraint that remains ENFORCED and is not part of the same
+ * ALTER.
+ */
+static bool
+ATCheckCheckConstrHasEnforcedParent(Relation conrel, Relation rel,
+ HeapTuple contuple,
+ List *changing_conids,
+ Oid *enforced_parentoid)
+{
+ Form_pg_constraint currcon;
+ Relation inhrel;
+ SysScanDesc scan;
+ ScanKeyData skey;
+ HeapTuple inheritsTuple;
+
+ /* Since this function recurses, it could be driven to stack overflow */
+ check_stack_depth();
+
+ currcon = (Form_pg_constraint) GETSTRUCT(contuple);
+ Assert(currcon->contype == CONSTRAINT_CHECK);
+
+ if (currcon->coninhcount <= 0)
+ return false;
+
+ inhrel = table_open(InheritsRelationId, AccessShareLock);
+
+ ScanKeyInit(&skey,
+ Anum_pg_inherits_inhrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(RelationGetRelid(rel)));
+ scan = systable_beginscan(inhrel, InheritsRelidSeqnoIndexId,
+ true, NULL, 1, &skey);
+
+ while (HeapTupleIsValid(inheritsTuple = systable_getnext(scan)))
+ {
+ Oid parentoid;
+ Relation parentrel = NULL;
+ SysScanDesc pscan;
+ ScanKeyData pkey[3];
+ HeapTuple parenttup;
+
+ parentoid = ((Form_pg_inherits) GETSTRUCT(inheritsTuple))->inhparent;
+
+ ScanKeyInit(&pkey[0],
+ Anum_pg_constraint_conrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(parentoid));
+ ScanKeyInit(&pkey[1],
+ Anum_pg_constraint_contypid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(InvalidOid));
+ ScanKeyInit(&pkey[2],
+ Anum_pg_constraint_conname,
+ BTEqualStrategyNumber, F_NAMEEQ,
+ NameGetDatum(&currcon->conname));
+
+ pscan = systable_beginscan(conrel, ConstraintRelidTypidNameIndexId,
+ true, NULL, 3, pkey);
+
+ /*
+ * ConstraintRelidTypidNameIndexId is unique on (conrelid, contypid,
+ * conname), so this loop body executes at most once per parent.
+ */
+ while (HeapTupleIsValid(parenttup = systable_getnext(pscan)))
+ {
+ Form_pg_constraint parentcon;
+
+ parentcon = (Form_pg_constraint) GETSTRUCT(parenttup);
+
+ if (parentcon->contype != CONSTRAINT_CHECK ||
+ parentcon->connoinherit ||
+ !parentcon->conenforced)
+ continue;
+
+ if (!constraints_equivalent(parenttup, contuple,
+ RelationGetDescr(conrel)))
+ elog(ERROR, "child table \"%s\" has different definition for check constraint \"%s\"",
+ RelationGetRelationName(rel),
+ NameStr(parentcon->conname));
+
+ /*
+ * A parent listed in changing_conids is being changed by the same
+ * ALTER, but it may not have been updated yet. For regular
+ * inheritance, recurse upward to check whether an equivalent
+ * enforced parent outside the ALTER will make it remain enforced.
+ * Partitions cannot have multiple parents, so they do not need
+ * this check.
+ */
+ if (!rel->rd_rel->relispartition &&
+ list_member_oid(changing_conids, parentcon->oid))
+ {
+ Oid parent_enforced_parentoid = InvalidOid;
+
+ if (parentrel == NULL)
+ parentrel = table_open(parentoid, NoLock);
+
+ if (!ATCheckCheckConstrHasEnforcedParent(conrel,
+ parentrel,
+ parenttup,
+ changing_conids,
+ &parent_enforced_parentoid))
+ continue;
+ }
+
+ *enforced_parentoid = parentoid;
+ if (parentrel != NULL)
+ table_close(parentrel, NoLock);
+ systable_endscan(pscan);
+ systable_endscan(scan);
+ table_close(inhrel, AccessShareLock);
+ return true;
+ }
+
+ if (parentrel != NULL)
+ table_close(parentrel, NoLock);
+ systable_endscan(pscan);
+ }
+
+ systable_endscan(scan);
+ table_close(inhrel, AccessShareLock);
+
+ return false;
+}
+
/*
* Returns true if the constraint's deferrability is altered.
*
DETAIL: drop cascades to table p1_c1
drop cascades to table p1_c2
drop cascades to table p1_c3
+-- 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);
+alter table p1_c1 alter constraint p1_a_check not enforced; --error
+ERROR: cannot mark inherited constraint "p1_a_check" as NOT ENFORCED
+DETAIL: The matching constraint on parent table "p1" is ENFORCED.
+alter table p1 alter constraint p1_a_check not enforced; --ok
+alter table p1_c1 alter constraint p1_a_check not enforced; --ok
+drop table p1 cascade;
+NOTICE: drop cascades to table p1_c1
+-- recursive NOT ENFORCED merges with ENFORCED constraints from other parents
+create table p1(a int constraint p1_a_check check (a > 0) enforced);
+create table p2(a int constraint p1_a_check check (a > 0) enforced);
+create table p1_c1() inherits (p1, p2);
+NOTICE: merging multiple inherited definitions of column "a"
+-- make p1_c1_g2's oid smaller than p1_c1_g1's, to ensure ordering of
+-- pg_constraint rows to not impact the test results.
+create table p1_c1_g2() inherits (p1_c1);
+create table p1_c1_g1() inherits (p1_c1);
+alter table p1_c1_g2 inherit p1_c1_g1;
+create table p1_c2() inherits (p1);
+alter table p1 alter constraint p1_a_check not enforced; --ok
+select conname, conenforced, convalidated, conrelid::regclass
+from pg_constraint
+where conname = 'p1_a_check' and contype = 'c'
+order by conrelid::regclass::text collate "C";
+ conname | conenforced | convalidated | conrelid
+------------+-------------+--------------+----------
+ p1_a_check | f | f | p1
+ p1_a_check | t | t | p1_c1
+ p1_a_check | t | t | p1_c1_g1
+ p1_a_check | t | t | p1_c1_g2
+ p1_a_check | f | f | p1_c2
+ p1_a_check | t | t | p2
+(6 rows)
+
+alter table p1_c1 alter constraint p1_a_check not enforced; --error
+ERROR: cannot mark inherited constraint "p1_a_check" as NOT ENFORCED
+DETAIL: The matching constraint on parent table "p2" is ENFORCED.
+alter table p2 alter constraint p1_a_check not enforced; --ok
+alter table p1_c1 alter constraint p1_a_check not enforced; --ok
+drop table p1, p2 cascade;
+NOTICE: drop cascades to 4 other objects
+DETAIL: drop cascades to table p1_c1
+drop cascades to table p1_c1_g1
+drop cascades to table p1_c1_g2
+drop cascades to table p1_c2
+-- recursive NOT ENFORCED can change all matching enforced parents together
+create table gp(a int constraint gp_a_check check (a > 0) enforced);
+create table p1() inherits (gp);
+create table p2() inherits (gp);
+create table p1_c1() inherits (p1, p2);
+NOTICE: merging multiple inherited definitions of column "a"
+alter table gp alter constraint gp_a_check not enforced; --ok
+select conname, conenforced, convalidated, conrelid::regclass
+from pg_constraint
+where conname = 'gp_a_check' and contype = 'c'
+order by conrelid::regclass::text collate "C";
+ conname | conenforced | convalidated | conrelid
+------------+-------------+--------------+----------
+ gp_a_check | f | f | gp
+ gp_a_check | f | f | p1
+ gp_a_check | f | f | p1_c1
+ gp_a_check | f | f | p2
+(4 rows)
+
+drop table gp cascade;
+NOTICE: drop cascades to 3 other objects
+DETAIL: drop cascades to table p1
+drop cascades to table p2
+drop cascades to table p1_c1
+-- recursive NOT ENFORCED can change a direct-plus-indirect diamond together
+create table gp(a int constraint gp_a_check check (a > 0) enforced);
+create table p1_c1() inherits (gp);
+create table p1() inherits (gp);
+alter table p1_c1 inherit p1;
+alter table gp alter constraint gp_a_check not enforced; --ok
+select conname, conenforced, convalidated, conrelid::regclass
+from pg_constraint
+where conname = 'gp_a_check' and contype = 'c'
+order by conrelid::regclass::text collate "C";
+ conname | conenforced | convalidated | conrelid
+------------+-------------+--------------+----------
+ gp_a_check | f | f | gp
+ gp_a_check | f | f | p1
+ gp_a_check | f | f | p1_c1
+(3 rows)
+
+drop table gp cascade;
+NOTICE: drop cascades to 2 other objects
+DETAIL: drop cascades to table p1
+drop cascades to table p1_c1
--for "no inherit" check constraint, it will not recurse to child table
create table p1(f1 int constraint p1_a_check check (f1 > 0) no inherit not enforced);
create table p1_c1(f1 int constraint p1_a_check check (f1 > 0) not enforced);
order by conrelid::regclass::text collate "C";
drop table p1 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);
+alter table p1_c1 alter constraint p1_a_check not enforced; --error
+alter table p1 alter constraint p1_a_check not enforced; --ok
+alter table p1_c1 alter constraint p1_a_check not enforced; --ok
+drop table p1 cascade;
+
+-- recursive NOT ENFORCED merges with ENFORCED constraints from other parents
+create table p1(a int constraint p1_a_check check (a > 0) enforced);
+create table p2(a int constraint p1_a_check check (a > 0) enforced);
+create table p1_c1() inherits (p1, p2);
+-- make p1_c1_g2's oid smaller than p1_c1_g1's, to ensure ordering of
+-- pg_constraint rows to not impact the test results.
+create table p1_c1_g2() inherits (p1_c1);
+create table p1_c1_g1() inherits (p1_c1);
+alter table p1_c1_g2 inherit p1_c1_g1;
+create table p1_c2() inherits (p1);
+alter table p1 alter constraint p1_a_check not enforced; --ok
+select conname, conenforced, convalidated, conrelid::regclass
+from pg_constraint
+where conname = 'p1_a_check' and contype = 'c'
+order by conrelid::regclass::text collate "C";
+alter table p1_c1 alter constraint p1_a_check not enforced; --error
+alter table p2 alter constraint p1_a_check not enforced; --ok
+alter table p1_c1 alter constraint p1_a_check not enforced; --ok
+drop table p1, p2 cascade;
+
+-- recursive NOT ENFORCED can change all matching enforced parents together
+create table gp(a int constraint gp_a_check check (a > 0) enforced);
+create table p1() inherits (gp);
+create table p2() inherits (gp);
+create table p1_c1() inherits (p1, p2);
+alter table gp alter constraint gp_a_check not enforced; --ok
+select conname, conenforced, convalidated, conrelid::regclass
+from pg_constraint
+where conname = 'gp_a_check' and contype = 'c'
+order by conrelid::regclass::text collate "C";
+drop table gp cascade;
+
+-- recursive NOT ENFORCED can change a direct-plus-indirect diamond together
+create table gp(a int constraint gp_a_check check (a > 0) enforced);
+create table p1_c1() inherits (gp);
+create table p1() inherits (gp);
+alter table p1_c1 inherit p1;
+alter table gp alter constraint gp_a_check not enforced; --ok
+select conname, conenforced, convalidated, conrelid::regclass
+from pg_constraint
+where conname = 'gp_a_check' and contype = 'c'
+order by conrelid::regclass::text collate "C";
+drop table gp cascade;
+
--for "no inherit" check constraint, it will not recurse to child table
create table p1(f1 int constraint p1_a_check check (f1 > 0) no inherit not enforced);
create table p1_c1(f1 int constraint p1_a_check check (f1 > 0) not enforced);