NOT ENFORCED constraint feature
Hi,
In SQL Standard 2023, a table's check or referential constraint can be
either ENFORCED or NOT ENFORCED.
Currently, when a DML statement is executed, all enforced constraints
are validated. If any constraint is violated, an exception is raised,
and the SQL transaction is rolled back. These are referred to as
ENFORCED constraints.
On the other hand, a NOT ENFORCED constraint is a rule defined in the
database but not checked when data is inserted or updated. This can
help speed up large data imports, improve performance when strict
validation isn't required, or handle cases where constraints are
enforced externally (e.g., by application logic). It also allows the
rule to be documented without enforcing it during normal operations.
The attached patch proposes adding the ability to define CHECK and
FOREIGN KEY constraints as NOT ENFORCED. If neither ENFORCED nor
NOT ENFORCED is explicitly specified when defining a constraint, the
default setting is that the constraint is ENFORCED. Note that this
addition differs from the properties of NOT VALID and DEFERRABLE
constraints, which skip checks only for existing data and determine
when to perform checks, respectively. In contrast, NOT ENFORCED
completely skips the checks altogether.
Adding NOT ENFORCED to CHECK constraints is simple, see 0001 patch,
but implementing it for FOREIGN KEY constraints requires more code due
to triggers, see 0002 - 0005 patches. There are various approaches for
implementing NOT ENFORCED foreign keys, what I thought of:
1. When defining a NOT ENFORCED foreign key, skip the creation of
triggers used for referential integrity check, while defining an
ENFORCED foreign key, remain the same as the current behaviour. If an
existing foreign key is changed to NOT ENFORCED, the triggers are
dropped, and when switching it back to ENFORCED, the triggers are
recreated.
2. Another approach could be to create the NOT ENFORCED constraint
with the triggers as usual, but disable those triggers by updating the
pg_trigger catalog so that they are never executed for the check. And
enable them when the constraint is changed back to ENFORCED.
3. Similarly, a final approach would involve updating the logic where
trigger execution is decided and skipping the execution if the
constraint is not enforced, rather than modifying the pg_trigger
catalog.
In the attached patch, the first approach has been implemented. This
requires more code changes but prevents unused triggers from being
left in the database and avoids the need for changes all over the
place to skip trigger execution, which could be missed in future code
additions.
The ALTER CONSTRAINT operation in the patch added code to handle
dropping and recreating triggers. An alternative approach would be to
simplify the process by dropping and recreating the FK constraint,
which would automatically handle skipping or creating triggers for NOT
ENFORCED or ENFORCED FK constraints. However, I wasn't sure if this
was the right approach, as I couldn't find any existing ALTER
operations that follow this pattern.
Also note that the existing CHECK constraints currently do not support
ALTER operations. This functionality may be essential for modifying a
constraint's enforcement status; otherwise, users must drop and
recreate the CHECK constraint to change its enforceability. I have not
yet begun work on this, as it would involve significant code
refactoring and updates to the documentation. I plan to start this
once we finalise the design and reach a common understanding regarding
this proposal.
Any comments, suggestions, or assistance would be greatly appreciated.
Thank you.
--
Regards,
Amul Sul
EDB: http://www.enterprisedb.com
Attachments:
v1-0001-Add-support-for-NOT-ENFORCED-in-CHECK-constraints.patchapplication/x-patch; name=v1-0001-Add-support-for-NOT-ENFORCED-in-CHECK-constraints.patchDownload
From 75d25c63d0b10d6439ade792899d6adb8f99d660 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Fri, 4 Oct 2024 18:22:42 +0530
Subject: [PATCH v1 1/5] Add support for NOT ENFORCED in CHECK constraints.
This added basic infrastructure for the NOT ENFORCED/ENFORCED
constraint supports where incluing grammar and catalog flags.
Note that CHECK constraints do not currently support ALTER operations,
so changing the enforceability of an existing constraint isn't
possible without dropping and recreating it. We could consider
adding support for altering CHECK constraints either in this
patch series or as a separatly.
---
doc/src/sgml/ddl.sgml | 8 ++-
doc/src/sgml/ref/alter_table.sgml | 14 +++--
doc/src/sgml/ref/create_table.sgml | 18 +++++-
src/backend/access/common/tupdesc.c | 4 +-
src/backend/catalog/heap.c | 13 ++--
src/backend/catalog/index.c | 1 +
src/backend/catalog/pg_constraint.c | 5 ++
src/backend/commands/tablecmds.c | 56 ++++++++++++------
src/backend/commands/trigger.c | 1 +
src/backend/commands/typecmds.c | 14 +++++
src/backend/executor/execMain.c | 8 ++-
src/backend/parser/gram.y | 72 ++++++++++++++++++-----
src/backend/parser/parse_utilcmd.c | 39 ++++++++++++
src/backend/utils/adt/ruleutils.c | 7 +++
src/backend/utils/cache/relcache.c | 1 +
src/include/access/tupdesc.h | 1 +
src/include/catalog/heap.h | 1 +
src/include/catalog/pg_constraint.h | 2 +
src/include/nodes/parsenodes.h | 3 +
src/include/parser/kwlist.h | 1 +
src/test/regress/expected/constraints.out | 22 ++++++-
src/test/regress/sql/constraints.sql | 15 ++++-
22 files changed, 253 insertions(+), 53 deletions(-)
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index 8ab0ddb112f..892a74346ca 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -581,7 +581,10 @@ CREATE TABLE products (
consists of the key word <literal>CHECK</literal> followed by an
expression in parentheses. The check constraint expression should
involve the column thus constrained, otherwise the constraint
- would not make too much sense.
+ would not make too much sense. The check constraint expression can be
+ followed by the <literal>ENFORCED</literal>, which is the default, or
+ <literal>NOT ENFORCED</literal> keyword, determining whether the
+ constraint check is performed or skipped after each statement.
</para>
<indexterm>
@@ -717,7 +720,8 @@ CREATE TABLE products (
to implement that. (This approach avoids the dump/restore problem because
<application>pg_dump</application> does not reinstall triggers until after
restoring data, so that the check will not be enforced during a
- dump/restore.)
+ dump/restore.) You can add that CHECK constraint with <literal>NOT ENFORCED</literal>,
+ allowing the underlying constraint rule to be represented without being enforced.
</para>
</note>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 36770c012a6..257f871d99e 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -58,7 +58,7 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable>
ALTER [ COLUMN ] <replaceable class="parameter">column_name</replaceable> SET COMPRESSION <replaceable class="parameter">compression_method</replaceable>
ADD <replaceable class="parameter">table_constraint</replaceable> [ NOT VALID ]
ADD <replaceable class="parameter">table_constraint_using_index</replaceable>
- ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+ ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]
VALIDATE CONSTRAINT <replaceable class="parameter">constraint_name</replaceable>
DROP CONSTRAINT [ IF EXISTS ] <replaceable class="parameter">constraint_name</replaceable> [ RESTRICT | CASCADE ]
DISABLE TRIGGER [ <replaceable class="parameter">trigger_name</replaceable> | ALL | USER ]
@@ -108,7 +108,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
PRIMARY KEY <replaceable class="parameter">index_parameters</replaceable> |
REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> ) ] [ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ]
[ ON DELETE <replaceable class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ] }
-[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]
<phrase>and <replaceable class="parameter">table_constraint</replaceable> is:</phrase>
@@ -119,7 +119,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
EXCLUDE [ USING <replaceable class="parameter">index_method</replaceable> ] ( <replaceable class="parameter">exclude_element</replaceable> WITH <replaceable class="parameter">operator</replaceable> [, ... ] ) <replaceable class="parameter">index_parameters</replaceable> [ WHERE ( <replaceable class="parameter">predicate</replaceable> ) ] |
FOREIGN KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> [, ... ] ) ]
[ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ] [ ON DELETE <replaceable class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ] }
-[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]
<phrase>and <replaceable class="parameter">table_constraint_using_index</replaceable> is:</phrase>
@@ -1427,9 +1427,11 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</para>
<para>
- Adding a <literal>CHECK</literal> or <literal>NOT NULL</literal> constraint requires
- scanning the table to verify that existing rows meet the constraint,
- but does not require a table rewrite.
+ Adding a enforced <literal>CHECK</literal> or <literal>NOT NULL</literal>
+ constraint requires scanning the table to verify that existing rows meet the
+ constraint, but does not require a table rewrite. If a <literal>CHECK</literal>
+ constraint is added as <literal>NOT ENFORCED</literal>, the validation will
+ not be performed.
</para>
<para>
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index 83859bac76f..55b51602136 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -71,7 +71,7 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI
PRIMARY KEY <replaceable class="parameter">index_parameters</replaceable> |
REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> ) ] [ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ]
[ ON DELETE <replaceable class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ] }
-[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]
<phrase>and <replaceable class="parameter">table_constraint</replaceable> is:</phrase>
@@ -83,7 +83,7 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI
FOREIGN KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">column_name</replaceable> ] ) REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">column_name</replaceable> ] ) ]
[ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ] [ ON DELETE <replaceable
class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ] }
-[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]
<phrase>and <replaceable class="parameter">like_option</replaceable> is:</phrase>
@@ -1362,6 +1362,20 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</listitem>
</varlistentry>
+ <varlistentry id="sql-createtable-parms-enforce">
+ <term><literal>ENFORCED</literal></term>
+ <term><literal>NOT ENFORCED</literal></term>
+ <listitem>
+ <para>
+ This is currently only allowed for <literal>CHECK</literal> constraints.
+ If the constraint is <literal>NOT ENFORCED</literal>, this clause
+ specifies that the constraint check will be skipped. When the constraint
+ is <literal>ENFORCED</literal>, check is performed after each statement.
+ This is the default.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="sql-createtable-method">
<term><literal>USING <replaceable class="parameter">method</replaceable></literal></term>
<listitem>
diff --git a/src/backend/access/common/tupdesc.c b/src/backend/access/common/tupdesc.c
index 47379fef220..41dd00e5547 100644
--- a/src/backend/access/common/tupdesc.c
+++ b/src/backend/access/common/tupdesc.c
@@ -226,6 +226,7 @@ CreateTupleDescCopyConstr(TupleDesc tupdesc)
cpy->check[i].ccbin = pstrdup(constr->check[i].ccbin);
cpy->check[i].ccvalid = constr->check[i].ccvalid;
cpy->check[i].ccnoinherit = constr->check[i].ccnoinherit;
+ cpy->check[i].ccenforced = constr->check[i].ccenforced;
}
}
@@ -548,7 +549,8 @@ equalTupleDescs(TupleDesc tupdesc1, TupleDesc tupdesc2)
if (!(strcmp(check1->ccname, check2->ccname) == 0 &&
strcmp(check1->ccbin, check2->ccbin) == 0 &&
check1->ccvalid == check2->ccvalid &&
- check1->ccnoinherit == check2->ccnoinherit))
+ check1->ccnoinherit == check2->ccnoinherit &&
+ check1->ccenforced == check2->ccenforced))
return false;
}
}
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 78e59384d1c..7aa8cb8055a 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -103,7 +103,8 @@ static ObjectAddress AddNewRelationType(const char *typeName,
static void RelationRemoveInheritance(Oid relid);
static Oid StoreRelCheck(Relation rel, const char *ccname, Node *expr,
bool is_validated, bool is_local, int inhcount,
- bool is_no_inherit, bool is_internal);
+ bool is_no_inherit, bool is_enforced,
+ bool is_internal);
static void StoreConstraints(Relation rel, List *cooked_constraints,
bool is_internal);
static bool MergeWithExistingConstraint(Relation rel, const char *ccname, Node *expr,
@@ -2073,7 +2074,7 @@ SetAttrMissing(Oid relid, char *attname, char *value)
static Oid
StoreRelCheck(Relation rel, const char *ccname, Node *expr,
bool is_validated, bool is_local, int inhcount,
- bool is_no_inherit, bool is_internal)
+ bool is_no_inherit, bool is_enforced, bool is_internal)
{
char *ccbin;
List *varList;
@@ -2139,6 +2140,7 @@ StoreRelCheck(Relation rel, const char *ccname, Node *expr,
false, /* Is Deferrable */
false, /* Is Deferred */
is_validated,
+ is_enforced, /* Is enforced */
InvalidOid, /* no parent constraint */
RelationGetRelid(rel), /* relation */
attNos, /* attrs in the constraint */
@@ -2212,7 +2214,7 @@ StoreConstraints(Relation rel, List *cooked_constraints, bool is_internal)
StoreRelCheck(rel, con->name, con->expr,
!con->skip_validation, con->is_local,
con->inhcount, con->is_no_inherit,
- is_internal);
+ con->is_enforced, is_internal);
numchecks++;
break;
default:
@@ -2345,6 +2347,7 @@ AddRelationNewConstraints(Relation rel,
cooked->attnum = colDef->attnum;
cooked->expr = expr;
cooked->skip_validation = false;
+ cooked->is_enforced = true;
cooked->is_local = is_local;
cooked->inhcount = is_local ? 0 : 1;
cooked->is_no_inherit = false;
@@ -2463,7 +2466,8 @@ AddRelationNewConstraints(Relation rel,
*/
constrOid =
StoreRelCheck(rel, ccname, expr, cdef->initially_valid, is_local,
- is_local ? 0 : 1, cdef->is_no_inherit, is_internal);
+ is_local ? 0 : 1, cdef->is_no_inherit,
+ cdef->is_enforced, is_internal);
numchecks++;
@@ -2474,6 +2478,7 @@ AddRelationNewConstraints(Relation rel,
cooked->attnum = 0;
cooked->expr = expr;
cooked->skip_validation = cdef->skip_validation;
+ cooked->is_enforced = cdef->is_enforced;
cooked->is_local = is_local;
cooked->inhcount = is_local ? 0 : 1;
cooked->is_no_inherit = cdef->is_no_inherit;
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 6084dfa97cb..edef21e88ab 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -1958,6 +1958,7 @@ index_constraint_create(Relation heapRelation,
deferrable,
initdeferred,
true,
+ true, /* Is enforced */
parentConstraintId,
RelationGetRelid(heapRelation),
indexInfo->ii_IndexAttrNumbers,
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index 1e2df031a84..14468b2b7d0 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -52,6 +52,7 @@ CreateConstraintEntry(const char *constraintName,
bool isDeferrable,
bool isDeferred,
bool isValidated,
+ bool isEnforced,
Oid parentConstrId,
Oid relId,
const int16 *constraintKey,
@@ -97,6 +98,9 @@ CreateConstraintEntry(const char *constraintName,
ObjectAddresses *addrs_auto;
ObjectAddresses *addrs_normal;
+ /* Only CHECK constraint can be not enforced */
+ Assert(isEnforced || constraintType == CONSTRAINT_CHECK);
+
conDesc = table_open(ConstraintRelationId, RowExclusiveLock);
Assert(constraintName);
@@ -181,6 +185,7 @@ CreateConstraintEntry(const char *constraintName,
values[Anum_pg_constraint_condeferrable - 1] = BoolGetDatum(isDeferrable);
values[Anum_pg_constraint_condeferred - 1] = BoolGetDatum(isDeferred);
values[Anum_pg_constraint_convalidated - 1] = BoolGetDatum(isValidated);
+ values[Anum_pg_constraint_conenforced - 1] = BoolGetDatum(isEnforced);
values[Anum_pg_constraint_conrelid - 1] = ObjectIdGetDatum(relId);
values[Anum_pg_constraint_contypid - 1] = ObjectIdGetDatum(domainId);
values[Anum_pg_constraint_conindid - 1] = ObjectIdGetDatum(indexRelId);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index af8c05b91f1..6f7123e5f71 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -363,7 +363,7 @@ static void RangeVarCallbackForTruncate(const RangeVar *relation,
Oid relId, Oid oldRelId, void *arg);
static List *MergeAttributes(List *columns, const List *supers, char relpersistence,
bool is_partition, List **supconstr);
-static List *MergeCheckConstraint(List *constraints, const char *name, Node *expr);
+static List *MergeCheckConstraint(List *constraints, const char *name, Node *expr, bool is_enforced);
static void MergeChildAttribute(List *inh_columns, int exist_attno, int newcol_attno, const ColumnDef *newdef);
static ColumnDef *MergeInheritedAttribute(List *inh_columns, int exist_attno, const ColumnDef *newdef);
static void MergeAttributesIntoExisting(Relation child_rel, Relation parent_rel, bool ispartition);
@@ -946,6 +946,7 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
cooked->attnum = attnum;
cooked->expr = colDef->cooked_default;
cooked->skip_validation = false;
+ cooked->is_enforced = true;
cooked->is_local = true; /* not used for defaults */
cooked->inhcount = 0; /* ditto */
cooked->is_no_inherit = false;
@@ -2824,7 +2825,8 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
name,
RelationGetRelationName(relation))));
- constraints = MergeCheckConstraint(constraints, name, expr);
+ constraints = MergeCheckConstraint(constraints, name, expr,
+ check[i].ccenforced);
}
}
@@ -3026,7 +3028,7 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
* the list.
*/
static List *
-MergeCheckConstraint(List *constraints, const char *name, Node *expr)
+MergeCheckConstraint(List *constraints, const char *name, Node *expr, bool is_enforced)
{
ListCell *lc;
CookedConstraint *newcon;
@@ -3067,6 +3069,7 @@ MergeCheckConstraint(List *constraints, const char *name, Node *expr)
newcon->name = pstrdup(name);
newcon->expr = expr;
newcon->inhcount = 1;
+ newcon->is_enforced = is_enforced;
return lappend(constraints, newcon);
}
@@ -9463,6 +9466,9 @@ ATAddCheckConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
{
CookedConstraint *ccon = (CookedConstraint *) lfirst(lcon);
+ /* Only CHECK constraint can be not enforced */
+ Assert(ccon->is_enforced || ccon->contype == CONSTRAINT_CHECK);
+
if (!ccon->skip_validation)
{
NewConstraint *newcon;
@@ -10226,6 +10232,7 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
fkconstraint->deferrable,
fkconstraint->initdeferred,
fkconstraint->initially_valid,
+ true, /* Is enforced */
parentConstr,
RelationGetRelid(rel),
fkattnum,
@@ -10528,6 +10535,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
fkconstraint->deferrable,
fkconstraint->initdeferred,
fkconstraint->initially_valid,
+ true, /* Is enforced */
parentConstr,
partitionId,
mapped_fkattnum,
@@ -11055,6 +11063,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
fkconstraint->deferrable,
fkconstraint->initdeferred,
constrForm->convalidated,
+ true, /* Is enforced */
parentConstrOid,
RelationGetRelid(partRel),
mapped_conkey,
@@ -11821,22 +11830,29 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
table_close(childrel, NoLock);
}
- /* Queue validation for phase 3 */
- newcon = (NewConstraint *) palloc0(sizeof(NewConstraint));
- newcon->name = constrName;
- newcon->contype = CONSTR_CHECK;
- newcon->refrelid = InvalidOid;
- newcon->refindid = InvalidOid;
- newcon->conid = con->oid;
+ /*
+ * Queue validation for phase 3 only if constraint is enforced;
+ * otherwise, adding it to the validation queue won't be very
+ * effective, as the verification will be skipped.
+ */
+ if (con->conenforced)
+ {
+ newcon = (NewConstraint *) palloc0(sizeof(NewConstraint));
+ newcon->name = constrName;
+ newcon->contype = CONSTR_CHECK;
+ newcon->refrelid = InvalidOid;
+ newcon->refindid = InvalidOid;
+ newcon->conid = con->oid;
- val = SysCacheGetAttrNotNull(CONSTROID, tuple,
- Anum_pg_constraint_conbin);
- conbin = TextDatumGetCString(val);
- newcon->qual = (Node *) stringToNode(conbin);
+ val = SysCacheGetAttrNotNull(CONSTROID, tuple,
+ Anum_pg_constraint_conbin);
+ conbin = TextDatumGetCString(val);
+ newcon->qual = (Node *) stringToNode(conbin);
- /* Find or create work queue entry for this table */
- tab = ATGetQueueEntry(wqueue, rel);
- tab->constraints = lappend(tab->constraints, newcon);
+ /* Find or create work queue entry for this table */
+ tab = ATGetQueueEntry(wqueue, rel);
+ tab->constraints = lappend(tab->constraints, newcon);
+ }
/*
* Invalidate relcache so that others see the new validated
@@ -11846,7 +11862,9 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
}
/*
- * Now update the catalog, while we have the door open.
+ * Now update the catalog regardless of enforcement; the validated
+ * flag will not take effect until the constraint is marked as
+ * enforced.
*/
copyTuple = heap_copytuple(tuple);
copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
@@ -15851,6 +15869,7 @@ constraints_equivalent(HeapTuple a, HeapTuple b, TupleDesc tupleDesc)
if (acon->condeferrable != bcon->condeferrable ||
acon->condeferred != bcon->condeferred ||
+ acon->conenforced != bcon->conenforced ||
strcmp(decompile_conbin(a, tupleDesc),
decompile_conbin(b, tupleDesc)) != 0)
return false;
@@ -19610,6 +19629,7 @@ DetachAddConstraintIfNeeded(List **wqueue, Relation partRel)
n->cooked_expr = nodeToString(make_ands_explicit(constraintExpr));
n->initially_valid = true;
n->skip_validation = true;
+ n->is_enforced = true;
/* It's a re-add, since it nominally already exists */
ATAddCheckConstraint(wqueue, tab, partRel, n,
true, false, true, ShareUpdateExclusiveLock);
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 3671e82535e..bce023e5329 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -810,6 +810,7 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
stmt->deferrable,
stmt->initdeferred,
true,
+ true, /* Is enforced */
InvalidOid, /* no parent */
RelationGetRelid(rel),
NULL, /* no conkey */
diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c
index 2a6550de907..e77fd2f140a 100644
--- a/src/backend/commands/typecmds.c
+++ b/src/backend/commands/typecmds.c
@@ -1007,6 +1007,12 @@ DefineDomain(CreateDomainStmt *stmt)
errmsg("specifying constraint deferrability not supported for domains")));
break;
+ case CONSTR_ATTR_NOT_ENFORCED:
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("specifying not enforced constraint not supported for domains")));
+ break;
+
default:
elog(ERROR, "unrecognized constraint subtype: %d",
(int) constr->contype);
@@ -2967,6 +2973,12 @@ AlterDomainAddConstraint(List *names, Node *newConstraint,
errmsg("specifying constraint deferrability not supported for domains")));
break;
+ case CONSTR_ATTR_NOT_ENFORCED:
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("specifying not enforced constraint not supported for domains")));
+ break;
+
default:
elog(ERROR, "unrecognized constraint subtype: %d",
(int) constr->contype);
@@ -3597,6 +3609,7 @@ domainAddCheckConstraint(Oid domainOid, Oid domainNamespace, Oid baseTypeOid,
false, /* Is Deferrable */
false, /* Is Deferred */
!constr->skip_validation, /* Is Validated */
+ true, /* Is enforced */
InvalidOid, /* no parent constraint */
InvalidOid, /* not a relation constraint */
NULL,
@@ -3704,6 +3717,7 @@ domainAddNotNullConstraint(Oid domainOid, Oid domainNamespace, Oid baseTypeOid,
false, /* Is Deferrable */
false, /* Is Deferred */
!constr->skip_validation, /* Is Validated */
+ true, /* Is enforced */
InvalidOid, /* no parent constraint */
InvalidOid, /* not a relation constraint */
NULL,
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index cc9a594cba5..cec69afe188 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1746,11 +1746,15 @@ ExecRelCheck(ResultRelInfo *resultRelInfo,
{
oldContext = MemoryContextSwitchTo(estate->es_query_cxt);
resultRelInfo->ri_ConstraintExprs =
- (ExprState **) palloc(ncheck * sizeof(ExprState *));
+ (ExprState **) palloc0(ncheck * sizeof(ExprState *));
for (i = 0; i < ncheck; i++)
{
Expr *checkconstr;
+ /* Skip not enforced constraint */
+ if (!check[i].ccenforced)
+ continue;
+
checkconstr = stringToNode(check[i].ccbin);
resultRelInfo->ri_ConstraintExprs[i] =
ExecPrepareExpr(checkconstr, estate);
@@ -1777,7 +1781,7 @@ ExecRelCheck(ResultRelInfo *resultRelInfo,
* is not to be treated as a failure. Therefore, use ExecCheck not
* ExecQual.
*/
- if (!ExecCheck(checkconstr, econtext))
+ if (checkconstr && !ExecCheck(checkconstr, econtext))
return check[i].ccname;
}
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 4aa8646af7b..437a0b9f2f0 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -158,6 +158,8 @@ typedef struct KeyActions
#define CAS_INITIALLY_DEFERRED 0x08
#define CAS_NOT_VALID 0x10
#define CAS_NO_INHERIT 0x20
+#define CAS_NOT_ENFORCED 0x40
+#define CAS_ENFORCED 0x80
#define parser_yyerror(msg) scanner_yyerror(msg, yyscanner)
@@ -211,7 +213,7 @@ static void SplitColQualList(List *qualList,
core_yyscan_t yyscanner);
static void processCASbits(int cas_bits, int location, const char *constrType,
bool *deferrable, bool *initdeferred, bool *not_valid,
- bool *no_inherit, core_yyscan_t yyscanner);
+ bool *no_inherit, bool *is_enforced, core_yyscan_t yyscanner);
static PartitionStrategy parsePartitionStrategy(char *strategy);
static void preprocess_pubobj_list(List *pubobjspec_list,
core_yyscan_t yyscanner);
@@ -724,9 +726,9 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
DETACH DICTIONARY DISABLE_P DISCARD DISTINCT DO DOCUMENT_P DOMAIN_P
DOUBLE_P DROP
- EACH ELSE EMPTY_P ENABLE_P ENCODING ENCRYPTED END_P ENUM_P ERROR_P ESCAPE
- EVENT EXCEPT EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION
- EXTENSION EXTERNAL EXTRACT
+ EACH ELSE EMPTY_P ENABLE_P ENCODING ENCRYPTED END_P ENFORCED ENUM_P ERROR_P
+ ESCAPE EVENT EXCEPT EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN
+ EXPRESSION EXTENSION EXTERNAL EXTRACT
FALSE_P FAMILY FETCH FILTER FINALIZE FIRST_P FLOAT_P FOLLOWING FOR
FORCE FOREIGN FORMAT FORWARD FREEZE FROM FULL FUNCTION FUNCTIONS
@@ -2677,7 +2679,7 @@ alter_table_cmd:
processCASbits($4, @4, "ALTER CONSTRAINT statement",
&c->deferrable,
&c->initdeferred,
- NULL, NULL, yyscanner);
+ NULL, NULL, &c->is_enforced, yyscanner);
$$ = (Node *) n;
}
/* ALTER TABLE <name> VALIDATE CONSTRAINT ... */
@@ -3976,6 +3978,7 @@ ColConstraintElem:
n->cooked_expr = NULL;
n->skip_validation = false;
n->initially_valid = true;
+ n->is_enforced = true;
$$ = (Node *) n;
}
| DEFAULT b_expr
@@ -4100,6 +4103,22 @@ ConstraintAttr:
n->location = @1;
$$ = (Node *) n;
}
+ | ENFORCED
+ {
+ Constraint *n = makeNode(Constraint);
+
+ n->contype = CONSTR_ATTR_ENFORCED;
+ n->location = @1;
+ $$ = (Node *) n;
+ }
+ | NOT ENFORCED
+ {
+ Constraint *n = makeNode(Constraint);
+
+ n->contype = CONSTR_ATTR_NOT_ENFORCED;
+ n->location = @1;
+ $$ = (Node *) n;
+ }
;
@@ -4162,7 +4181,7 @@ ConstraintElem:
n->cooked_expr = NULL;
processCASbits($5, @5, "CHECK",
NULL, NULL, &n->skip_validation,
- &n->is_no_inherit, yyscanner);
+ &n->is_no_inherit, &n->is_enforced, yyscanner);
n->initially_valid = !n->skip_validation;
$$ = (Node *) n;
}
@@ -4182,7 +4201,7 @@ ConstraintElem:
n->indexspace = $9;
processCASbits($10, @10, "UNIQUE",
&n->deferrable, &n->initdeferred, NULL,
- NULL, yyscanner);
+ NULL, NULL, yyscanner);
$$ = (Node *) n;
}
| UNIQUE ExistingIndex ConstraintAttributeSpec
@@ -4198,7 +4217,7 @@ ConstraintElem:
n->indexspace = NULL;
processCASbits($3, @3, "UNIQUE",
&n->deferrable, &n->initdeferred, NULL,
- NULL, yyscanner);
+ NULL, NULL, yyscanner);
$$ = (Node *) n;
}
| PRIMARY KEY '(' columnList opt_without_overlaps ')' opt_c_include opt_definition OptConsTableSpace
@@ -4216,7 +4235,7 @@ ConstraintElem:
n->indexspace = $9;
processCASbits($10, @10, "PRIMARY KEY",
&n->deferrable, &n->initdeferred, NULL,
- NULL, yyscanner);
+ NULL, NULL, yyscanner);
$$ = (Node *) n;
}
| PRIMARY KEY ExistingIndex ConstraintAttributeSpec
@@ -4232,7 +4251,7 @@ ConstraintElem:
n->indexspace = NULL;
processCASbits($4, @4, "PRIMARY KEY",
&n->deferrable, &n->initdeferred, NULL,
- NULL, yyscanner);
+ NULL, NULL, yyscanner);
$$ = (Node *) n;
}
| EXCLUDE access_method_clause '(' ExclusionConstraintList ')'
@@ -4252,7 +4271,7 @@ ConstraintElem:
n->where_clause = $9;
processCASbits($10, @10, "EXCLUDE",
&n->deferrable, &n->initdeferred, NULL,
- NULL, yyscanner);
+ NULL, NULL, yyscanner);
$$ = (Node *) n;
}
| FOREIGN KEY '(' columnList optionalPeriodName ')' REFERENCES qualified_name
@@ -4282,6 +4301,7 @@ ConstraintElem:
processCASbits($12, @12, "FOREIGN KEY",
&n->deferrable, &n->initdeferred,
&n->skip_validation, NULL,
+ NULL,
yyscanner);
n->initially_valid = !n->skip_validation;
$$ = (Node *) n;
@@ -4322,7 +4342,7 @@ DomainConstraintElem:
n->cooked_expr = NULL;
processCASbits($5, @5, "CHECK",
NULL, NULL, &n->skip_validation,
- &n->is_no_inherit, yyscanner);
+ &n->is_no_inherit, NULL, yyscanner);
n->initially_valid = !n->skip_validation;
$$ = (Node *) n;
}
@@ -4336,7 +4356,7 @@ DomainConstraintElem:
/* no NOT VALID support yet */
processCASbits($3, @3, "NOT NULL",
NULL, NULL, NULL,
- &n->is_no_inherit, yyscanner);
+ &n->is_no_inherit, NULL, yyscanner);
n->initially_valid = true;
$$ = (Node *) n;
}
@@ -5998,7 +6018,7 @@ CreateTrigStmt:
n->transitionRels = NIL;
processCASbits($11, @11, "TRIGGER",
&n->deferrable, &n->initdeferred, NULL,
- NULL, yyscanner);
+ NULL, NULL, yyscanner);
n->constrrel = $10;
$$ = (Node *) n;
}
@@ -6167,7 +6187,8 @@ ConstraintAttributeSpec:
parser_errposition(@2)));
/* generic message for other conflicts */
if ((newspec & (CAS_NOT_DEFERRABLE | CAS_DEFERRABLE)) == (CAS_NOT_DEFERRABLE | CAS_DEFERRABLE) ||
- (newspec & (CAS_INITIALLY_IMMEDIATE | CAS_INITIALLY_DEFERRED)) == (CAS_INITIALLY_IMMEDIATE | CAS_INITIALLY_DEFERRED))
+ (newspec & (CAS_INITIALLY_IMMEDIATE | CAS_INITIALLY_DEFERRED)) == (CAS_INITIALLY_IMMEDIATE | CAS_INITIALLY_DEFERRED) ||
+ (newspec & (CAS_NOT_ENFORCED | CAS_ENFORCED)) == (CAS_NOT_ENFORCED | CAS_ENFORCED))
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("conflicting constraint properties"),
@@ -6183,6 +6204,8 @@ ConstraintAttributeElem:
| INITIALLY DEFERRED { $$ = CAS_INITIALLY_DEFERRED; }
| NOT VALID { $$ = CAS_NOT_VALID; }
| NO INHERIT { $$ = CAS_NO_INHERIT; }
+ | NOT ENFORCED { $$ = CAS_NOT_ENFORCED; }
+ | ENFORCED { $$ = CAS_ENFORCED; }
;
@@ -17636,6 +17659,7 @@ unreserved_keyword:
| ENABLE_P
| ENCODING
| ENCRYPTED
+ | ENFORCED
| ENUM_P
| ERROR_P
| ESCAPE
@@ -18213,6 +18237,7 @@ bare_label_keyword:
| ENCODING
| ENCRYPTED
| END_P
+ | ENFORCED
| ENUM_P
| ERROR_P
| ESCAPE
@@ -19306,7 +19331,7 @@ SplitColQualList(List *qualList,
static void
processCASbits(int cas_bits, int location, const char *constrType,
bool *deferrable, bool *initdeferred, bool *not_valid,
- bool *no_inherit, core_yyscan_t yyscanner)
+ bool *no_inherit, bool *is_enforced, core_yyscan_t yyscanner)
{
/* defaults */
if (deferrable)
@@ -19315,6 +19340,8 @@ processCASbits(int cas_bits, int location, const char *constrType,
*initdeferred = false;
if (not_valid)
*not_valid = false;
+ if (is_enforced)
+ *is_enforced = true;
if (cas_bits & (CAS_DEFERRABLE | CAS_INITIALLY_DEFERRED))
{
@@ -19367,6 +19394,19 @@ processCASbits(int cas_bits, int location, const char *constrType,
constrType),
parser_errposition(location)));
}
+
+ if (cas_bits & CAS_NOT_ENFORCED)
+ {
+ if (is_enforced)
+ *is_enforced = false;
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ /* translator: %s is CHECK, UNIQUE, or similar */
+ errmsg("%s constraints cannot be marked NOT ENFORCED",
+ constrType),
+ parser_errposition(location)));
+ }
}
/*
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 1e15ce10b48..024cc256b7d 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -835,6 +835,8 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column)
case CONSTR_ATTR_NOT_DEFERRABLE:
case CONSTR_ATTR_DEFERRED:
case CONSTR_ATTR_IMMEDIATE:
+ case CONSTR_ATTR_ENFORCED:
+ case CONSTR_ATTR_NOT_ENFORCED:
/* transformConstraintAttrs took care of these */
break;
@@ -955,6 +957,8 @@ transformTableConstraint(CreateStmtContext *cxt, Constraint *constraint)
case CONSTR_ATTR_NOT_DEFERRABLE:
case CONSTR_ATTR_DEFERRED:
case CONSTR_ATTR_IMMEDIATE:
+ case CONSTR_ATTR_ENFORCED:
+ case CONSTR_ATTR_NOT_ENFORCED:
elog(ERROR, "invalid context for constraint type %d",
constraint->contype);
break;
@@ -1317,6 +1321,7 @@ expandTableLikeClause(RangeVar *heapRel, TableLikeClause *table_like_clause)
n->is_no_inherit = ccnoinherit;
n->raw_expr = NULL;
n->cooked_expr = nodeToString(ccbin_node);
+ n->is_enforced = true;
/* We can skip validation, since the new table should be empty. */
n->skip_validation = true;
@@ -3715,6 +3720,7 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
Constraint *lastprimarycon = NULL;
bool saw_deferrability = false;
bool saw_initially = false;
+ bool saw_enforced = false;
ListCell *clist;
#define SUPPORTS_ATTRS(node) \
@@ -3810,12 +3816,45 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
lastprimarycon->initdeferred = false;
break;
+ case CONSTR_ATTR_ENFORCED:
+ if (lastprimarycon &&
+ lastprimarycon->contype != CONSTR_CHECK)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("misplaced ENFORCED clause"),
+ parser_errposition(cxt->pstate, con->location)));
+ if (saw_enforced)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("multiple ENFORCED/NOT ENFORCED clauses not allowed"),
+ parser_errposition(cxt->pstate, con->location)));
+ saw_enforced = true;
+ lastprimarycon->is_enforced = true;
+ break;
+
+ case CONSTR_ATTR_NOT_ENFORCED:
+ if (lastprimarycon &&
+ lastprimarycon->contype != CONSTR_CHECK)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("misplaced NOT ENFORCED clause"),
+ parser_errposition(cxt->pstate, con->location)));
+ if (saw_enforced)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("multiple ENFORCED/NOT ENFORCED clauses not allowed"),
+ parser_errposition(cxt->pstate, con->location)));
+ saw_enforced = true;
+ lastprimarycon->is_enforced = false;
+ break;
+
default:
/* Otherwise it's not an attribute */
lastprimarycon = con;
/* reset flags for new primary node */
saw_deferrability = false;
saw_initially = false;
+ saw_enforced = false;
break;
}
}
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 2177d17e278..cfdc4e31cd4 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -2372,6 +2372,9 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
appendStringInfoChar(&buf, ')');
}
+ if (!conForm->conenforced)
+ appendStringInfoString(&buf, " NOT ENFORCED");
+
break;
}
case CONSTRAINT_PRIMARY:
@@ -2514,6 +2517,10 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
appendStringInfo(&buf, "CHECK (%s)%s",
consrc,
conForm->connoinherit ? " NO INHERIT" : "");
+
+ if (!conForm->conenforced)
+ appendStringInfoString(&buf, " NOT ENFORCED");
+
break;
}
case CONSTRAINT_TRIGGER:
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index c326f687eb4..a603b74c778 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4624,6 +4624,7 @@ CheckConstraintFetch(Relation relation)
}
check[found].ccvalid = conform->convalidated;
+ check[found].ccenforced = conform->conenforced;
check[found].ccnoinherit = conform->connoinherit;
check[found].ccname = MemoryContextStrdup(CacheMemoryContext,
NameStr(conform->conname));
diff --git a/src/include/access/tupdesc.h b/src/include/access/tupdesc.h
index 8930a28d660..96bac018083 100644
--- a/src/include/access/tupdesc.h
+++ b/src/include/access/tupdesc.h
@@ -30,6 +30,7 @@ typedef struct ConstrCheck
char *ccname;
char *ccbin; /* nodeToString representation of expr */
bool ccvalid;
+ bool ccenforced;
bool ccnoinherit; /* this is a non-inheritable constraint */
} ConstrCheck;
diff --git a/src/include/catalog/heap.h b/src/include/catalog/heap.h
index c512824cd1c..96b22b4f86c 100644
--- a/src/include/catalog/heap.h
+++ b/src/include/catalog/heap.h
@@ -40,6 +40,7 @@ typedef struct CookedConstraint
AttrNumber attnum; /* which attr (only for DEFAULT) */
Node *expr; /* transformed default or check expr */
bool skip_validation; /* skip validation? (only for CHECK) */
+ bool is_enforced; /* is enforced? (only for CHECK) */
bool is_local; /* constraint has local (non-inherited) def */
int inhcount; /* number of times constraint is inherited */
bool is_no_inherit; /* constraint has local def and cannot be
diff --git a/src/include/catalog/pg_constraint.h b/src/include/catalog/pg_constraint.h
index 115217a6162..22074cd9e8a 100644
--- a/src/include/catalog/pg_constraint.h
+++ b/src/include/catalog/pg_constraint.h
@@ -52,6 +52,7 @@ CATALOG(pg_constraint,2606,ConstraintRelationId)
bool condeferrable; /* deferrable constraint? */
bool condeferred; /* deferred by default? */
bool convalidated; /* constraint has been validated? */
+ bool conenforced; /* enforced constraint? */
/*
* conrelid and conkey are only meaningful if the constraint applies to a
@@ -223,6 +224,7 @@ extern Oid CreateConstraintEntry(const char *constraintName,
bool isDeferrable,
bool isDeferred,
bool isValidated,
+ bool isEnforced,
Oid parentConstrId,
Oid relId,
const int16 *constraintKey,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 1c314cd9074..c29002edb24 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2725,6 +2725,8 @@ typedef enum ConstrType /* types of constraints */
CONSTR_ATTR_NOT_DEFERRABLE,
CONSTR_ATTR_DEFERRED,
CONSTR_ATTR_IMMEDIATE,
+ CONSTR_ATTR_ENFORCED,
+ CONSTR_ATTR_NOT_ENFORCED,
} ConstrType;
/* Foreign key action codes */
@@ -2748,6 +2750,7 @@ typedef struct Constraint
bool initdeferred; /* INITIALLY DEFERRED? */
bool skip_validation; /* skip validation of existing rows? */
bool initially_valid; /* mark the new constraint as valid? */
+ bool is_enforced; /* enforced constraint? */
bool is_no_inherit; /* is constraint non-inheritable? */
Node *raw_expr; /* CHECK or DEFAULT expression, as
* untransformed parse tree */
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 899d64ad55f..f2bc378394e 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -153,6 +153,7 @@ PG_KEYWORD("enable", ENABLE_P, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("encoding", ENCODING, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("encrypted", ENCRYPTED, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("end", END_P, RESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("enforced", ENFORCED, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("enum", ENUM_P, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("error", ERROR_P, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("escape", ESCAPE, UNRESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/test/regress/expected/constraints.out b/src/test/regress/expected/constraints.out
index cf0b80d6169..7fc1ced7efc 100644
--- a/src/test/regress/expected/constraints.out
+++ b/src/test/regress/expected/constraints.out
@@ -86,6 +86,25 @@ SELECT * FROM CHECK_TBL;
6
(3 rows)
+CREATE TABLE NE_CHECK_TBL (x int,
+ CONSTRAINT CHECK_CON CHECK (x > 3) NOT ENFORCED);
+INSERT INTO NE_CHECK_TBL VALUES (5);
+INSERT INTO NE_CHECK_TBL VALUES (4);
+INSERT INTO NE_CHECK_TBL VALUES (3);
+INSERT INTO NE_CHECK_TBL VALUES (2);
+INSERT INTO NE_CHECK_TBL VALUES (6);
+INSERT INTO NE_CHECK_TBL VALUES (1);
+SELECT * FROM NE_CHECK_TBL;
+ x
+---
+ 5
+ 4
+ 3
+ 2
+ 6
+ 1
+(6 rows)
+
CREATE SEQUENCE CHECK_SEQ;
CREATE TABLE CHECK2_TBL (x int, y text, z int,
CONSTRAINT SEQUENCE_CON
@@ -119,7 +138,8 @@ CREATE TABLE INSERT_TBL (x INT DEFAULT nextval('insert_seq'),
y TEXT DEFAULT '-NULL-',
z INT DEFAULT -1 * currval('insert_seq'),
CONSTRAINT INSERT_TBL_CON CHECK (x >= 3 AND y <> 'check failed' AND x < 8),
- CHECK (x + z = 0));
+ CHECK (x + z = 0) ENFORCED, /* no change it is a default */
+ CONSTRAINT NE_INSERT_TBL_CON CHECK (x + z = 1) NOT ENFORCED);
INSERT INTO INSERT_TBL(x,z) VALUES (2, -2);
ERROR: new row for relation "insert_tbl" violates check constraint "insert_tbl_con"
DETAIL: Failing row contains (2, -NULL-, -2).
diff --git a/src/test/regress/sql/constraints.sql b/src/test/regress/sql/constraints.sql
index e3e3bea7091..dac4f03bd71 100644
--- a/src/test/regress/sql/constraints.sql
+++ b/src/test/regress/sql/constraints.sql
@@ -66,6 +66,18 @@ INSERT INTO CHECK_TBL VALUES (1);
SELECT * FROM CHECK_TBL;
+CREATE TABLE NE_CHECK_TBL (x int,
+ CONSTRAINT CHECK_CON CHECK (x > 3) NOT ENFORCED);
+
+INSERT INTO NE_CHECK_TBL VALUES (5);
+INSERT INTO NE_CHECK_TBL VALUES (4);
+INSERT INTO NE_CHECK_TBL VALUES (3);
+INSERT INTO NE_CHECK_TBL VALUES (2);
+INSERT INTO NE_CHECK_TBL VALUES (6);
+INSERT INTO NE_CHECK_TBL VALUES (1);
+
+SELECT * FROM NE_CHECK_TBL;
+
CREATE SEQUENCE CHECK_SEQ;
CREATE TABLE CHECK2_TBL (x int, y text, z int,
@@ -91,7 +103,8 @@ CREATE TABLE INSERT_TBL (x INT DEFAULT nextval('insert_seq'),
y TEXT DEFAULT '-NULL-',
z INT DEFAULT -1 * currval('insert_seq'),
CONSTRAINT INSERT_TBL_CON CHECK (x >= 3 AND y <> 'check failed' AND x < 8),
- CHECK (x + z = 0));
+ CHECK (x + z = 0) ENFORCED, /* no change it is a default */
+ CONSTRAINT NE_INSERT_TBL_CON CHECK (x + z = 1) NOT ENFORCED);
INSERT INTO INSERT_TBL(x,z) VALUES (2, -2);
--
2.18.0
v1-0002-refactor-split-ATExecAlterConstrRecurse.patchapplication/x-patch; name=v1-0002-refactor-split-ATExecAlterConstrRecurse.patchDownload
From e71dc15381a3e7876de6fb39a8c3ab67f090d89b Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 23 Sep 2024 12:31:07 +0530
Subject: [PATCH v1 2/5] refactor: split ATExecAlterConstrRecurse()
When changing a NOT ENFORCED foreign key constraint to ENFORCED, we
need to create the necessary triggers for enforcing the constraint. We
can set the deferrability at the time of creation and skip the code
used for other constraint alteration operations.
Additionally, move the code that iterates over each child constraint
of the constraint being altered into a separate function.
---
src/backend/commands/tablecmds.c | 194 +++++++++++++++++++------------
1 file changed, 118 insertions(+), 76 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 6f7123e5f71..fe786f02304 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -384,6 +384,12 @@ static ObjectAddress ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd,
static bool ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
Relation rel, HeapTuple contuple, List **otherrelids,
LOCKMODE lockmode);
+static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
+ bool deferrable, bool initdeferred,
+ List **otherrelids);
+static void ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
+ Relation rel, HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode);
static ObjectAddress ATExecValidateConstraint(List **wqueue,
Relation rel, char *constrName,
bool recurse, bool recursing, LOCKMODE lockmode);
@@ -11577,9 +11583,6 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
{
HeapTuple copyTuple;
Form_pg_constraint copy_con;
- HeapTuple tgtuple;
- ScanKeyData tgkey;
- SysScanDesc tgscan;
copyTuple = heap_copytuple(contuple);
copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
@@ -11600,53 +11603,8 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
* Now we need to update the multiple entries in pg_trigger that
* implement the constraint.
*/
- ScanKeyInit(&tgkey,
- Anum_pg_trigger_tgconstraint,
- BTEqualStrategyNumber, F_OIDEQ,
- ObjectIdGetDatum(conoid));
- tgscan = systable_beginscan(tgrel, TriggerConstraintIndexId, true,
- NULL, 1, &tgkey);
- while (HeapTupleIsValid(tgtuple = systable_getnext(tgscan)))
- {
- Form_pg_trigger tgform = (Form_pg_trigger) GETSTRUCT(tgtuple);
- Form_pg_trigger copy_tg;
- HeapTuple tgCopyTuple;
-
- /*
- * Remember OIDs of other relation(s) involved in FK constraint.
- * (Note: it's likely that we could skip forcing a relcache inval
- * for other rels that don't have a trigger whose properties
- * change, but let's be conservative.)
- */
- if (tgform->tgrelid != RelationGetRelid(rel))
- *otherrelids = list_append_unique_oid(*otherrelids,
- tgform->tgrelid);
-
- /*
- * Update deferrability of RI_FKey_noaction_del,
- * RI_FKey_noaction_upd, RI_FKey_check_ins and RI_FKey_check_upd
- * triggers, but not others; see createForeignKeyActionTriggers
- * and CreateFKCheckTrigger.
- */
- if (tgform->tgfoid != F_RI_FKEY_NOACTION_DEL &&
- tgform->tgfoid != F_RI_FKEY_NOACTION_UPD &&
- tgform->tgfoid != F_RI_FKEY_CHECK_INS &&
- tgform->tgfoid != F_RI_FKEY_CHECK_UPD)
- continue;
-
- tgCopyTuple = heap_copytuple(tgtuple);
- copy_tg = (Form_pg_trigger) GETSTRUCT(tgCopyTuple);
-
- copy_tg->tgdeferrable = cmdcon->deferrable;
- copy_tg->tginitdeferred = cmdcon->initdeferred;
- CatalogTupleUpdate(tgrel, &tgCopyTuple->t_self, tgCopyTuple);
-
- InvokeObjectPostAlterHook(TriggerRelationId, tgform->oid, 0);
-
- heap_freetuple(tgCopyTuple);
- }
-
- systable_endscan(tgscan);
+ AlterConstrTriggerDeferrability(conoid, tgrel, rel, cmdcon->deferrable,
+ cmdcon->initdeferred, otherrelids);
}
/*
@@ -11659,36 +11617,120 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
*/
if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)
- {
- ScanKeyData pkey;
- SysScanDesc pscan;
- HeapTuple childtup;
-
- ScanKeyInit(&pkey,
- Anum_pg_constraint_conparentid,
- BTEqualStrategyNumber, F_OIDEQ,
- ObjectIdGetDatum(conoid));
-
- pscan = systable_beginscan(conrel, ConstraintParentIndexId,
- true, NULL, 1, &pkey);
-
- while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
- {
- Form_pg_constraint childcon = (Form_pg_constraint) GETSTRUCT(childtup);
- Relation childrel;
-
- childrel = table_open(childcon->conrelid, lockmode);
- ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, childrel, childtup,
- otherrelids, lockmode);
- table_close(childrel, NoLock);
- }
-
- systable_endscan(pscan);
- }
+ ATExecAlterChildConstr(cmdcon, conrel, tgrel, rel, contuple,
+ otherrelids, lockmode);
return changed;
}
+/*
+ * A subroutine of ATExecAlterConstrRecurse that updated constraint trigger's
+ * deferrability.
+ *
+ * The arguments to this function have the same meaning as the arguments to
+ * ATExecAlterConstrRecurse.
+ */
+static void
+AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
+ bool deferrable, bool initdeferred,
+ List **otherrelids)
+{
+ HeapTuple tgtuple;
+ ScanKeyData tgkey;
+ SysScanDesc tgscan;
+
+ ScanKeyInit(&tgkey,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+ tgscan = systable_beginscan(tgrel, TriggerConstraintIndexId, true,
+ NULL, 1, &tgkey);
+ while (HeapTupleIsValid(tgtuple = systable_getnext(tgscan)))
+ {
+ Form_pg_trigger tgform = (Form_pg_trigger) GETSTRUCT(tgtuple);
+ Form_pg_trigger copy_tg;
+ HeapTuple tgCopyTuple;
+
+ /*
+ * Remember OIDs of other relation(s) involved in FK constraint.
+ * (Note: it's likely that we could skip forcing a relcache inval for
+ * other rels that don't have a trigger whose properties change, but
+ * let's be conservative.)
+ */
+ if (tgform->tgrelid != RelationGetRelid(rel))
+ *otherrelids = list_append_unique_oid(*otherrelids,
+ tgform->tgrelid);
+
+ /*
+ * Update enable status and deferrability of RI_FKey_noaction_del,
+ * RI_FKey_noaction_upd, RI_FKey_check_ins and RI_FKey_check_upd
+ * triggers, but not others; see createForeignKeyActionTriggers and
+ * CreateFKCheckTrigger.
+ */
+ if (tgform->tgfoid != F_RI_FKEY_NOACTION_DEL &&
+ tgform->tgfoid != F_RI_FKEY_NOACTION_UPD &&
+ tgform->tgfoid != F_RI_FKEY_CHECK_INS &&
+ tgform->tgfoid != F_RI_FKEY_CHECK_UPD)
+ continue;
+
+ tgCopyTuple = heap_copytuple(tgtuple);
+ copy_tg = (Form_pg_trigger) GETSTRUCT(tgCopyTuple);
+
+ copy_tg->tgdeferrable = deferrable;
+ copy_tg->tginitdeferred = initdeferred;
+ CatalogTupleUpdate(tgrel, &tgCopyTuple->t_self, tgCopyTuple);
+
+ InvokeObjectPostAlterHook(TriggerRelationId, tgform->oid, 0);
+
+ heap_freetuple(tgCopyTuple);
+ }
+
+ systable_endscan(tgscan);
+}
+
+/*
+ * Invokes ATExecAlterConstrRecurse for each constraint that is a child of the
+ * specified constraint.
+ *
+ * The arguments to this function have the same meaning as the arguments to
+ * ATExecAlterConstrRecurse.
+ */
+static void
+ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
+ Relation rel, HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode)
+{
+ Form_pg_constraint currcon;
+ Oid conoid;
+ ScanKeyData pkey;
+ SysScanDesc pscan;
+ HeapTuple childtup;
+
+ currcon = (Form_pg_constraint) GETSTRUCT(contuple);
+ conoid = currcon->oid;
+
+ ScanKeyInit(&pkey,
+ Anum_pg_constraint_conparentid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+
+ pscan = systable_beginscan(conrel, ConstraintParentIndexId,
+ true, NULL, 1, &pkey);
+
+ while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
+ {
+ Form_pg_constraint childcon = (Form_pg_constraint) GETSTRUCT(childtup);
+ Relation childrel;
+
+ childrel = table_open(childcon->conrelid, lockmode);
+ ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, childrel, childtup,
+ otherrelids, lockmode);
+ table_close(childrel, NoLock);
+ }
+
+ systable_endscan(pscan);
+}
+
/*
* ALTER TABLE VALIDATE CONSTRAINT
*
--
2.18.0
v1-0003-refactor-Change-ATExecAlterConstrRecurse-argument.patchapplication/x-patch; name=v1-0003-refactor-Change-ATExecAlterConstrRecurse-argument.patchDownload
From dca36ab357d57c908d5b6c263d039110b47be212 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 23 Sep 2024 12:37:05 +0530
Subject: [PATCH v1 3/5] refactor: Change ATExecAlterConstrRecurse() argument.
Instead of passing the Relation of the referencing relation, we will
pass a relid. Opening the relation using the relid again should not
cause significant issues. However, this approach makes recursion more
efficient, especially in the later patch where we will be recreating
the referencing and referred triggers. In addition to that passing
relid of the referred relation too and Oids of triggers.
----
NOTE: This patch is not meant to be committed separately. It should
be squashed with the main patch that adds ENFORCED/NOT ENFORCED.
----
---
src/backend/commands/tablecmds.c | 66 ++++++++++++++++++++++----------
1 file changed, 45 insertions(+), 21 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index fe786f02304..f6e527b0a69 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -382,14 +382,22 @@ static void AlterSeqNamespaces(Relation classRel, Relation rel,
static ObjectAddress ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd,
bool recurse, bool recursing, LOCKMODE lockmode);
static bool ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode);
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger);
static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
bool deferrable, bool initdeferred,
List **otherrelids);
static void ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode);
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger);
static ObjectAddress ATExecValidateConstraint(List **wqueue,
Relation rel, char *constrName,
bool recurse, bool recursing, LOCKMODE lockmode);
@@ -11523,8 +11531,10 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
currcon->condeferred != cmdcon->initdeferred ||
rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
- if (ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, rel, contuple,
- &otherrelids, lockmode))
+ if (ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, currcon->conrelid,
+ currcon->confrelid, contuple, &otherrelids,
+ lockmode, InvalidOid, InvalidOid,
+ InvalidOid, InvalidOid))
ObjectAddressSet(address, ConstraintRelationId, currcon->oid);
}
@@ -11557,13 +11567,18 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
*/
static bool
ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode)
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger)
{
Form_pg_constraint currcon;
Oid conoid;
Oid refrelid;
bool changed = false;
+ Relation rel;
/* since this function recurses, it could be driven to stack overflow */
check_stack_depth();
@@ -11572,6 +11587,8 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
conoid = currcon->oid;
refrelid = currcon->confrelid;
+ rel = table_open(currcon->conrelid, lockmode);
+
/*
* Update pg_constraint with the flags from cmdcon.
*
@@ -11617,8 +11634,14 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
*/
if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)
- ATExecAlterChildConstr(cmdcon, conrel, tgrel, rel, contuple,
- otherrelids, lockmode);
+ ATExecAlterChildConstr(cmdcon, conrel, tgrel, fkrelid, pkrelid,
+ contuple, otherrelids, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
+
+ table_close(rel, NoLock);
return changed;
}
@@ -11697,8 +11720,12 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
*/
static void
ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode)
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger)
{
Form_pg_constraint currcon;
Oid conoid;
@@ -11718,15 +11745,12 @@ ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
true, NULL, 1, &pkey);
while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
- {
- Form_pg_constraint childcon = (Form_pg_constraint) GETSTRUCT(childtup);
- Relation childrel;
-
- childrel = table_open(childcon->conrelid, lockmode);
- ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, childrel, childtup,
- otherrelids, lockmode);
- table_close(childrel, NoLock);
- }
+ ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, fkrelid, pkrelid,
+ childtup, otherrelids, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
systable_endscan(pscan);
}
--
2.18.0
v1-0004-Remove-hastriggers-flag-check-before-fetching-FK-.patchapplication/x-patch; name=v1-0004-Remove-hastriggers-flag-check-before-fetching-FK-.patchDownload
From f2d762fc846c6c3dfd04258c1a6f64d7a78a747a Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 7 Oct 2024 12:40:07 +0530
Subject: [PATCH v1 4/5] Remove hastriggers flag check before fetching FK
constraints.
With NOT ENFORCED, foreign key constraints will be created without
triggers. Therefore, the criteria for fetching foreign keys based on
the presence of triggers no longer apply.
----
NOTE: This patch is intended to reduce the diff noise from the main
patch and is not meant to be committed separately. It should be
squashed with the main patch that adds ENFORCED/NOT ENFORCED.
----
---
src/backend/utils/cache/relcache.c | 5 -
src/bin/pg_dump/pg_dump.c | 8 +-
src/bin/psql/describe.c | 223 ++++++++++++++---------------
3 files changed, 107 insertions(+), 129 deletions(-)
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index a603b74c778..711a5290729 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4707,11 +4707,6 @@ RelationGetFKeyList(Relation relation)
if (relation->rd_fkeyvalid)
return relation->rd_fkeylist;
- /* Fast path: non-partitioned tables without triggers can't have FKs */
- if (!relation->rd_rel->relhastriggers &&
- relation->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
- return NIL;
-
/*
* We build the list we intend to return (in the caller's context) while
* doing the scan. After successfully completing the scan, we copy that
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 1b47c388ced..7225bb3c466 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -7823,13 +7823,7 @@ getConstraints(Archive *fout, TableInfo tblinfo[], int numTables)
{
TableInfo *tinfo = &tblinfo[i];
- /*
- * For partitioned tables, foreign keys have no triggers so they must
- * be included anyway in case some foreign keys are defined.
- */
- if ((!tinfo->hastriggers &&
- tinfo->relkind != RELKIND_PARTITIONED_TABLE) ||
- !(tinfo->dobj.dump & DUMP_COMPONENT_DEFINITION))
+ if (!(tinfo->dobj.dump & DUMP_COMPONENT_DEFINITION))
continue;
/* OK, we need info for this table */
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 6a36c910833..478fc3476f9 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2503,135 +2503,124 @@ describeOneTableDetails(const char *schemaname,
}
/*
- * Print foreign-key constraints (there are none if no triggers,
- * except if the table is partitioned, in which case the triggers
- * appear in the partitions)
+ * Print foreign-key constraints
*/
- if (tableinfo.hastriggers ||
- tableinfo.relkind == RELKIND_PARTITIONED_TABLE)
+ if (pset.sversion >= 120000 &&
+ (tableinfo.ispartition || tableinfo.relkind == RELKIND_PARTITIONED_TABLE))
{
- if (pset.sversion >= 120000 &&
- (tableinfo.ispartition || tableinfo.relkind == RELKIND_PARTITIONED_TABLE))
+ /*
+ * Put the constraints defined in this table first, followed
+ * by the constraints defined in ancestor partitioned tables.
+ */
+ printfPQExpBuffer(&buf,
+ "SELECT conrelid = '%s'::pg_catalog.regclass AS sametable,\n"
+ " conname,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef,\n"
+ " conrelid::pg_catalog.regclass AS ontable\n"
+ " FROM pg_catalog.pg_constraint,\n"
+ " pg_catalog.pg_partition_ancestors('%s')\n"
+ " WHERE conrelid = relid AND contype = 'f' AND conparentid = 0\n"
+ "ORDER BY sametable DESC, conname;",
+ oid, oid);
+ }
+ else
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT true as sametable, conname,\n"
+ " pg_catalog.pg_get_constraintdef(r.oid, true) as condef,\n"
+ " conrelid::pg_catalog.regclass AS ontable\n"
+ "FROM pg_catalog.pg_constraint r\n"
+ "WHERE r.conrelid = '%s' AND r.contype = 'f'\n",
+ oid);
+
+ if (pset.sversion >= 120000)
+ appendPQExpBufferStr(&buf, " AND conparentid = 0\n");
+ appendPQExpBufferStr(&buf, "ORDER BY conname");
+ }
+
+ result = PSQLexec(buf.data);
+ if (!result)
+ goto error_return;
+ else
+ tuples = PQntuples(result);
+
+ if (tuples > 0)
+ {
+ int i_sametable = PQfnumber(result, "sametable"),
+ i_conname = PQfnumber(result, "conname"),
+ i_condef = PQfnumber(result, "condef"),
+ i_ontable = PQfnumber(result, "ontable");
+
+ printTableAddFooter(&cont, _("Foreign-key constraints:"));
+ for (i = 0; i < tuples; i++)
{
/*
- * Put the constraints defined in this table first, followed
- * by the constraints defined in ancestor partitioned tables.
+ * Print untranslated constraint name and definition. Use
+ * a "TABLE tab" prefix when the constraint is defined in
+ * a parent partitioned table.
*/
- printfPQExpBuffer(&buf,
- "SELECT conrelid = '%s'::pg_catalog.regclass AS sametable,\n"
- " conname,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef,\n"
- " conrelid::pg_catalog.regclass AS ontable\n"
- " FROM pg_catalog.pg_constraint,\n"
- " pg_catalog.pg_partition_ancestors('%s')\n"
- " WHERE conrelid = relid AND contype = 'f' AND conparentid = 0\n"
- "ORDER BY sametable DESC, conname;",
- oid, oid);
- }
- else
- {
- printfPQExpBuffer(&buf,
- "SELECT true as sametable, conname,\n"
- " pg_catalog.pg_get_constraintdef(r.oid, true) as condef,\n"
- " conrelid::pg_catalog.regclass AS ontable\n"
- "FROM pg_catalog.pg_constraint r\n"
- "WHERE r.conrelid = '%s' AND r.contype = 'f'\n",
- oid);
-
- if (pset.sversion >= 120000)
- appendPQExpBufferStr(&buf, " AND conparentid = 0\n");
- appendPQExpBufferStr(&buf, "ORDER BY conname");
- }
-
- result = PSQLexec(buf.data);
- if (!result)
- goto error_return;
- else
- tuples = PQntuples(result);
-
- if (tuples > 0)
- {
- int i_sametable = PQfnumber(result, "sametable"),
- i_conname = PQfnumber(result, "conname"),
- i_condef = PQfnumber(result, "condef"),
- i_ontable = PQfnumber(result, "ontable");
-
- printTableAddFooter(&cont, _("Foreign-key constraints:"));
- for (i = 0; i < tuples; i++)
- {
- /*
- * Print untranslated constraint name and definition. Use
- * a "TABLE tab" prefix when the constraint is defined in
- * a parent partitioned table.
- */
- if (strcmp(PQgetvalue(result, i, i_sametable), "f") == 0)
- printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
- PQgetvalue(result, i, i_ontable),
- PQgetvalue(result, i, i_conname),
- PQgetvalue(result, i, i_condef));
- else
- printfPQExpBuffer(&buf, " \"%s\" %s",
- PQgetvalue(result, i, i_conname),
- PQgetvalue(result, i, i_condef));
-
- printTableAddFooter(&cont, buf.data);
- }
- }
- PQclear(result);
- }
-
- /* print incoming foreign-key references */
- if (tableinfo.hastriggers ||
- tableinfo.relkind == RELKIND_PARTITIONED_TABLE)
- {
- if (pset.sversion >= 120000)
- {
- printfPQExpBuffer(&buf,
- "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
- " FROM pg_catalog.pg_constraint c\n"
- " WHERE confrelid IN (SELECT pg_catalog.pg_partition_ancestors('%s')\n"
- " UNION ALL VALUES ('%s'::pg_catalog.regclass))\n"
- " AND contype = 'f' AND conparentid = 0\n"
- "ORDER BY conname;",
- oid, oid);
- }
- else
- {
- printfPQExpBuffer(&buf,
- "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
- " FROM pg_catalog.pg_constraint\n"
- " WHERE confrelid = %s AND contype = 'f'\n"
- "ORDER BY conname;",
- oid);
- }
-
- result = PSQLexec(buf.data);
- if (!result)
- goto error_return;
- else
- tuples = PQntuples(result);
-
- if (tuples > 0)
- {
- int i_conname = PQfnumber(result, "conname"),
- i_ontable = PQfnumber(result, "ontable"),
- i_condef = PQfnumber(result, "condef");
-
- printTableAddFooter(&cont, _("Referenced by:"));
- for (i = 0; i < tuples; i++)
- {
+ if (strcmp(PQgetvalue(result, i, i_sametable), "f") == 0)
printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
PQgetvalue(result, i, i_ontable),
PQgetvalue(result, i, i_conname),
PQgetvalue(result, i, i_condef));
+ else
+ printfPQExpBuffer(&buf, " \"%s\" %s",
+ PQgetvalue(result, i, i_conname),
+ PQgetvalue(result, i, i_condef));
- printTableAddFooter(&cont, buf.data);
- }
+ printTableAddFooter(&cont, buf.data);
}
- PQclear(result);
}
+ PQclear(result);
+
+ if (pset.sversion >= 120000)
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
+ " FROM pg_catalog.pg_constraint c\n"
+ " WHERE confrelid IN (SELECT pg_catalog.pg_partition_ancestors('%s')\n"
+ " UNION ALL VALUES ('%s'::pg_catalog.regclass))\n"
+ " AND contype = 'f' AND conparentid = 0\n"
+ "ORDER BY conname;",
+ oid, oid);
+ }
+ else
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
+ " FROM pg_catalog.pg_constraint\n"
+ " WHERE confrelid = %s AND contype = 'f'\n"
+ "ORDER BY conname;",
+ oid);
+ }
+
+ result = PSQLexec(buf.data);
+ if (!result)
+ goto error_return;
+ else
+ tuples = PQntuples(result);
+
+ if (tuples > 0)
+ {
+ int i_conname = PQfnumber(result, "conname"),
+ i_ontable = PQfnumber(result, "ontable"),
+ i_condef = PQfnumber(result, "condef");
+
+ printTableAddFooter(&cont, _("Referenced by:"));
+ for (i = 0; i < tuples; i++)
+ {
+ printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
+ PQgetvalue(result, i, i_ontable),
+ PQgetvalue(result, i, i_conname),
+ PQgetvalue(result, i, i_condef));
+
+ printTableAddFooter(&cont, buf.data);
+ }
+ }
+ PQclear(result);
/* print any row-level policies */
if (pset.sversion >= 90500)
--
2.18.0
v1-0005-Add-support-for-NOT-ENFORCED-in-foreign-key-const.patchapplication/x-patch; name=v1-0005-Add-support-for-NOT-ENFORCED-in-foreign-key-const.patchDownload
From 70a5ffa86fc8133778fcdc9e8178db0eec5795d1 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Tue, 8 Oct 2024 11:35:17 +0530
Subject: [PATCH v1 5/5] Add support for NOT ENFORCED in foreign key
constraints.
Normally, when a foreign key (FK) constraint is created on a table,
action/check triggers are associated with the constraint to ensure data
integrity. With this patch, when constraints are marked as NOT
ENFORCED, integrity checks are not required, and thus, these triggers
are unnecessary. As a result, when creating a NOT ENFORCED FK
constraint or changing an existing FK constraint to NOT ENFORCED, the
creation of triggers will be skipped, or existing triggers will be
dropped, respectively. And when changing an existing NOT ENFORCED FK
constraint to ENFORCED, those triggers will be created.
---
doc/src/sgml/advanced.sgml | 13 +
doc/src/sgml/ddl.sgml | 12 +-
doc/src/sgml/ref/alter_table.sgml | 5 +
doc/src/sgml/ref/create_table.sgml | 10 +-
src/backend/catalog/pg_constraint.c | 5 +-
src/backend/commands/tablecmds.c | 538 +++++++++++++++-------
src/backend/parser/gram.y | 3 +-
src/backend/parser/parse_utilcmd.c | 6 +-
src/backend/utils/cache/relcache.c | 1 +
src/include/utils/rel.h | 3 +
src/test/regress/expected/foreign_key.out | 118 ++++-
src/test/regress/sql/foreign_key.sql | 87 +++-
12 files changed, 620 insertions(+), 181 deletions(-)
diff --git a/doc/src/sgml/advanced.sgml b/doc/src/sgml/advanced.sgml
index 755c9f14850..b7d90de69d7 100644
--- a/doc/src/sgml/advanced.sgml
+++ b/doc/src/sgml/advanced.sgml
@@ -125,6 +125,19 @@ ERROR: insert or update on table "weather" violates foreign key constraint "wea
DETAIL: Key (city)=(Berkeley) is not present in table "cities".
</screen>
</para>
+ <para>
+ If, for some reason, you do not want to enforce this constraint and wish to
+ avoid the error, you can modify the foreign key constraint to <literal>NOT ENFORCED</literal>,
+ or specify it in the <link linkend="sql-createtable"><command>CREATE TABLE</command></link>.
+ </para>
+ <para>
+ Let's change the constraint to <literal>NOT ENFORCED</literal>, and when you
+ attempt to reinsert the same record, it will not result in any error.
+<programlisting>
+ALTER TABLE weather ALTER CONSTRAINT weather_city_fkey NOT ENFORCED;
+INSERT INTO weather VALUES ('Berkeley', 45, 53, 0.0, '1994-11-28');
+</programlisting>
+ </para>
<para>
The behavior of foreign keys can be finely tuned to your
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index 892a74346ca..c718e2d8f0d 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -1030,11 +1030,13 @@ CREATE TABLE example (
</indexterm>
<para>
- A foreign key constraint specifies that the values in a column (or
- a group of columns) must match the values appearing in some row
- of another table.
- We say this maintains the <firstterm>referential
- integrity</firstterm> between two related tables.
+ A foreign key constraint with <literal>ENFORCED</literal>, the default
+ setting, specifies that the values in a column (or a group of columns)
+ must match the values appearing in some row of another table. We say this
+ maintains the <firstterm>referential integrity</firstterm> between two
+ related tables. If a foreign key constraint is created with <literal>NOT ENFORCED</literal>,
+ the check for aforementioned <firstterm>referential integrity</firstterm>
+ will not be performed.
</para>
<para>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 257f871d99e..26ef967a66b 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -554,6 +554,11 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
This form alters the attributes of a constraint that was previously
created. Currently only foreign key constraints may be altered.
</para>
+ <para>
+ Note that changing constraints to <literal>ENFORCED</literal>, which were
+ previously marked as valid and <literal>NOT ENFORCED</literal>, will
+ trigger validation, similar to <literal>VALIDATE CONSTRAINT</literal>.
+ </para>
</listitem>
</varlistentry>
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index 55b51602136..77452efe740 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -1367,11 +1367,11 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
<term><literal>NOT ENFORCED</literal></term>
<listitem>
<para>
- This is currently only allowed for <literal>CHECK</literal> constraints.
- If the constraint is <literal>NOT ENFORCED</literal>, this clause
- specifies that the constraint check will be skipped. When the constraint
- is <literal>ENFORCED</literal>, check is performed after each statement.
- This is the default.
+ This is currently only allowed for foreign key and <literal>CHECK</literal>
+ constraints. If the constraint is <literal>NOT ENFORCED</literal>, this
+ clause specifies that the constraint check will be skipped. When the
+ constraint is <literal>ENFORCED</literal>, check is performed after each
+ statement. This is the default.
</para>
</listitem>
</varlistentry>
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index 14468b2b7d0..69d645761d7 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -98,8 +98,9 @@ CreateConstraintEntry(const char *constraintName,
ObjectAddresses *addrs_auto;
ObjectAddresses *addrs_normal;
- /* Only CHECK constraint can be not enforced */
- Assert(isEnforced || constraintType == CONSTRAINT_CHECK);
+ /* Only CHECK or FOREIGN KEY constraint can be not enforced */
+ Assert(isEnforced || constraintType == CONSTRAINT_CHECK ||
+ constraintType == CONSTRAINT_FOREIGN);
conDesc = table_open(ConstraintRelationId, RowExclusiveLock);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index f6e527b0a69..d7f6eb53a1d 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -379,19 +379,29 @@ static void AlterIndexNamespaces(Relation classRel, Relation rel,
static void AlterSeqNamespaces(Relation classRel, Relation rel,
Oid oldNspOid, Oid newNspOid, ObjectAddresses *objsMoved,
LOCKMODE lockmode);
-static ObjectAddress ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd,
+static ObjectAddress ATExecAlterConstraint(List **wqueue, Relation rel, AlterTableCmd *cmd,
bool recurse, bool recursing, LOCKMODE lockmode);
-static bool ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
+static bool ATExecAlterConstrRecurse(List **wqueue, Constraint *cmdcon,
+ Relation conrel, Relation tgrel,
const Oid fkrelid, const Oid pkrelid,
HeapTuple contuple, List **otherrelids,
LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
Oid ReferencedParentUpdTrigger,
Oid ReferencingParentInsTrigger,
Oid ReferencingParentUpdTrigger);
-static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
+static void ATExecAlterConstrEnforceability(List **wqueue, Constraint *cmdcon,
+ Relation conrel, Relation tgrel,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger);
+static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Oid fkrel,
bool deferrable, bool initdeferred,
List **otherrelids);
-static void ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
+static void ATExecAlterChildConstr(List **wqueue, Constraint *cmdcon,
+ Relation conrel, Relation tgrel,
const Oid fkrelid, const Oid pkrelid,
HeapTuple contuple, List **otherrelids,
LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
@@ -5359,7 +5369,7 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
lockmode);
break;
case AT_AlterConstraint: /* ALTER CONSTRAINT */
- address = ATExecAlterConstraint(rel, cmd, false, false, lockmode);
+ address = ATExecAlterConstraint(wqueue, rel, cmd, false, false, lockmode);
break;
case AT_ValidateConstraint: /* VALIDATE CONSTRAINT */
address = ATExecValidateConstraint(wqueue, rel, cmd->name, cmd->recurse,
@@ -9480,8 +9490,9 @@ ATAddCheckConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
{
CookedConstraint *ccon = (CookedConstraint *) lfirst(lcon);
- /* Only CHECK constraint can be not enforced */
- Assert(ccon->is_enforced || ccon->contype == CONSTRAINT_CHECK);
+ /* Only CHECK or FOREIGN KEY constraint can be not enforced */
+ Assert(ccon->is_enforced || ccon->contype == CONSTRAINT_CHECK ||
+ ccon->contype == CONSTRAINT_FOREIGN);
if (!ccon->skip_validation)
{
@@ -10192,8 +10203,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
bool conislocal;
int coninhcount;
bool connoinherit;
- Oid deleteTriggerOid,
- updateTriggerOid;
+ Oid deleteTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
/*
* Verify relkind for each referenced partition. At the top level, this
@@ -10246,7 +10257,7 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
fkconstraint->deferrable,
fkconstraint->initdeferred,
fkconstraint->initially_valid,
- true, /* Is enforced */
+ fkconstraint->is_enforced,
parentConstr,
RelationGetRelid(rel),
fkattnum,
@@ -10294,13 +10305,15 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
CommandCounterIncrement();
/*
- * Create the action triggers that enforce the constraint.
+ * Create action triggers to enforce the constraint, or skip them if the
+ * constraint is not enforced.
*/
- createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
- fkconstraint,
- constrOid, indexOid,
- parentDelTrigger, parentUpdTrigger,
- &deleteTriggerOid, &updateTriggerOid);
+ if (fkconstraint->is_enforced)
+ createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
+ fkconstraint,
+ constrOid, indexOid,
+ parentDelTrigger, parentUpdTrigger,
+ &deleteTriggerOid, &updateTriggerOid);
/*
* If the referenced table is partitioned, recurse on ourselves to handle
@@ -10409,8 +10422,8 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
Oid parentInsTrigger, Oid parentUpdTrigger,
bool with_period)
{
- Oid insertTriggerOid,
- updateTriggerOid;
+ Oid insertTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
Assert(OidIsValid(parentConstr));
@@ -10420,29 +10433,32 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
errmsg("foreign key constraints are not supported on foreign tables")));
/*
- * Add the check triggers to it and, if necessary, schedule it to be
- * checked in Phase 3.
+ * Add check triggers if the constraint is enforced, and if needed,
+ * schedule them to be checked in Phase 3.
*
* If the relation is partitioned, drill down to do it to its partitions.
*/
- createForeignKeyCheckTriggers(RelationGetRelid(rel),
- RelationGetRelid(pkrel),
- fkconstraint,
- parentConstr,
- indexOid,
- parentInsTrigger, parentUpdTrigger,
- &insertTriggerOid, &updateTriggerOid);
+ if (fkconstraint->is_enforced)
+ createForeignKeyCheckTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
+ fkconstraint,
+ parentConstr,
+ indexOid,
+ parentInsTrigger, parentUpdTrigger,
+ &insertTriggerOid, &updateTriggerOid);
if (rel->rd_rel->relkind == RELKIND_RELATION)
{
/*
* Tell Phase 3 to check that the constraint is satisfied by existing
- * rows. We can skip this during table creation, when requested
- * explicitly by specifying NOT VALID in an ADD FOREIGN KEY command,
- * and when we're recreating a constraint following a SET DATA TYPE
- * operation that did not impugn its validity.
+ * rows. We can skip this during table creation, when constraint is
+ * specified as NOT ENFORCED, when requested explicitly by specifying
+ * NOT VALID in an ADD FOREIGN KEY command, and when we're recreating
+ * a constraint following a SET DATA TYPE operation that did not
+ * impugn its validity.
*/
- if (wqueue && !old_check_ok && !fkconstraint->skip_validation)
+ if (wqueue && !old_check_ok && !fkconstraint->skip_validation &&
+ fkconstraint->is_enforced)
{
NewConstraint *newcon;
AlteredTableInfo *tab;
@@ -10549,7 +10565,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
fkconstraint->deferrable,
fkconstraint->initdeferred,
fkconstraint->initially_valid,
- true, /* Is enforced */
+ fkconstraint->is_enforced,
parentConstr,
partitionId,
mapped_fkattnum,
@@ -10727,8 +10743,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
int numfkdelsetcols;
AttrNumber confdelsetcols[INDEX_MAX_KEYS];
Constraint *fkconstraint;
- Oid deleteTriggerOid,
- updateTriggerOid;
+ Oid deleteTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(constrOid));
if (!HeapTupleIsValid(tuple))
@@ -10783,6 +10799,7 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
fkconstraint->conname = NameStr(constrForm->conname);
fkconstraint->deferrable = constrForm->condeferrable;
fkconstraint->initdeferred = constrForm->condeferred;
+ fkconstraint->is_enforced = constrForm->conenforced;
fkconstraint->location = -1;
fkconstraint->pktable = NULL;
/* ->fk_attrs determined below */
@@ -10822,9 +10839,11 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
* parent OIDs for similar triggers that will be created on the
* partition in addFkRecurseReferenced().
*/
- GetForeignKeyActionTriggers(trigrel, constrOid,
- constrForm->confrelid, constrForm->conrelid,
- &deleteTriggerOid, &updateTriggerOid);
+ if (constrForm->conenforced)
+ GetForeignKeyActionTriggers(trigrel, constrOid,
+ constrForm->confrelid,
+ constrForm->conrelid,
+ &deleteTriggerOid, &updateTriggerOid);
addFkRecurseReferenced(NULL,
fkconstraint,
@@ -10984,17 +11003,18 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
mapped_conkey[i] = attmap->attnums[conkey[i] - 1];
/*
- * Get the "check" triggers belonging to the constraint to pass as
- * parent OIDs for similar triggers that will be created on the
- * partition in addFkRecurseReferencing(). They are also passed to
- * tryAttachPartitionForeignKey() below to simply assign as parents to
- * the partition's existing "check" triggers, that is, if the
- * corresponding constraints is deemed attachable to the parent
- * constraint.
+ * Get the "check" triggers belonging to the constraint, if it is
+ * enforced, to pass as parent OIDs for similar triggers that will be
+ * created on the partition in addFkRecurseReferencing(). They are
+ * also passed to tryAttachPartitionForeignKey() below to simply
+ * assign as parents to the partition's existing "check" triggers,
+ * that is, if the corresponding constraints is deemed attachable to
+ * the parent constraint.
*/
- GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
- constrForm->confrelid, constrForm->conrelid,
- &insertTriggerOid, &updateTriggerOid);
+ if (constrForm->conenforced)
+ GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
+ constrForm->confrelid, constrForm->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
/*
* Before creating a new constraint, see whether any existing FKs are
@@ -11036,6 +11056,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
/* ->conname determined below */
fkconstraint->deferrable = constrForm->condeferrable;
fkconstraint->initdeferred = constrForm->condeferred;
+ fkconstraint->is_enforced = constrForm->conenforced;
fkconstraint->location = -1;
fkconstraint->pktable = NULL;
/* ->fk_attrs determined below */
@@ -11077,7 +11098,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
fkconstraint->deferrable,
fkconstraint->initdeferred,
constrForm->convalidated,
- true, /* Is enforced */
+ fkconstraint->is_enforced,
parentConstrOid,
RelationGetRelid(partRel),
mapped_conkey,
@@ -11217,6 +11238,7 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
!partConstr->convalidated ||
partConstr->condeferrable != parentConstr->condeferrable ||
partConstr->condeferred != parentConstr->condeferred ||
+ partConstr->conenforced != parentConstr->conenforced ||
partConstr->confupdtype != parentConstr->confupdtype ||
partConstr->confdeltype != parentConstr->confdeltype ||
partConstr->confmatchtype != parentConstr->confmatchtype)
@@ -11276,18 +11298,22 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
ConstraintSetParentConstraint(fk->conoid, parentConstrOid, partRelid);
/*
- * Like the constraint, attach partition's "check" triggers to the
- * corresponding parent triggers.
+ * Similar to the constraint, attach the partition's "check" triggers to
+ * the corresponding parent triggers if the constraint is enforced.
+ * Non-enforced constraints do not have these triggers.
*/
- GetForeignKeyCheckTriggers(trigrel,
- fk->conoid, fk->confrelid, fk->conrelid,
- &insertTriggerOid, &updateTriggerOid);
- Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
- TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
- partRelid);
- Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
- TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
- partRelid);
+ if (fk->conenforced)
+ {
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
+ partRelid);
+ Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
+ partRelid);
+ }
CommandCounterIncrement();
return true;
@@ -11426,8 +11452,8 @@ GetForeignKeyCheckTriggers(Relation trigrel,
* InvalidObjectAddress.
*/
static ObjectAddress
-ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
- bool recursing, LOCKMODE lockmode)
+ATExecAlterConstraint(List **wqueue, Relation rel, AlterTableCmd *cmd,
+ bool recurse, bool recursing, LOCKMODE lockmode)
{
Constraint *cmdcon;
Relation conrel;
@@ -11529,12 +11555,14 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
address = InvalidObjectAddress;
if (currcon->condeferrable != cmdcon->deferrable ||
currcon->condeferred != cmdcon->initdeferred ||
+ currcon->conenforced != cmdcon->is_enforced ||
rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
- if (ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, currcon->conrelid,
- currcon->confrelid, contuple, &otherrelids,
- lockmode, InvalidOid, InvalidOid,
- InvalidOid, InvalidOid))
+ if (ATExecAlterConstrRecurse(wqueue, cmdcon, conrel, tgrel,
+ currcon->conrelid, currcon->confrelid,
+ contuple, &otherrelids, lockmode,
+ InvalidOid, InvalidOid, InvalidOid,
+ InvalidOid))
ObjectAddressSet(address, ConstraintRelationId, currcon->oid);
}
@@ -11566,8 +11594,8 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
* but existing releases don't do that.)
*/
static bool
-ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
- const Oid fkrelid, const Oid pkrelid,
+ATExecAlterConstrRecurse(List **wqueue, Constraint *cmdcon, Relation conrel,
+ Relation tgrel, const Oid fkrelid, const Oid pkrelid,
HeapTuple contuple, List **otherrelids,
LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
Oid ReferencedParentUpdTrigger,
@@ -11576,19 +11604,18 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
{
Form_pg_constraint currcon;
Oid conoid;
+ Oid conrelid;
Oid refrelid;
bool changed = false;
- Relation rel;
/* since this function recurses, it could be driven to stack overflow */
check_stack_depth();
currcon = (Form_pg_constraint) GETSTRUCT(contuple);
conoid = currcon->oid;
+ conrelid = currcon->conrelid;
refrelid = currcon->confrelid;
- rel = table_open(currcon->conrelid, lockmode);
-
/*
* Update pg_constraint with the flags from cmdcon.
*
@@ -11596,7 +11623,8 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
* silently do nothing.
*/
if (currcon->condeferrable != cmdcon->deferrable ||
- currcon->condeferred != cmdcon->initdeferred)
+ currcon->condeferred != cmdcon->initdeferred ||
+ currcon->conenforced != cmdcon->is_enforced)
{
HeapTuple copyTuple;
Form_pg_constraint copy_con;
@@ -11605,6 +11633,7 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
copy_con->condeferrable = cmdcon->deferrable;
copy_con->condeferred = cmdcon->initdeferred;
+ copy_con->conenforced = cmdcon->is_enforced;
CatalogTupleUpdate(conrel, ©Tuple->t_self, copyTuple);
InvokeObjectPostAlterHook(ConstraintRelationId,
@@ -11614,38 +11643,214 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
changed = true;
/* Make new constraint flags visible to others */
- CacheInvalidateRelcache(rel);
+ CacheInvalidateRelcacheByRelid(conrelid);
+ }
+ if (currcon->conenforced != cmdcon->is_enforced)
+ {
+ ATExecAlterConstrEnforceability(wqueue, cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, contuple, otherrelids, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
+ }
+ else
+ {
/*
* Now we need to update the multiple entries in pg_trigger that
* implement the constraint.
*/
- AlterConstrTriggerDeferrability(conoid, tgrel, rel, cmdcon->deferrable,
- cmdcon->initdeferred, otherrelids);
+ AlterConstrTriggerDeferrability(conoid, tgrel, conrelid,
+ cmdcon->deferrable,
+ cmdcon->initdeferred,
+ otherrelids);
+
+ /*
+ * If the table at either end of the constraint is partitioned, we
+ * need to recurse and handle every constraint that is a child of this
+ * one.
+ *
+ * (This assumes that the recurse flag is forcibly set for partitioned
+ * tables, and not set for legacy inheritance, though we don't check
+ * for that here.)
+ */
+ if (get_rel_relkind(conrelid) == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)
+ ATExecAlterChildConstr(wqueue, cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, contuple, otherrelids, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
}
- /*
- * If the table at either end of the constraint is partitioned, we need to
- * recurse and handle every constraint that is a child of this one.
- *
- * (This assumes that the recurse flag is forcibly set for partitioned
- * tables, and not set for legacy inheritance, though we don't check for
- * that here.)
- */
- if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
- get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)
- ATExecAlterChildConstr(cmdcon, conrel, tgrel, fkrelid, pkrelid,
- contuple, otherrelids, lockmode,
- ReferencedParentDelTrigger,
- ReferencedParentUpdTrigger,
- ReferencingParentInsTrigger,
- ReferencingParentUpdTrigger);
-
- table_close(rel, NoLock);
-
return changed;
}
+/*
+ * A subroutine of ATExecAlterConstrRecurse that updates the enforceability of
+ * a foreign key constraint. Depending on whether the constraint is being set
+ * to enforced or not enforced, it creates or drops the trigger accordingly.
+ *
+ * The arguments to this function have the same meaning as the arguments to
+ * ATExecAlterConstrRecurse.
+ */
+static void
+ATExecAlterConstrEnforceability(List **wqueue, Constraint *cmdcon,
+ Relation conrel, Relation tgrel,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger)
+{
+ Form_pg_constraint currcon;
+ Oid conoid;
+
+ currcon = (Form_pg_constraint) GETSTRUCT(contuple);
+ conoid = currcon->oid;
+
+ /* Drop triggers */
+ if (!cmdcon->is_enforced)
+ {
+ HeapTuple tgtuple;
+ ScanKeyData tgkey;
+ SysScanDesc tgscan;
+
+ /*
+ * When setting a constraint to NOT ENFORCED, the constraint triggers
+ * need to be dropped. Therefore, we must process the child relations
+ * first, followed by the parent, to account for dependencies.
+ */
+ if (get_rel_relkind(currcon->conrelid) == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE)
+ ATExecAlterChildConstr(wqueue, cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, contuple, otherrelids, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
+
+ ScanKeyInit(&tgkey,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+ tgscan = systable_beginscan(tgrel, TriggerConstraintIndexId, true,
+ NULL, 1, &tgkey);
+ while (HeapTupleIsValid(tgtuple = systable_getnext(tgscan)))
+ {
+ Form_pg_trigger tgform = (Form_pg_trigger) GETSTRUCT(tgtuple);
+ ObjectAddress trigger;
+
+ /*
+ * Delete only RI_FKey_noaction_del, RI_FKey_noaction_upd,
+ * RI_FKey_check_ins and RI_FKey_check_upd triggers, but not
+ * others; see createForeignKeyActionTriggers and
+ * CreateFKCheckTrigger.
+ */
+ if (tgform->tgfoid != F_RI_FKEY_NOACTION_DEL &&
+ tgform->tgfoid != F_RI_FKEY_NOACTION_UPD &&
+ tgform->tgfoid != F_RI_FKEY_CHECK_INS &&
+ tgform->tgfoid != F_RI_FKEY_CHECK_UPD)
+ continue;
+
+ deleteDependencyRecordsFor(TriggerRelationId, tgform->oid,
+ false);
+ /* make dependency deletion visible to performDeletion */
+ CommandCounterIncrement();
+ ObjectAddressSet(trigger, TriggerRelationId, tgform->oid);
+ performDeletion(&trigger, DROP_RESTRICT, 0);
+ /* make trigger drop visible, in case the loop iterates */
+ CommandCounterIncrement();
+ }
+
+ systable_endscan(tgscan);
+ }
+ else /* Create triggers */
+ {
+ Relation rel;
+ Oid ReferencedDelTriggerOid = InvalidOid,
+ ReferencedUpdTriggerOid = InvalidOid,
+ ReferencingInsTriggerOid = InvalidOid,
+ ReferencingUpdTriggerOid = InvalidOid;
+
+ /* Update the foreign key action required for trigger creation. */
+ cmdcon->fk_matchtype = currcon->confmatchtype;
+ cmdcon->fk_upd_action = currcon->confupdtype;
+ cmdcon->fk_del_action = currcon->confdeltype;
+
+ rel = table_open(currcon->conrelid, lockmode);
+
+ /* Create referenced triggers */
+ if (currcon->conrelid == fkrelid)
+ createForeignKeyActionTriggers(rel,
+ currcon->confrelid,
+ cmdcon,
+ conoid,
+ currcon->conindid,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ &ReferencedDelTriggerOid,
+ &ReferencedUpdTriggerOid);
+
+ /* Create referencing triggers */
+ if (currcon->confrelid == pkrelid)
+ {
+ createForeignKeyCheckTriggers(currcon->conrelid,
+ pkrelid,
+ cmdcon,
+ conoid,
+ currcon->conindid,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger,
+ &ReferencingInsTriggerOid,
+ &ReferencingUpdTriggerOid);
+
+ /*
+ * Tell Phase 3 to check that the constraint is satisfied by
+ * existing rows, but skip this step if the rows have not been
+ * validated previously.
+ */
+ if (rel->rd_rel->relkind == RELKIND_RELATION &&
+ wqueue && currcon->convalidated)
+ {
+ NewConstraint *newcon;
+ AlteredTableInfo *tab;
+
+ tab = ATGetQueueEntry(wqueue, rel);
+
+ newcon = (NewConstraint *) palloc0(sizeof(NewConstraint));
+ newcon->name = pstrdup(NameStr(currcon->conname));
+ newcon->conid = currcon->oid;
+ newcon->contype = CONSTR_FOREIGN;
+ newcon->refrelid = pkrelid;
+ newcon->refindid = currcon->conindid;
+ newcon->conwithperiod = currcon->conperiod;
+ newcon->qual = (Node *) cmdcon;
+
+ tab->constraints = lappend(tab->constraints, newcon);
+ }
+ }
+
+ /*
+ * If the table at either end of the constraint is partitioned, we
+ * need to recurse and create triggers for each constraint that is a
+ * child of this one.
+ */
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE)
+ ATExecAlterChildConstr(wqueue, cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, contuple, otherrelids, lockmode,
+ ReferencedDelTriggerOid,
+ ReferencedUpdTriggerOid,
+ ReferencingInsTriggerOid,
+ ReferencingUpdTriggerOid);
+ table_close(rel, NoLock);
+ }
+}
+
/*
* A subroutine of ATExecAlterConstrRecurse that updated constraint trigger's
* deferrability.
@@ -11654,7 +11859,7 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
* ATExecAlterConstrRecurse.
*/
static void
-AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
+AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Oid fkrel,
bool deferrable, bool initdeferred,
List **otherrelids)
{
@@ -11680,7 +11885,7 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
* other rels that don't have a trigger whose properties change, but
* let's be conservative.)
*/
- if (tgform->tgrelid != RelationGetRelid(rel))
+ if (tgform->tgrelid != fkrel)
*otherrelids = list_append_unique_oid(*otherrelids,
tgform->tgrelid);
@@ -11719,8 +11924,8 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
* ATExecAlterConstrRecurse.
*/
static void
-ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
- const Oid fkrelid, const Oid pkrelid,
+ATExecAlterChildConstr(List **wqueue, Constraint *cmdcon, Relation conrel,
+ Relation tgrel, const Oid fkrelid, const Oid pkrelid,
HeapTuple contuple, List **otherrelids,
LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
Oid ReferencedParentUpdTrigger,
@@ -11745,9 +11950,9 @@ ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
true, NULL, 1, &pkey);
while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
- ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, fkrelid, pkrelid,
- childtup, otherrelids, lockmode,
- ReferencedParentDelTrigger,
+ ATExecAlterConstrRecurse(wqueue, cmdcon, conrel, tgrel,
+ fkrelid, pkrelid, childtup, otherrelids,
+ lockmode, ReferencedParentDelTrigger,
ReferencedParentUpdTrigger,
ReferencingParentInsTrigger,
ReferencingParentUpdTrigger);
@@ -11820,25 +12025,33 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
if (con->contype == CONSTRAINT_FOREIGN)
{
- NewConstraint *newcon;
- Constraint *fkconstraint;
+ /*
+ * Queue validation for phase 3 only if constraint is enforced;
+ * otherwise, adding it to the validation queue won't be very
+ * effective, as the verification will be skipped.
+ */
+ if (con->conenforced)
+ {
+ NewConstraint *newcon;
+ Constraint *fkconstraint;
- /* Queue validation for phase 3 */
- fkconstraint = makeNode(Constraint);
- /* for now this is all we need */
- fkconstraint->conname = constrName;
+ /* Queue validation for phase 3 */
+ fkconstraint = makeNode(Constraint);
+ /* for now this is all we need */
+ fkconstraint->conname = constrName;
- newcon = (NewConstraint *) palloc0(sizeof(NewConstraint));
- newcon->name = constrName;
- newcon->contype = CONSTR_FOREIGN;
- newcon->refrelid = con->confrelid;
- newcon->refindid = con->conindid;
- newcon->conid = con->oid;
- newcon->qual = (Node *) fkconstraint;
+ newcon = (NewConstraint *) palloc0(sizeof(NewConstraint));
+ newcon->name = constrName;
+ newcon->contype = CONSTR_FOREIGN;
+ newcon->refrelid = con->confrelid;
+ newcon->refindid = con->conindid;
+ newcon->conid = con->oid;
+ newcon->qual = (Node *) fkconstraint;
- /* Find or create work queue entry for this table */
- tab = ATGetQueueEntry(wqueue, rel);
- tab->constraints = lappend(tab->constraints, newcon);
+ /* Find or create work queue entry for this table */
+ tab = ATGetQueueEntry(wqueue, rel);
+ tab->constraints = lappend(tab->constraints, newcon);
+ }
/*
* We disallow creating invalid foreign keys to or from
@@ -11897,9 +12110,9 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
}
/*
- * Queue validation for phase 3 only if constraint is enforced;
- * otherwise, adding it to the validation queue won't be very
- * effective, as the verification will be skipped.
+ * Queue validation for phase 3 only if the constraint is
+ * enforced, for the same reason outlined for the foreign key
+ * constraint.
*/
if (con->conenforced)
{
@@ -11930,7 +12143,10 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
/*
* Now update the catalog regardless of enforcement; the validated
* flag will not take effect until the constraint is marked as
- * enforced.
+ * enforced. When changing a non-enforced constraint to enforced, the
+ * validation will only occur if the validation flag is true.
+ * Otherwise, the user will need to explicitly perform constraint
+ * validation.
*/
copyTuple = heap_copytuple(tuple);
copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
@@ -19444,46 +19660,52 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
/*
* Also, look up the partition's "check" triggers corresponding to the
- * constraint being detached and detach them from the parent triggers.
+ * enforced constraint being detached and detach them from the parent
+ * triggers. Non-enforced constraints do not have these triggers;
+ * therefore, this step is not needed.
*/
- GetForeignKeyCheckTriggers(trigrel,
- fk->conoid, fk->confrelid, fk->conrelid,
- &insertTriggerOid, &updateTriggerOid);
- Assert(OidIsValid(insertTriggerOid));
- TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
- RelationGetRelid(partRel));
- Assert(OidIsValid(updateTriggerOid));
- TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
- RelationGetRelid(partRel));
+ if (fk->conenforced)
+ {
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ Assert(OidIsValid(updateTriggerOid));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
- /*
- * Make the action triggers on the referenced relation. When this was
- * a partition the action triggers pointed to the parent rel (they
- * still do), but now we need separate ones of our own.
- */
- fkconstraint = makeNode(Constraint);
- fkconstraint->contype = CONSTRAINT_FOREIGN;
- fkconstraint->conname = pstrdup(NameStr(conform->conname));
- fkconstraint->deferrable = conform->condeferrable;
- fkconstraint->initdeferred = conform->condeferred;
- fkconstraint->location = -1;
- fkconstraint->pktable = NULL;
- fkconstraint->fk_attrs = NIL;
- fkconstraint->pk_attrs = NIL;
- fkconstraint->fk_matchtype = conform->confmatchtype;
- fkconstraint->fk_upd_action = conform->confupdtype;
- fkconstraint->fk_del_action = conform->confdeltype;
- fkconstraint->fk_del_set_cols = NIL;
- fkconstraint->old_conpfeqop = NIL;
- fkconstraint->old_pktable_oid = InvalidOid;
- fkconstraint->skip_validation = false;
- fkconstraint->initially_valid = true;
+ /*
+ * Make the action triggers on the referenced relation. When this
+ * was a partition the action triggers pointed to the parent rel
+ * (they still do), but now we need separate ones of our own.
+ */
+ fkconstraint = makeNode(Constraint);
+ fkconstraint->contype = CONSTRAINT_FOREIGN;
+ fkconstraint->conname = pstrdup(NameStr(conform->conname));
+ fkconstraint->deferrable = conform->condeferrable;
+ fkconstraint->initdeferred = conform->condeferred;
+ fkconstraint->is_enforced = conform->conenforced;
+ fkconstraint->location = -1;
+ fkconstraint->pktable = NULL;
+ fkconstraint->fk_attrs = NIL;
+ fkconstraint->pk_attrs = NIL;
+ fkconstraint->fk_matchtype = conform->confmatchtype;
+ fkconstraint->fk_upd_action = conform->confupdtype;
+ fkconstraint->fk_del_action = conform->confdeltype;
+ fkconstraint->fk_del_set_cols = NIL;
+ fkconstraint->old_conpfeqop = NIL;
+ fkconstraint->old_pktable_oid = InvalidOid;
+ fkconstraint->skip_validation = false;
+ fkconstraint->initially_valid = true;
- createForeignKeyActionTriggers(partRel, conform->confrelid,
- fkconstraint, fk->conoid,
- conform->conindid,
- InvalidOid, InvalidOid,
- NULL, NULL);
+ createForeignKeyActionTriggers(partRel, conform->confrelid,
+ fkconstraint, fk->conoid,
+ conform->conindid,
+ InvalidOid, InvalidOid,
+ NULL, NULL);
+ }
ReleaseSysCache(contup);
}
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 437a0b9f2f0..6a68e32d623 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -4040,6 +4040,7 @@ ColConstraintElem:
n->fk_del_set_cols = ($5)->deleteAction->cols;
n->skip_validation = false;
n->initially_valid = true;
+ n->is_enforced = true;
$$ = (Node *) n;
}
;
@@ -4301,7 +4302,7 @@ ConstraintElem:
processCASbits($12, @12, "FOREIGN KEY",
&n->deferrable, &n->initdeferred,
&n->skip_validation, NULL,
- NULL,
+ &n->is_enforced,
yyscanner);
n->initially_valid = !n->skip_validation;
$$ = (Node *) n;
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 024cc256b7d..8dad6bfacb9 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -3818,7 +3818,8 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
case CONSTR_ATTR_ENFORCED:
if (lastprimarycon &&
- lastprimarycon->contype != CONSTR_CHECK)
+ lastprimarycon->contype != CONSTR_CHECK &&
+ lastprimarycon->contype != CONSTR_FOREIGN)
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("misplaced ENFORCED clause"),
@@ -3834,7 +3835,8 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
case CONSTR_ATTR_NOT_ENFORCED:
if (lastprimarycon &&
- lastprimarycon->contype != CONSTR_CHECK)
+ lastprimarycon->contype != CONSTR_CHECK &&
+ lastprimarycon->contype != CONSTR_FOREIGN)
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("misplaced NOT ENFORCED clause"),
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 711a5290729..029676e0227 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4738,6 +4738,7 @@ RelationGetFKeyList(Relation relation)
info->conoid = constraint->oid;
info->conrelid = constraint->conrelid;
info->confrelid = constraint->confrelid;
+ info->conenforced = constraint->conenforced;
DeconstructFkConstraintRow(htup, &info->nkeys,
info->conkey,
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 87002049538..2db011ddfb7 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -284,6 +284,9 @@ typedef struct ForeignKeyCacheInfo
/* number of columns in the foreign key */
int nkeys;
+ /* Is enforced ? */
+ bool conenforced;
+
/*
* these arrays each have nkeys valid entries:
*/
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 8c04a24b37d..36d3fdddaa5 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -1,12 +1,43 @@
--
-- FOREIGN KEY
--
--- MATCH FULL
+-- NOT ENFORCED
--
-- First test, check and cascade
--
CREATE TABLE PKTABLE ( ptest1 int PRIMARY KEY, ptest2 text );
-CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE, ftest2 int );
+CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE NOT ENFORCED,
+ ftest2 int );
+-- Inserting into the foreign key table will not result in an error, even if
+-- there is no matching key in the referenced table.
+INSERT INTO FKTABLE VALUES (1, 2);
+INSERT INTO FKTABLE VALUES (2, 3);
+-- Check FKTABLE
+SELECT * FROM FKTABLE;
+ ftest1 | ftest2
+--------+--------
+ 1 | 2
+ 2 | 3
+(2 rows)
+
+-- Changing it back to ENFORCED will result in an error due to validation of
+-- existing rows.
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_fkey"
+DETAIL: Key (ftest1)=(1) is not present in table "pktable".
+-- Remove any existing rows that violate the constraint, then attempt to change
+-- it.
+TRUNCATE FKTABLE;
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+-- Any further inserts will fail due to the enforcement.
+INSERT INTO FKTABLE VALUES (3, 4);
+ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_fkey"
+DETAIL: Key (ftest1)=(3) is not present in table "pktable".
+--
+-- MATCH FULL
+--
+-- First test, check and cascade
+--
-- Insert test data into PKTABLE
INSERT INTO PKTABLE VALUES (1, 'Test1');
INSERT INTO PKTABLE VALUES (2, 'Test2');
@@ -351,6 +382,8 @@ INSERT INTO FKTABLE VALUES (1, NULL);
ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL;
ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_ftest2_fkey"
DETAIL: MATCH FULL does not allow mixing of null and nonnull key values.
+-- NOT ENFORCED will bypass the initial check.
+ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL NOT ENFORCED;
DROP TABLE FKTABLE;
DROP TABLE PKTABLE;
-- MATCH SIMPLE
@@ -1276,13 +1309,24 @@ INSERT INTO fktable VALUES (0, 20);
ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_fk_fkey"
DETAIL: Key (fk)=(20) is not present in table "pktable".
COMMIT;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT ENFORCED;
+BEGIN;
+-- doesn't match FK, no error.
+UPDATE pktable SET id = 10 WHERE id = 5;
+-- doesn't match PK, no error.
+INSERT INTO fktable VALUES (0, 20);
+ROLLBACK;
-- try additional syntax
-ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE ENFORCED;
-- illegal option
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE INITIALLY DEFERRED;
ERROR: constraint declared INITIALLY DEFERRED must be DEFERRABLE
LINE 1: ...e ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE INITIALLY ...
^
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORCED;
+ERROR: conflicting constraint properties
+LINE 1: ...fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORC...
+ ^
-- test order of firing of FK triggers when several RI-induced changes need to
-- be made to the same row. This was broken by subtransaction-related
-- changes in 8.0.
@@ -1580,7 +1624,8 @@ ALTER TABLE fk_partitioned_fk DROP COLUMN fdrop1;
CREATE TABLE fk_partitioned_fk_1 (fdrop1 int, fdrop2 int, a int, fdrop3 int, b int);
ALTER TABLE fk_partitioned_fk_1 DROP COLUMN fdrop1, DROP COLUMN fdrop2, DROP COLUMN fdrop3;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FROM (0,0) TO (1000,1000);
-ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk;
+ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
@@ -1665,6 +1710,37 @@ Indexes:
Referenced by:
TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b)
+-- Check the exsting FK trigger
+CREATE TEMP TABLE tmp_trg_info AS SELECT tgtype, tgrelid::regclass, tgconstraint
+FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+SELECT count(1) FROM tmp_trg_info;
+ count
+-------
+ 14
+(1 row)
+
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+-- No triggers
+SELECT count(1) FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+ count
+-------
+ 0
+(1 row)
+
+-- Changing it back to ENFORCED will recreate the necessary triggers.
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+-- Should be exactly the same number of triggers found as before
+SELECT COUNT(1) FROM pg_trigger pgt JOIN tmp_trg_info tt
+ON (pgt.tgtype = tt.tgtype AND pgt.tgrelid = tt.tgrelid AND pgt.tgconstraint = tt.tgconstraint);
+ count
+-------
+ 14
+(1 row)
+
ALTER TABLE fk_partitioned_fk DROP CONSTRAINT fk_partitioned_fk_a_b_fkey;
-- done.
DROP TABLE fk_notpartitioned_pk, fk_partitioned_fk;
@@ -1868,6 +1944,40 @@ Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
Foreign-key constraints:
TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
+DROP TABLE fk_partitioned_fk_2;
+CREATE TABLE fk_partitioned_fk_2 (b int, a int,
+ FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
+BEGIN;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have two constraints
+\d fk_partitioned_fk_2
+ Table "public.fk_partitioned_fk_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ b | integer | | |
+ a | integer | | |
+Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
+Foreign-key constraints:
+ "fk_partitioned_fk_2_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED
+ TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
+
+DROP TABLE fk_partitioned_fk_2;
+ROLLBACK;
+BEGIN;
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have only one constraint
+\d fk_partitioned_fk_2
+ Table "public.fk_partitioned_fk_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ b | integer | | |
+ a | integer | | |
+Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
+Foreign-key constraints:
+ TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED
+
+ROLLBACK;
DROP TABLE fk_partitioned_fk_2;
CREATE TABLE fk_partitioned_fk_4 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE) PARTITION BY RANGE (b, a);
CREATE TABLE fk_partitioned_fk_4_1 PARTITION OF fk_partitioned_fk_4 FOR VALUES FROM (1,1) TO (100,100);
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index d1aac5357f0..96813331fc2 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -2,13 +2,39 @@
-- FOREIGN KEY
--
--- MATCH FULL
+-- NOT ENFORCED
--
-- First test, check and cascade
--
CREATE TABLE PKTABLE ( ptest1 int PRIMARY KEY, ptest2 text );
-CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE, ftest2 int );
+CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE NOT ENFORCED,
+ ftest2 int );
+-- Inserting into the foreign key table will not result in an error, even if
+-- there is no matching key in the referenced table.
+INSERT INTO FKTABLE VALUES (1, 2);
+INSERT INTO FKTABLE VALUES (2, 3);
+
+-- Check FKTABLE
+SELECT * FROM FKTABLE;
+
+-- Changing it back to ENFORCED will result in an error due to validation of
+-- existing rows.
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+
+-- Remove any existing rows that violate the constraint, then attempt to change
+-- it.
+TRUNCATE FKTABLE;
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+
+-- Any further inserts will fail due to the enforcement.
+INSERT INTO FKTABLE VALUES (3, 4);
+
+--
+-- MATCH FULL
+--
+-- First test, check and cascade
+--
-- Insert test data into PKTABLE
INSERT INTO PKTABLE VALUES (1, 'Test1');
INSERT INTO PKTABLE VALUES (2, 'Test2');
@@ -230,6 +256,9 @@ INSERT INTO FKTABLE VALUES (1, NULL);
ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL;
+-- NOT ENFORCED will bypass the initial check.
+ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL NOT ENFORCED;
+
DROP TABLE FKTABLE;
DROP TABLE PKTABLE;
@@ -968,10 +997,22 @@ INSERT INTO fktable VALUES (0, 20);
COMMIT;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT ENFORCED;
+
+BEGIN;
+
+-- doesn't match FK, no error.
+UPDATE pktable SET id = 10 WHERE id = 5;
+-- doesn't match PK, no error.
+INSERT INTO fktable VALUES (0, 20);
+
+ROLLBACK;
+
-- try additional syntax
-ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE ENFORCED;
-- illegal option
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORCED;
-- test order of firing of FK triggers when several RI-induced changes need to
-- be made to the same row. This was broken by subtransaction-related
@@ -1182,7 +1223,8 @@ ALTER TABLE fk_partitioned_fk DROP COLUMN fdrop1;
CREATE TABLE fk_partitioned_fk_1 (fdrop1 int, fdrop2 int, a int, fdrop3 int, b int);
ALTER TABLE fk_partitioned_fk_1 DROP COLUMN fdrop1, DROP COLUMN fdrop2, DROP COLUMN fdrop3;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FROM (0,0) TO (1000,1000);
-ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk;
+ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
@@ -1236,6 +1278,27 @@ UPDATE fk_notpartitioned_pk SET b = 1502 WHERE a = 1500;
UPDATE fk_notpartitioned_pk SET b = 2504 WHERE a = 2500;
-- check psql behavior
\d fk_notpartitioned_pk
+
+-- Check the exsting FK trigger
+CREATE TEMP TABLE tmp_trg_info AS SELECT tgtype, tgrelid::regclass, tgconstraint
+FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+SELECT count(1) FROM tmp_trg_info;
+
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+-- No triggers
+SELECT count(1) FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+
+-- Changing it back to ENFORCED will recreate the necessary triggers.
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+
+-- Should be exactly the same number of triggers found as before
+SELECT COUNT(1) FROM pg_trigger pgt JOIN tmp_trg_info tt
+ON (pgt.tgtype = tt.tgtype AND pgt.tgrelid = tt.tgrelid AND pgt.tgconstraint = tt.tgconstraint);
+
ALTER TABLE fk_partitioned_fk DROP CONSTRAINT fk_partitioned_fk_a_b_fkey;
-- done.
DROP TABLE fk_notpartitioned_pk, fk_partitioned_fk;
@@ -1372,6 +1435,22 @@ ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN
\d fk_partitioned_fk_2
DROP TABLE fk_partitioned_fk_2;
+CREATE TABLE fk_partitioned_fk_2 (b int, a int,
+ FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
+BEGIN;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have two constraints
+\d fk_partitioned_fk_2
+DROP TABLE fk_partitioned_fk_2;
+ROLLBACK;
+BEGIN;
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have only one constraint
+\d fk_partitioned_fk_2
+ROLLBACK;
+DROP TABLE fk_partitioned_fk_2;
+
CREATE TABLE fk_partitioned_fk_4 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE) PARTITION BY RANGE (b, a);
CREATE TABLE fk_partitioned_fk_4_1 PARTITION OF fk_partitioned_fk_4 FOR VALUES FROM (1,1) TO (100,100);
CREATE TABLE fk_partitioned_fk_4_2 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE SET NULL);
--
2.18.0
On Tue, Oct 8, 2024, at 11:06, Amul Sul wrote:
The attached patch proposes adding the ability to define CHECK and
FOREIGN KEY constraints as NOT ENFORCED.
Thanks for working on this!
Adding NOT ENFORCED to CHECK constraints is simple, see 0001 patch,
I've looked at the 0001 patch and think it looks simple and straight forward.
but implementing it for FOREIGN KEY constraints requires more code due
to triggers, see 0002 - 0005 patches.
I can't say that much yet about the code changes in 0002 - 0005 yet,
but I've tested the patches and successfully experimented with the feature.
Also think the documentation is good and sound. Only found a minor typo:
- Adding a enforced <literal>CHECK</literal> or <literal>NOT NULL</literal>
+ Adding an enforced <literal>CHECK</literal> or <literal>NOT NULL</literal>
There are various approaches for
implementing NOT ENFORCED foreign keys, what I thought of:1. When defining a NOT ENFORCED foreign key, skip the creation of
triggers used for referential integrity check, while defining an
ENFORCED foreign key, remain the same as the current behaviour. If an
existing foreign key is changed to NOT ENFORCED, the triggers are
dropped, and when switching it back to ENFORCED, the triggers are
recreated.2. Another approach could be to create the NOT ENFORCED constraint
with the triggers as usual, but disable those triggers by updating the
pg_trigger catalog so that they are never executed for the check. And
enable them when the constraint is changed back to ENFORCED.3. Similarly, a final approach would involve updating the logic where
trigger execution is decided and skipping the execution if the
constraint is not enforced, rather than modifying the pg_trigger
catalog.In the attached patch, the first approach has been implemented. This
requires more code changes but prevents unused triggers from being
left in the database and avoids the need for changes all over the
place to skip trigger execution, which could be missed in future code
additions.
I also like the first approach, since I think it's nice the pg_trigger
entires are inserted / deleted upon enforced / not enforced.
The ALTER CONSTRAINT operation in the patch added code to handle
dropping and recreating triggers. An alternative approach would be to
simplify the process by dropping and recreating the FK constraint,
which would automatically handle skipping or creating triggers for NOT
ENFORCED or ENFORCED FK constraints. However, I wasn't sure if this
was the right approach, as I couldn't find any existing ALTER
operations that follow this pattern.
I think the current approach of dropping and recreating the triggers is best,
since if we would instead be dropping and recreating the FK constraint,
that would cause problems if some other future SQL feature would need to
introduce dependencies on the FK constraints via pg_depend.
Best regards,
Joel
On 2024-10-09 We 5:14 AM, Joel Jacobson wrote:
On Tue, Oct 8, 2024, at 11:06, Amul Sul wrote:
The attached patch proposes adding the ability to define CHECK and
FOREIGN KEY constraints as NOT ENFORCED.Thanks for working on this!
Adding NOT ENFORCED to CHECK constraints is simple, see 0001 patch,
I've looked at the 0001 patch and think it looks simple and straight forward.
but implementing it for FOREIGN KEY constraints requires more code due
to triggers, see 0002 - 0005 patches.I can't say that much yet about the code changes in 0002 - 0005 yet,
but I've tested the patches and successfully experimented with the feature.Also think the documentation is good and sound. Only found a minor typo: - Adding a enforced <literal>CHECK</literal> or <literal>NOT NULL</literal> + Adding an enforced <literal>CHECK</literal> or <literal>NOT NULL</literal>There are various approaches for
implementing NOT ENFORCED foreign keys, what I thought of:1. When defining a NOT ENFORCED foreign key, skip the creation of
triggers used for referential integrity check, while defining an
ENFORCED foreign key, remain the same as the current behaviour. If an
existing foreign key is changed to NOT ENFORCED, the triggers are
dropped, and when switching it back to ENFORCED, the triggers are
recreated.2. Another approach could be to create the NOT ENFORCED constraint
with the triggers as usual, but disable those triggers by updating the
pg_trigger catalog so that they are never executed for the check. And
enable them when the constraint is changed back to ENFORCED.3. Similarly, a final approach would involve updating the logic where
trigger execution is decided and skipping the execution if the
constraint is not enforced, rather than modifying the pg_trigger
catalog.In the attached patch, the first approach has been implemented. This
requires more code changes but prevents unused triggers from being
left in the database and avoids the need for changes all over the
place to skip trigger execution, which could be missed in future code
additions.I also like the first approach, since I think it's nice the pg_trigger
entires are inserted / deleted upon enforced / not enforced.
I also prefer this, as it gets us out from any possible dance with
enabling / disabling triggers.
cheers
andrew
--
Andrew Dunstan
EDB:https://www.enterprisedb.com
On Wed, Oct 9, 2024 at 2:44 PM Joel Jacobson <joel@compiler.org> wrote:
On Tue, Oct 8, 2024, at 11:06, Amul Sul wrote:
The attached patch proposes adding the ability to define CHECK and
FOREIGN KEY constraints as NOT ENFORCED.Thanks for working on this!
Adding NOT ENFORCED to CHECK constraints is simple, see 0001 patch,
I've looked at the 0001 patch and think it looks simple and straight forward.
Thanks for looking into it.
but implementing it for FOREIGN KEY constraints requires more code due
to triggers, see 0002 - 0005 patches.I can't say that much yet about the code changes in 0002 - 0005 yet,
but I've tested the patches and successfully experimented with the feature.Also think the documentation is good and sound. Only found a minor typo: - Adding a enforced <literal>CHECK</literal> or <literal>NOT NULL</literal> + Adding an enforced <literal>CHECK</literal> or <literal>NOT NULL</literal>
Ok, will fix it in the next version.
There are various approaches for
implementing NOT ENFORCED foreign keys, what I thought of:1. When defining a NOT ENFORCED foreign key, skip the creation of
triggers used for referential integrity check, while defining an
ENFORCED foreign key, remain the same as the current behaviour. If an
existing foreign key is changed to NOT ENFORCED, the triggers are
dropped, and when switching it back to ENFORCED, the triggers are
recreated.2. Another approach could be to create the NOT ENFORCED constraint
with the triggers as usual, but disable those triggers by updating the
pg_trigger catalog so that they are never executed for the check. And
enable them when the constraint is changed back to ENFORCED.3. Similarly, a final approach would involve updating the logic where
trigger execution is decided and skipping the execution if the
constraint is not enforced, rather than modifying the pg_trigger
catalog.In the attached patch, the first approach has been implemented. This
requires more code changes but prevents unused triggers from being
left in the database and avoids the need for changes all over the
place to skip trigger execution, which could be missed in future code
additions.I also like the first approach, since I think it's nice the pg_trigger
entires are inserted / deleted upon enforced / not enforced.The ALTER CONSTRAINT operation in the patch added code to handle
dropping and recreating triggers. An alternative approach would be to
simplify the process by dropping and recreating the FK constraint,
which would automatically handle skipping or creating triggers for NOT
ENFORCED or ENFORCED FK constraints. However, I wasn't sure if this
was the right approach, as I couldn't find any existing ALTER
operations that follow this pattern.I think the current approach of dropping and recreating the triggers is best,
since if we would instead be dropping and recreating the FK constraint,
that would cause problems if some other future SQL feature would need to
introduce dependencies on the FK constraints via pg_depend.
Yes, that was my initial thought as well, and recreating the
dependencies would be both painful and prone to bugs.
Regards,
Amul
On Wed, Oct 9, 2024 at 6:45 PM Andrew Dunstan <andrew@dunslane.net> wrote:
On 2024-10-09 We 5:14 AM, Joel Jacobson wrote:
On Tue, Oct 8, 2024, at 11:06, Amul Sul wrote:
The attached patch proposes adding the ability to define CHECK and
FOREIGN KEY constraints as NOT ENFORCED.Thanks for working on this!
Adding NOT ENFORCED to CHECK constraints is simple, see 0001 patch,
I've looked at the 0001 patch and think it looks simple and straight forward.
but implementing it for FOREIGN KEY constraints requires more code due
to triggers, see 0002 - 0005 patches.I can't say that much yet about the code changes in 0002 - 0005 yet,
but I've tested the patches and successfully experimented with the feature.Also think the documentation is good and sound. Only found a minor typo: - Adding a enforced <literal>CHECK</literal> or <literal>NOT NULL</literal> + Adding an enforced <literal>CHECK</literal> or <literal>NOT NULL</literal>There are various approaches for
implementing NOT ENFORCED foreign keys, what I thought of:1. When defining a NOT ENFORCED foreign key, skip the creation of
triggers used for referential integrity check, while defining an
ENFORCED foreign key, remain the same as the current behaviour. If an
existing foreign key is changed to NOT ENFORCED, the triggers are
dropped, and when switching it back to ENFORCED, the triggers are
recreated.2. Another approach could be to create the NOT ENFORCED constraint
with the triggers as usual, but disable those triggers by updating the
pg_trigger catalog so that they are never executed for the check. And
enable them when the constraint is changed back to ENFORCED.3. Similarly, a final approach would involve updating the logic where
trigger execution is decided and skipping the execution if the
constraint is not enforced, rather than modifying the pg_trigger
catalog.In the attached patch, the first approach has been implemented. This
requires more code changes but prevents unused triggers from being
left in the database and avoids the need for changes all over the
place to skip trigger execution, which could be missed in future code
additions.I also like the first approach, since I think it's nice the pg_trigger
entires are inserted / deleted upon enforced / not enforced.I also prefer this, as it gets us out from any possible dance with enabling / disabling triggers.
Agreed. Thanks for the inputs.
Regards,
Amul.
On Tue, Oct 15, 2024 at 10:00 AM Amul Sul <sulamul@gmail.com> wrote:
On Wed, Oct 9, 2024 at 2:44 PM Joel Jacobson <joel@compiler.org> wrote:
On Tue, Oct 8, 2024, at 11:06, Amul Sul wrote:
The attached patch proposes adding the ability to define CHECK and
FOREIGN KEY constraints as NOT ENFORCED.Thanks for working on this!
Adding NOT ENFORCED to CHECK constraints is simple, see 0001 patch,
I've looked at the 0001 patch and think it looks simple and straight forward.
Thanks for looking into it.
but implementing it for FOREIGN KEY constraints requires more code due
to triggers, see 0002 - 0005 patches.I can't say that much yet about the code changes in 0002 - 0005 yet,
but I've tested the patches and successfully experimented with the feature.Also think the documentation is good and sound. Only found a minor typo: - Adding a enforced <literal>CHECK</literal> or <literal>NOT NULL</literal> + Adding an enforced <literal>CHECK</literal> or <literal>NOT NULL</literal>Ok, will fix it in the next version.
Attached is the rebased version on the latest master(5b0c46ea093),
including the aforesaid fix.
Regards,
Amul
Attachments:
v2-0001-Add-support-for-NOT-ENFORCED-in-CHECK-constraints.patchapplication/octet-stream; name=v2-0001-Add-support-for-NOT-ENFORCED-in-CHECK-constraints.patchDownload
From 6f1b2af3b38b4d0e9258bc0ff500d003882dd8bd Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Fri, 4 Oct 2024 18:22:42 +0530
Subject: [PATCH v2 1/5] Add support for NOT ENFORCED in CHECK constraints.
This added basic infrastructure for the NOT ENFORCED/ENFORCED
constraint supports where incluing grammar and catalog flags.
Note that CHECK constraints do not currently support ALTER operations,
so changing the enforceability of an existing constraint isn't
possible without dropping and recreating it. We could consider
adding support for altering CHECK constraints either in this
patch series or as a separatly.
---
doc/src/sgml/ddl.sgml | 8 ++-
doc/src/sgml/ref/alter_table.sgml | 14 +++--
doc/src/sgml/ref/create_table.sgml | 18 +++++-
src/backend/access/common/tupdesc.c | 4 +-
src/backend/catalog/heap.c | 12 ++--
src/backend/catalog/index.c | 1 +
src/backend/catalog/pg_constraint.c | 5 ++
src/backend/commands/tablecmds.c | 54 +++++++++++------
src/backend/commands/trigger.c | 1 +
src/backend/commands/typecmds.c | 14 +++++
src/backend/executor/execMain.c | 8 ++-
src/backend/parser/gram.y | 72 ++++++++++++++++++-----
src/backend/parser/parse_utilcmd.c | 39 ++++++++++++
src/backend/utils/adt/ruleutils.c | 7 +++
src/backend/utils/cache/relcache.c | 1 +
src/include/access/tupdesc.h | 1 +
src/include/catalog/heap.h | 1 +
src/include/catalog/pg_constraint.h | 2 +
src/include/nodes/parsenodes.h | 3 +
src/include/parser/kwlist.h | 1 +
src/test/regress/expected/constraints.out | 22 ++++++-
src/test/regress/sql/constraints.sql | 15 ++++-
22 files changed, 250 insertions(+), 53 deletions(-)
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index f02f67d7b86..5f801903ddd 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -582,7 +582,10 @@ CREATE TABLE products (
consists of the key word <literal>CHECK</literal> followed by an
expression in parentheses. The check constraint expression should
involve the column thus constrained, otherwise the constraint
- would not make too much sense.
+ would not make too much sense. The check constraint expression can be
+ followed by the <literal>ENFORCED</literal>, which is the default, or
+ <literal>NOT ENFORCED</literal> keyword, determining whether the
+ constraint check is performed or skipped after each statement.
</para>
<indexterm>
@@ -718,7 +721,8 @@ CREATE TABLE products (
to implement that. (This approach avoids the dump/restore problem because
<application>pg_dump</application> does not reinstall triggers until after
restoring data, so that the check will not be enforced during a
- dump/restore.)
+ dump/restore.) You can add that CHECK constraint with <literal>NOT ENFORCED</literal>,
+ allowing the underlying constraint rule to be represented without being enforced.
</para>
</note>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 36770c012a6..88d9169d7a6 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -58,7 +58,7 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable>
ALTER [ COLUMN ] <replaceable class="parameter">column_name</replaceable> SET COMPRESSION <replaceable class="parameter">compression_method</replaceable>
ADD <replaceable class="parameter">table_constraint</replaceable> [ NOT VALID ]
ADD <replaceable class="parameter">table_constraint_using_index</replaceable>
- ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+ ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]
VALIDATE CONSTRAINT <replaceable class="parameter">constraint_name</replaceable>
DROP CONSTRAINT [ IF EXISTS ] <replaceable class="parameter">constraint_name</replaceable> [ RESTRICT | CASCADE ]
DISABLE TRIGGER [ <replaceable class="parameter">trigger_name</replaceable> | ALL | USER ]
@@ -108,7 +108,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
PRIMARY KEY <replaceable class="parameter">index_parameters</replaceable> |
REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> ) ] [ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ]
[ ON DELETE <replaceable class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ] }
-[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]
<phrase>and <replaceable class="parameter">table_constraint</replaceable> is:</phrase>
@@ -119,7 +119,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
EXCLUDE [ USING <replaceable class="parameter">index_method</replaceable> ] ( <replaceable class="parameter">exclude_element</replaceable> WITH <replaceable class="parameter">operator</replaceable> [, ... ] ) <replaceable class="parameter">index_parameters</replaceable> [ WHERE ( <replaceable class="parameter">predicate</replaceable> ) ] |
FOREIGN KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> [, ... ] ) ]
[ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ] [ ON DELETE <replaceable class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ] }
-[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]
<phrase>and <replaceable class="parameter">table_constraint_using_index</replaceable> is:</phrase>
@@ -1427,9 +1427,11 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</para>
<para>
- Adding a <literal>CHECK</literal> or <literal>NOT NULL</literal> constraint requires
- scanning the table to verify that existing rows meet the constraint,
- but does not require a table rewrite.
+ Adding an enforced <literal>CHECK</literal> or <literal>NOT NULL</literal>
+ constraint requires scanning the table to verify that existing rows meet the
+ constraint, but does not require a table rewrite. If a <literal>CHECK</literal>
+ constraint is added as <literal>NOT ENFORCED</literal>, the validation will
+ not be performed.
</para>
<para>
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index 83859bac76f..55b51602136 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -71,7 +71,7 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI
PRIMARY KEY <replaceable class="parameter">index_parameters</replaceable> |
REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> ) ] [ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ]
[ ON DELETE <replaceable class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ] }
-[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]
<phrase>and <replaceable class="parameter">table_constraint</replaceable> is:</phrase>
@@ -83,7 +83,7 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI
FOREIGN KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">column_name</replaceable> ] ) REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">column_name</replaceable> ] ) ]
[ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ] [ ON DELETE <replaceable
class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ] }
-[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]
<phrase>and <replaceable class="parameter">like_option</replaceable> is:</phrase>
@@ -1362,6 +1362,20 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</listitem>
</varlistentry>
+ <varlistentry id="sql-createtable-parms-enforce">
+ <term><literal>ENFORCED</literal></term>
+ <term><literal>NOT ENFORCED</literal></term>
+ <listitem>
+ <para>
+ This is currently only allowed for <literal>CHECK</literal> constraints.
+ If the constraint is <literal>NOT ENFORCED</literal>, this clause
+ specifies that the constraint check will be skipped. When the constraint
+ is <literal>ENFORCED</literal>, check is performed after each statement.
+ This is the default.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="sql-createtable-method">
<term><literal>USING <replaceable class="parameter">method</replaceable></literal></term>
<listitem>
diff --git a/src/backend/access/common/tupdesc.c b/src/backend/access/common/tupdesc.c
index 47379fef220..41dd00e5547 100644
--- a/src/backend/access/common/tupdesc.c
+++ b/src/backend/access/common/tupdesc.c
@@ -226,6 +226,7 @@ CreateTupleDescCopyConstr(TupleDesc tupdesc)
cpy->check[i].ccbin = pstrdup(constr->check[i].ccbin);
cpy->check[i].ccvalid = constr->check[i].ccvalid;
cpy->check[i].ccnoinherit = constr->check[i].ccnoinherit;
+ cpy->check[i].ccenforced = constr->check[i].ccenforced;
}
}
@@ -548,7 +549,8 @@ equalTupleDescs(TupleDesc tupdesc1, TupleDesc tupdesc2)
if (!(strcmp(check1->ccname, check2->ccname) == 0 &&
strcmp(check1->ccbin, check2->ccbin) == 0 &&
check1->ccvalid == check2->ccvalid &&
- check1->ccnoinherit == check2->ccnoinherit))
+ check1->ccnoinherit == check2->ccnoinherit &&
+ check1->ccenforced == check2->ccenforced))
return false;
}
}
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index c54a543c536..39bc76e3361 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -103,7 +103,7 @@ static ObjectAddress AddNewRelationType(const char *typeName,
static void RelationRemoveInheritance(Oid relid);
static Oid StoreRelCheck(Relation rel, const char *ccname, Node *expr,
bool is_validated, bool is_local, int16 inhcount,
- bool is_no_inherit, bool is_internal);
+ bool is_no_inherit, bool is_enforced, bool is_internal);
static void StoreConstraints(Relation rel, List *cooked_constraints,
bool is_internal);
static bool MergeWithExistingConstraint(Relation rel, const char *ccname, Node *expr,
@@ -2075,7 +2075,7 @@ SetAttrMissing(Oid relid, char *attname, char *value)
static Oid
StoreRelCheck(Relation rel, const char *ccname, Node *expr,
bool is_validated, bool is_local, int16 inhcount,
- bool is_no_inherit, bool is_internal)
+ bool is_no_inherit, bool is_enforced, bool is_internal)
{
char *ccbin;
List *varList;
@@ -2141,6 +2141,7 @@ StoreRelCheck(Relation rel, const char *ccname, Node *expr,
false, /* Is Deferrable */
false, /* Is Deferred */
is_validated,
+ is_enforced, /* Is enforced */
InvalidOid, /* no parent constraint */
RelationGetRelid(rel), /* relation */
attNos, /* attrs in the constraint */
@@ -2214,7 +2215,7 @@ StoreConstraints(Relation rel, List *cooked_constraints, bool is_internal)
StoreRelCheck(rel, con->name, con->expr,
!con->skip_validation, con->is_local,
con->inhcount, con->is_no_inherit,
- is_internal);
+ con->is_enforced, is_internal);
numchecks++;
break;
default:
@@ -2347,6 +2348,7 @@ AddRelationNewConstraints(Relation rel,
cooked->attnum = colDef->attnum;
cooked->expr = expr;
cooked->skip_validation = false;
+ cooked->is_enforced = true;
cooked->is_local = is_local;
cooked->inhcount = is_local ? 0 : 1;
cooked->is_no_inherit = false;
@@ -2465,7 +2467,8 @@ AddRelationNewConstraints(Relation rel,
*/
constrOid =
StoreRelCheck(rel, ccname, expr, cdef->initially_valid, is_local,
- is_local ? 0 : 1, cdef->is_no_inherit, is_internal);
+ is_local ? 0 : 1, cdef->is_no_inherit,
+ cdef->is_enforced, is_internal);
numchecks++;
@@ -2476,6 +2479,7 @@ AddRelationNewConstraints(Relation rel,
cooked->attnum = 0;
cooked->expr = expr;
cooked->skip_validation = cdef->skip_validation;
+ cooked->is_enforced = cdef->is_enforced;
cooked->is_local = is_local;
cooked->inhcount = is_local ? 0 : 1;
cooked->is_no_inherit = cdef->is_no_inherit;
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index f9bb721c5fe..d8c88d4294f 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -1958,6 +1958,7 @@ index_constraint_create(Relation heapRelation,
deferrable,
initdeferred,
true,
+ true, /* Is enforced */
parentConstraintId,
RelationGetRelid(heapRelation),
indexInfo->ii_IndexAttrNumbers,
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index 54f3fb50a57..2b461abf47e 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -53,6 +53,7 @@ CreateConstraintEntry(const char *constraintName,
bool isDeferrable,
bool isDeferred,
bool isValidated,
+ bool isEnforced,
Oid parentConstrId,
Oid relId,
const int16 *constraintKey,
@@ -98,6 +99,9 @@ CreateConstraintEntry(const char *constraintName,
ObjectAddresses *addrs_auto;
ObjectAddresses *addrs_normal;
+ /* Only CHECK constraint can be not enforced */
+ Assert(isEnforced || constraintType == CONSTRAINT_CHECK);
+
conDesc = table_open(ConstraintRelationId, RowExclusiveLock);
Assert(constraintName);
@@ -182,6 +186,7 @@ CreateConstraintEntry(const char *constraintName,
values[Anum_pg_constraint_condeferrable - 1] = BoolGetDatum(isDeferrable);
values[Anum_pg_constraint_condeferred - 1] = BoolGetDatum(isDeferred);
values[Anum_pg_constraint_convalidated - 1] = BoolGetDatum(isValidated);
+ values[Anum_pg_constraint_conenforced - 1] = BoolGetDatum(isEnforced);
values[Anum_pg_constraint_conrelid - 1] = ObjectIdGetDatum(relId);
values[Anum_pg_constraint_contypid - 1] = ObjectIdGetDatum(domainId);
values[Anum_pg_constraint_conindid - 1] = ObjectIdGetDatum(indexRelId);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 4345b96de5e..96b77cb3484 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -372,7 +372,7 @@ static void RangeVarCallbackForTruncate(const RangeVar *relation,
Oid relId, Oid oldRelId, void *arg);
static List *MergeAttributes(List *columns, const List *supers, char relpersistence,
bool is_partition, List **supconstr);
-static List *MergeCheckConstraint(List *constraints, const char *name, Node *expr);
+static List *MergeCheckConstraint(List *constraints, const char *name, Node *expr, bool is_enforced);
static void MergeChildAttribute(List *inh_columns, int exist_attno, int newcol_attno, const ColumnDef *newdef);
static ColumnDef *MergeInheritedAttribute(List *inh_columns, int exist_attno, const ColumnDef *newdef);
static void MergeAttributesIntoExisting(Relation child_rel, Relation parent_rel, bool ispartition);
@@ -964,6 +964,7 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
cooked->attnum = attnum;
cooked->expr = colDef->cooked_default;
cooked->skip_validation = false;
+ cooked->is_enforced = true;
cooked->is_local = true; /* not used for defaults */
cooked->inhcount = 0; /* ditto */
cooked->is_no_inherit = false;
@@ -2842,7 +2843,8 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
name,
RelationGetRelationName(relation))));
- constraints = MergeCheckConstraint(constraints, name, expr);
+ constraints = MergeCheckConstraint(constraints, name, expr,
+ check[i].ccenforced);
}
}
@@ -3044,7 +3046,7 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
* the list.
*/
static List *
-MergeCheckConstraint(List *constraints, const char *name, Node *expr)
+MergeCheckConstraint(List *constraints, const char *name, Node *expr, bool is_enforced)
{
ListCell *lc;
CookedConstraint *newcon;
@@ -3085,6 +3087,7 @@ MergeCheckConstraint(List *constraints, const char *name, Node *expr)
newcon->name = pstrdup(name);
newcon->expr = expr;
newcon->inhcount = 1;
+ newcon->is_enforced = is_enforced;
return lappend(constraints, newcon);
}
@@ -9481,6 +9484,9 @@ ATAddCheckConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
{
CookedConstraint *ccon = (CookedConstraint *) lfirst(lcon);
+ /* Only CHECK constraint can be not enforced */
+ Assert(ccon->is_enforced || ccon->contype == CONSTRAINT_CHECK);
+
if (!ccon->skip_validation)
{
NewConstraint *newcon;
@@ -10253,6 +10259,7 @@ addFkConstraint(addFkConstraintSides fkside,
fkconstraint->deferrable,
fkconstraint->initdeferred,
fkconstraint->initially_valid,
+ true, /* Is enforced */
parentConstr,
RelationGetRelid(rel),
fkattnum,
@@ -11922,22 +11929,29 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
table_close(childrel, NoLock);
}
- /* Queue validation for phase 3 */
- newcon = (NewConstraint *) palloc0(sizeof(NewConstraint));
- newcon->name = constrName;
- newcon->contype = CONSTR_CHECK;
- newcon->refrelid = InvalidOid;
- newcon->refindid = InvalidOid;
- newcon->conid = con->oid;
+ /*
+ * Queue validation for phase 3 only if constraint is enforced;
+ * otherwise, adding it to the validation queue won't be very
+ * effective, as the verification will be skipped.
+ */
+ if (con->conenforced)
+ {
+ newcon = (NewConstraint *) palloc0(sizeof(NewConstraint));
+ newcon->name = constrName;
+ newcon->contype = CONSTR_CHECK;
+ newcon->refrelid = InvalidOid;
+ newcon->refindid = InvalidOid;
+ newcon->conid = con->oid;
- val = SysCacheGetAttrNotNull(CONSTROID, tuple,
- Anum_pg_constraint_conbin);
- conbin = TextDatumGetCString(val);
- newcon->qual = (Node *) stringToNode(conbin);
+ val = SysCacheGetAttrNotNull(CONSTROID, tuple,
+ Anum_pg_constraint_conbin);
+ conbin = TextDatumGetCString(val);
+ newcon->qual = (Node *) stringToNode(conbin);
- /* Find or create work queue entry for this table */
- tab = ATGetQueueEntry(wqueue, rel);
- tab->constraints = lappend(tab->constraints, newcon);
+ /* Find or create work queue entry for this table */
+ tab = ATGetQueueEntry(wqueue, rel);
+ tab->constraints = lappend(tab->constraints, newcon);
+ }
/*
* Invalidate relcache so that others see the new validated
@@ -11947,7 +11961,9 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
}
/*
- * Now update the catalog, while we have the door open.
+ * Now update the catalog regardless of enforcement; the validated
+ * flag will not take effect until the constraint is marked as
+ * enforced.
*/
copyTuple = heap_copytuple(tuple);
copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
@@ -15952,6 +15968,7 @@ constraints_equivalent(HeapTuple a, HeapTuple b, TupleDesc tupleDesc)
if (acon->condeferrable != bcon->condeferrable ||
acon->condeferred != bcon->condeferred ||
+ acon->conenforced != bcon->conenforced ||
strcmp(decompile_conbin(a, tupleDesc),
decompile_conbin(b, tupleDesc)) != 0)
return false;
@@ -19769,6 +19786,7 @@ DetachAddConstraintIfNeeded(List **wqueue, Relation partRel)
n->cooked_expr = nodeToString(make_ands_explicit(constraintExpr));
n->initially_valid = true;
n->skip_validation = true;
+ n->is_enforced = true;
/* It's a re-add, since it nominally already exists */
ATAddCheckConstraint(wqueue, tab, partRel, n,
true, false, true, ShareUpdateExclusiveLock);
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 09356e46d16..a490d44548a 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -810,6 +810,7 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
stmt->deferrable,
stmt->initdeferred,
true,
+ true, /* Is enforced */
InvalidOid, /* no parent */
RelationGetRelid(rel),
NULL, /* no conkey */
diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c
index 2a6550de907..e77fd2f140a 100644
--- a/src/backend/commands/typecmds.c
+++ b/src/backend/commands/typecmds.c
@@ -1007,6 +1007,12 @@ DefineDomain(CreateDomainStmt *stmt)
errmsg("specifying constraint deferrability not supported for domains")));
break;
+ case CONSTR_ATTR_NOT_ENFORCED:
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("specifying not enforced constraint not supported for domains")));
+ break;
+
default:
elog(ERROR, "unrecognized constraint subtype: %d",
(int) constr->contype);
@@ -2967,6 +2973,12 @@ AlterDomainAddConstraint(List *names, Node *newConstraint,
errmsg("specifying constraint deferrability not supported for domains")));
break;
+ case CONSTR_ATTR_NOT_ENFORCED:
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("specifying not enforced constraint not supported for domains")));
+ break;
+
default:
elog(ERROR, "unrecognized constraint subtype: %d",
(int) constr->contype);
@@ -3597,6 +3609,7 @@ domainAddCheckConstraint(Oid domainOid, Oid domainNamespace, Oid baseTypeOid,
false, /* Is Deferrable */
false, /* Is Deferred */
!constr->skip_validation, /* Is Validated */
+ true, /* Is enforced */
InvalidOid, /* no parent constraint */
InvalidOid, /* not a relation constraint */
NULL,
@@ -3704,6 +3717,7 @@ domainAddNotNullConstraint(Oid domainOid, Oid domainNamespace, Oid baseTypeOid,
false, /* Is Deferrable */
false, /* Is Deferred */
!constr->skip_validation, /* Is Validated */
+ true, /* Is enforced */
InvalidOid, /* no parent constraint */
InvalidOid, /* not a relation constraint */
NULL,
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index cc9a594cba5..cec69afe188 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1746,11 +1746,15 @@ ExecRelCheck(ResultRelInfo *resultRelInfo,
{
oldContext = MemoryContextSwitchTo(estate->es_query_cxt);
resultRelInfo->ri_ConstraintExprs =
- (ExprState **) palloc(ncheck * sizeof(ExprState *));
+ (ExprState **) palloc0(ncheck * sizeof(ExprState *));
for (i = 0; i < ncheck; i++)
{
Expr *checkconstr;
+ /* Skip not enforced constraint */
+ if (!check[i].ccenforced)
+ continue;
+
checkconstr = stringToNode(check[i].ccbin);
resultRelInfo->ri_ConstraintExprs[i] =
ExecPrepareExpr(checkconstr, estate);
@@ -1777,7 +1781,7 @@ ExecRelCheck(ResultRelInfo *resultRelInfo,
* is not to be treated as a failure. Therefore, use ExecCheck not
* ExecQual.
*/
- if (!ExecCheck(checkconstr, econtext))
+ if (checkconstr && !ExecCheck(checkconstr, econtext))
return check[i].ccname;
}
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 89fdb94c237..7c632881226 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -143,6 +143,8 @@ typedef struct KeyActions
#define CAS_INITIALLY_DEFERRED 0x08
#define CAS_NOT_VALID 0x10
#define CAS_NO_INHERIT 0x20
+#define CAS_NOT_ENFORCED 0x40
+#define CAS_ENFORCED 0x80
#define parser_yyerror(msg) scanner_yyerror(msg, yyscanner)
@@ -197,7 +199,7 @@ static void SplitColQualList(List *qualList,
core_yyscan_t yyscanner);
static void processCASbits(int cas_bits, int location, const char *constrType,
bool *deferrable, bool *initdeferred, bool *not_valid,
- bool *no_inherit, core_yyscan_t yyscanner);
+ bool *no_inherit, bool *is_enforced, core_yyscan_t yyscanner);
static PartitionStrategy parsePartitionStrategy(char *strategy, int location,
core_yyscan_t yyscanner);
static void preprocess_pubobj_list(List *pubobjspec_list,
@@ -711,9 +713,9 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
DETACH DICTIONARY DISABLE_P DISCARD DISTINCT DO DOCUMENT_P DOMAIN_P
DOUBLE_P DROP
- EACH ELSE EMPTY_P ENABLE_P ENCODING ENCRYPTED END_P ENUM_P ERROR_P ESCAPE
- EVENT EXCEPT EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION
- EXTENSION EXTERNAL EXTRACT
+ EACH ELSE EMPTY_P ENABLE_P ENCODING ENCRYPTED END_P ENFORCED ENUM_P ERROR_P
+ ESCAPE EVENT EXCEPT EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN
+ EXPRESSION EXTENSION EXTERNAL EXTRACT
FALSE_P FAMILY FETCH FILTER FINALIZE FIRST_P FLOAT_P FOLLOWING FOR
FORCE FOREIGN FORMAT FORWARD FREEZE FROM FULL FUNCTION FUNCTIONS
@@ -2658,7 +2660,7 @@ alter_table_cmd:
processCASbits($4, @4, "ALTER CONSTRAINT statement",
&c->deferrable,
&c->initdeferred,
- NULL, NULL, yyscanner);
+ NULL, NULL, &c->is_enforced, yyscanner);
$$ = (Node *) n;
}
/* ALTER TABLE <name> VALIDATE CONSTRAINT ... */
@@ -3960,6 +3962,7 @@ ColConstraintElem:
n->cooked_expr = NULL;
n->skip_validation = false;
n->initially_valid = true;
+ n->is_enforced = true;
$$ = (Node *) n;
}
| DEFAULT b_expr
@@ -4084,6 +4087,22 @@ ConstraintAttr:
n->location = @1;
$$ = (Node *) n;
}
+ | ENFORCED
+ {
+ Constraint *n = makeNode(Constraint);
+
+ n->contype = CONSTR_ATTR_ENFORCED;
+ n->location = @1;
+ $$ = (Node *) n;
+ }
+ | NOT ENFORCED
+ {
+ Constraint *n = makeNode(Constraint);
+
+ n->contype = CONSTR_ATTR_NOT_ENFORCED;
+ n->location = @1;
+ $$ = (Node *) n;
+ }
;
@@ -4146,7 +4165,7 @@ ConstraintElem:
n->cooked_expr = NULL;
processCASbits($5, @5, "CHECK",
NULL, NULL, &n->skip_validation,
- &n->is_no_inherit, yyscanner);
+ &n->is_no_inherit, &n->is_enforced, yyscanner);
n->initially_valid = !n->skip_validation;
$$ = (Node *) n;
}
@@ -4166,7 +4185,7 @@ ConstraintElem:
n->indexspace = $9;
processCASbits($10, @10, "UNIQUE",
&n->deferrable, &n->initdeferred, NULL,
- NULL, yyscanner);
+ NULL, NULL, yyscanner);
$$ = (Node *) n;
}
| UNIQUE ExistingIndex ConstraintAttributeSpec
@@ -4182,7 +4201,7 @@ ConstraintElem:
n->indexspace = NULL;
processCASbits($3, @3, "UNIQUE",
&n->deferrable, &n->initdeferred, NULL,
- NULL, yyscanner);
+ NULL, NULL, yyscanner);
$$ = (Node *) n;
}
| PRIMARY KEY '(' columnList opt_without_overlaps ')' opt_c_include opt_definition OptConsTableSpace
@@ -4200,7 +4219,7 @@ ConstraintElem:
n->indexspace = $9;
processCASbits($10, @10, "PRIMARY KEY",
&n->deferrable, &n->initdeferred, NULL,
- NULL, yyscanner);
+ NULL, NULL, yyscanner);
$$ = (Node *) n;
}
| PRIMARY KEY ExistingIndex ConstraintAttributeSpec
@@ -4216,7 +4235,7 @@ ConstraintElem:
n->indexspace = NULL;
processCASbits($4, @4, "PRIMARY KEY",
&n->deferrable, &n->initdeferred, NULL,
- NULL, yyscanner);
+ NULL, NULL, yyscanner);
$$ = (Node *) n;
}
| EXCLUDE access_method_clause '(' ExclusionConstraintList ')'
@@ -4236,7 +4255,7 @@ ConstraintElem:
n->where_clause = $9;
processCASbits($10, @10, "EXCLUDE",
&n->deferrable, &n->initdeferred, NULL,
- NULL, yyscanner);
+ NULL, NULL, yyscanner);
$$ = (Node *) n;
}
| FOREIGN KEY '(' columnList optionalPeriodName ')' REFERENCES qualified_name
@@ -4266,6 +4285,7 @@ ConstraintElem:
processCASbits($12, @12, "FOREIGN KEY",
&n->deferrable, &n->initdeferred,
&n->skip_validation, NULL,
+ NULL,
yyscanner);
n->initially_valid = !n->skip_validation;
$$ = (Node *) n;
@@ -4306,7 +4326,7 @@ DomainConstraintElem:
n->cooked_expr = NULL;
processCASbits($5, @5, "CHECK",
NULL, NULL, &n->skip_validation,
- &n->is_no_inherit, yyscanner);
+ &n->is_no_inherit, NULL, yyscanner);
n->initially_valid = !n->skip_validation;
$$ = (Node *) n;
}
@@ -4320,7 +4340,7 @@ DomainConstraintElem:
/* no NOT VALID support yet */
processCASbits($3, @3, "NOT NULL",
NULL, NULL, NULL,
- &n->is_no_inherit, yyscanner);
+ &n->is_no_inherit, NULL, yyscanner);
n->initially_valid = true;
$$ = (Node *) n;
}
@@ -5983,7 +6003,7 @@ CreateTrigStmt:
n->transitionRels = NIL;
processCASbits($11, @11, "TRIGGER",
&n->deferrable, &n->initdeferred, NULL,
- NULL, yyscanner);
+ NULL, NULL, yyscanner);
n->constrrel = $10;
$$ = (Node *) n;
}
@@ -6152,7 +6172,8 @@ ConstraintAttributeSpec:
parser_errposition(@2)));
/* generic message for other conflicts */
if ((newspec & (CAS_NOT_DEFERRABLE | CAS_DEFERRABLE)) == (CAS_NOT_DEFERRABLE | CAS_DEFERRABLE) ||
- (newspec & (CAS_INITIALLY_IMMEDIATE | CAS_INITIALLY_DEFERRED)) == (CAS_INITIALLY_IMMEDIATE | CAS_INITIALLY_DEFERRED))
+ (newspec & (CAS_INITIALLY_IMMEDIATE | CAS_INITIALLY_DEFERRED)) == (CAS_INITIALLY_IMMEDIATE | CAS_INITIALLY_DEFERRED) ||
+ (newspec & (CAS_NOT_ENFORCED | CAS_ENFORCED)) == (CAS_NOT_ENFORCED | CAS_ENFORCED))
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("conflicting constraint properties"),
@@ -6168,6 +6189,8 @@ ConstraintAttributeElem:
| INITIALLY DEFERRED { $$ = CAS_INITIALLY_DEFERRED; }
| NOT VALID { $$ = CAS_NOT_VALID; }
| NO INHERIT { $$ = CAS_NO_INHERIT; }
+ | NOT ENFORCED { $$ = CAS_NOT_ENFORCED; }
+ | ENFORCED { $$ = CAS_ENFORCED; }
;
@@ -17671,6 +17694,7 @@ unreserved_keyword:
| ENABLE_P
| ENCODING
| ENCRYPTED
+ | ENFORCED
| ENUM_P
| ERROR_P
| ESCAPE
@@ -18248,6 +18272,7 @@ bare_label_keyword:
| ENCODING
| ENCRYPTED
| END_P
+ | ENFORCED
| ENUM_P
| ERROR_P
| ESCAPE
@@ -19387,7 +19412,7 @@ SplitColQualList(List *qualList,
static void
processCASbits(int cas_bits, int location, const char *constrType,
bool *deferrable, bool *initdeferred, bool *not_valid,
- bool *no_inherit, core_yyscan_t yyscanner)
+ bool *no_inherit, bool *is_enforced, core_yyscan_t yyscanner)
{
/* defaults */
if (deferrable)
@@ -19396,6 +19421,8 @@ processCASbits(int cas_bits, int location, const char *constrType,
*initdeferred = false;
if (not_valid)
*not_valid = false;
+ if (is_enforced)
+ *is_enforced = true;
if (cas_bits & (CAS_DEFERRABLE | CAS_INITIALLY_DEFERRED))
{
@@ -19448,6 +19475,19 @@ processCASbits(int cas_bits, int location, const char *constrType,
constrType),
parser_errposition(location)));
}
+
+ if (cas_bits & CAS_NOT_ENFORCED)
+ {
+ if (is_enforced)
+ *is_enforced = false;
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ /* translator: %s is CHECK, UNIQUE, or similar */
+ errmsg("%s constraints cannot be marked NOT ENFORCED",
+ constrType),
+ parser_errposition(location)));
+ }
}
/*
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 1e15ce10b48..024cc256b7d 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -835,6 +835,8 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column)
case CONSTR_ATTR_NOT_DEFERRABLE:
case CONSTR_ATTR_DEFERRED:
case CONSTR_ATTR_IMMEDIATE:
+ case CONSTR_ATTR_ENFORCED:
+ case CONSTR_ATTR_NOT_ENFORCED:
/* transformConstraintAttrs took care of these */
break;
@@ -955,6 +957,8 @@ transformTableConstraint(CreateStmtContext *cxt, Constraint *constraint)
case CONSTR_ATTR_NOT_DEFERRABLE:
case CONSTR_ATTR_DEFERRED:
case CONSTR_ATTR_IMMEDIATE:
+ case CONSTR_ATTR_ENFORCED:
+ case CONSTR_ATTR_NOT_ENFORCED:
elog(ERROR, "invalid context for constraint type %d",
constraint->contype);
break;
@@ -1317,6 +1321,7 @@ expandTableLikeClause(RangeVar *heapRel, TableLikeClause *table_like_clause)
n->is_no_inherit = ccnoinherit;
n->raw_expr = NULL;
n->cooked_expr = nodeToString(ccbin_node);
+ n->is_enforced = true;
/* We can skip validation, since the new table should be empty. */
n->skip_validation = true;
@@ -3715,6 +3720,7 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
Constraint *lastprimarycon = NULL;
bool saw_deferrability = false;
bool saw_initially = false;
+ bool saw_enforced = false;
ListCell *clist;
#define SUPPORTS_ATTRS(node) \
@@ -3810,12 +3816,45 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
lastprimarycon->initdeferred = false;
break;
+ case CONSTR_ATTR_ENFORCED:
+ if (lastprimarycon &&
+ lastprimarycon->contype != CONSTR_CHECK)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("misplaced ENFORCED clause"),
+ parser_errposition(cxt->pstate, con->location)));
+ if (saw_enforced)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("multiple ENFORCED/NOT ENFORCED clauses not allowed"),
+ parser_errposition(cxt->pstate, con->location)));
+ saw_enforced = true;
+ lastprimarycon->is_enforced = true;
+ break;
+
+ case CONSTR_ATTR_NOT_ENFORCED:
+ if (lastprimarycon &&
+ lastprimarycon->contype != CONSTR_CHECK)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("misplaced NOT ENFORCED clause"),
+ parser_errposition(cxt->pstate, con->location)));
+ if (saw_enforced)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("multiple ENFORCED/NOT ENFORCED clauses not allowed"),
+ parser_errposition(cxt->pstate, con->location)));
+ saw_enforced = true;
+ lastprimarycon->is_enforced = false;
+ break;
+
default:
/* Otherwise it's not an attribute */
lastprimarycon = con;
/* reset flags for new primary node */
saw_deferrability = false;
saw_initially = false;
+ saw_enforced = false;
break;
}
}
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 2177d17e278..cfdc4e31cd4 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -2372,6 +2372,9 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
appendStringInfoChar(&buf, ')');
}
+ if (!conForm->conenforced)
+ appendStringInfoString(&buf, " NOT ENFORCED");
+
break;
}
case CONSTRAINT_PRIMARY:
@@ -2514,6 +2517,10 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
appendStringInfo(&buf, "CHECK (%s)%s",
consrc,
conForm->connoinherit ? " NO INHERIT" : "");
+
+ if (!conForm->conenforced)
+ appendStringInfoString(&buf, " NOT ENFORCED");
+
break;
}
case CONSTRAINT_TRIGGER:
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 5bbb654a5db..6347b20971c 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4587,6 +4587,7 @@ CheckConstraintFetch(Relation relation)
}
check[found].ccvalid = conform->convalidated;
+ check[found].ccenforced = conform->conenforced;
check[found].ccnoinherit = conform->connoinherit;
check[found].ccname = MemoryContextStrdup(CacheMemoryContext,
NameStr(conform->conname));
diff --git a/src/include/access/tupdesc.h b/src/include/access/tupdesc.h
index 8930a28d660..96bac018083 100644
--- a/src/include/access/tupdesc.h
+++ b/src/include/access/tupdesc.h
@@ -30,6 +30,7 @@ typedef struct ConstrCheck
char *ccname;
char *ccbin; /* nodeToString representation of expr */
bool ccvalid;
+ bool ccenforced;
bool ccnoinherit; /* this is a non-inheritable constraint */
} ConstrCheck;
diff --git a/src/include/catalog/heap.h b/src/include/catalog/heap.h
index d6a2c791290..5c6a86c0f58 100644
--- a/src/include/catalog/heap.h
+++ b/src/include/catalog/heap.h
@@ -40,6 +40,7 @@ typedef struct CookedConstraint
AttrNumber attnum; /* which attr (only for DEFAULT) */
Node *expr; /* transformed default or check expr */
bool skip_validation; /* skip validation? (only for CHECK) */
+ bool is_enforced; /* is enforced? (only for CHECK) */
bool is_local; /* constraint has local (non-inherited) def */
int16 inhcount; /* number of times constraint is inherited */
bool is_no_inherit; /* constraint has local def and cannot be
diff --git a/src/include/catalog/pg_constraint.h b/src/include/catalog/pg_constraint.h
index 35788315bc4..3772a6431d1 100644
--- a/src/include/catalog/pg_constraint.h
+++ b/src/include/catalog/pg_constraint.h
@@ -52,6 +52,7 @@ CATALOG(pg_constraint,2606,ConstraintRelationId)
bool condeferrable; /* deferrable constraint? */
bool condeferred; /* deferred by default? */
bool convalidated; /* constraint has been validated? */
+ bool conenforced; /* enforced constraint? */
/*
* conrelid and conkey are only meaningful if the constraint applies to a
@@ -223,6 +224,7 @@ extern Oid CreateConstraintEntry(const char *constraintName,
bool isDeferrable,
bool isDeferred,
bool isValidated,
+ bool isEnforced,
Oid parentConstrId,
Oid relId,
const int16 *constraintKey,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 0d96db56386..e029568b2e1 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2736,6 +2736,8 @@ typedef enum ConstrType /* types of constraints */
CONSTR_ATTR_NOT_DEFERRABLE,
CONSTR_ATTR_DEFERRED,
CONSTR_ATTR_IMMEDIATE,
+ CONSTR_ATTR_ENFORCED,
+ CONSTR_ATTR_NOT_ENFORCED,
} ConstrType;
/* Foreign key action codes */
@@ -2759,6 +2761,7 @@ typedef struct Constraint
bool initdeferred; /* INITIALLY DEFERRED? */
bool skip_validation; /* skip validation of existing rows? */
bool initially_valid; /* mark the new constraint as valid? */
+ bool is_enforced; /* enforced constraint? */
bool is_no_inherit; /* is constraint non-inheritable? */
Node *raw_expr; /* CHECK or DEFAULT expression, as
* untransformed parse tree */
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 899d64ad55f..f2bc378394e 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -153,6 +153,7 @@ PG_KEYWORD("enable", ENABLE_P, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("encoding", ENCODING, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("encrypted", ENCRYPTED, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("end", END_P, RESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("enforced", ENFORCED, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("enum", ENUM_P, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("error", ERROR_P, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("escape", ESCAPE, UNRESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/test/regress/expected/constraints.out b/src/test/regress/expected/constraints.out
index cf0b80d6169..7fc1ced7efc 100644
--- a/src/test/regress/expected/constraints.out
+++ b/src/test/regress/expected/constraints.out
@@ -86,6 +86,25 @@ SELECT * FROM CHECK_TBL;
6
(3 rows)
+CREATE TABLE NE_CHECK_TBL (x int,
+ CONSTRAINT CHECK_CON CHECK (x > 3) NOT ENFORCED);
+INSERT INTO NE_CHECK_TBL VALUES (5);
+INSERT INTO NE_CHECK_TBL VALUES (4);
+INSERT INTO NE_CHECK_TBL VALUES (3);
+INSERT INTO NE_CHECK_TBL VALUES (2);
+INSERT INTO NE_CHECK_TBL VALUES (6);
+INSERT INTO NE_CHECK_TBL VALUES (1);
+SELECT * FROM NE_CHECK_TBL;
+ x
+---
+ 5
+ 4
+ 3
+ 2
+ 6
+ 1
+(6 rows)
+
CREATE SEQUENCE CHECK_SEQ;
CREATE TABLE CHECK2_TBL (x int, y text, z int,
CONSTRAINT SEQUENCE_CON
@@ -119,7 +138,8 @@ CREATE TABLE INSERT_TBL (x INT DEFAULT nextval('insert_seq'),
y TEXT DEFAULT '-NULL-',
z INT DEFAULT -1 * currval('insert_seq'),
CONSTRAINT INSERT_TBL_CON CHECK (x >= 3 AND y <> 'check failed' AND x < 8),
- CHECK (x + z = 0));
+ CHECK (x + z = 0) ENFORCED, /* no change it is a default */
+ CONSTRAINT NE_INSERT_TBL_CON CHECK (x + z = 1) NOT ENFORCED);
INSERT INTO INSERT_TBL(x,z) VALUES (2, -2);
ERROR: new row for relation "insert_tbl" violates check constraint "insert_tbl_con"
DETAIL: Failing row contains (2, -NULL-, -2).
diff --git a/src/test/regress/sql/constraints.sql b/src/test/regress/sql/constraints.sql
index e3e3bea7091..dac4f03bd71 100644
--- a/src/test/regress/sql/constraints.sql
+++ b/src/test/regress/sql/constraints.sql
@@ -66,6 +66,18 @@ INSERT INTO CHECK_TBL VALUES (1);
SELECT * FROM CHECK_TBL;
+CREATE TABLE NE_CHECK_TBL (x int,
+ CONSTRAINT CHECK_CON CHECK (x > 3) NOT ENFORCED);
+
+INSERT INTO NE_CHECK_TBL VALUES (5);
+INSERT INTO NE_CHECK_TBL VALUES (4);
+INSERT INTO NE_CHECK_TBL VALUES (3);
+INSERT INTO NE_CHECK_TBL VALUES (2);
+INSERT INTO NE_CHECK_TBL VALUES (6);
+INSERT INTO NE_CHECK_TBL VALUES (1);
+
+SELECT * FROM NE_CHECK_TBL;
+
CREATE SEQUENCE CHECK_SEQ;
CREATE TABLE CHECK2_TBL (x int, y text, z int,
@@ -91,7 +103,8 @@ CREATE TABLE INSERT_TBL (x INT DEFAULT nextval('insert_seq'),
y TEXT DEFAULT '-NULL-',
z INT DEFAULT -1 * currval('insert_seq'),
CONSTRAINT INSERT_TBL_CON CHECK (x >= 3 AND y <> 'check failed' AND x < 8),
- CHECK (x + z = 0));
+ CHECK (x + z = 0) ENFORCED, /* no change it is a default */
+ CONSTRAINT NE_INSERT_TBL_CON CHECK (x + z = 1) NOT ENFORCED);
INSERT INTO INSERT_TBL(x,z) VALUES (2, -2);
--
2.43.5
v2-0002-refactor-split-ATExecAlterConstrRecurse.patchapplication/octet-stream; name=v2-0002-refactor-split-ATExecAlterConstrRecurse.patchDownload
From 4f724e9df3024905d9b3e489f072ea70f9e4310c Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 23 Sep 2024 12:31:07 +0530
Subject: [PATCH v2 2/5] refactor: split ATExecAlterConstrRecurse()
When changing a NOT ENFORCED foreign key constraint to ENFORCED, we
need to create the necessary triggers for enforcing the constraint. We
can set the deferrability at the time of creation and skip the code
used for other constraint alteration operations.
Additionally, move the code that iterates over each child constraint
of the constraint being altered into a separate function.
---
src/backend/commands/tablecmds.c | 194 +++++++++++++++++++------------
1 file changed, 118 insertions(+), 76 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 96b77cb3484..3be8580cd8c 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -393,6 +393,12 @@ static ObjectAddress ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd,
static bool ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
Relation rel, HeapTuple contuple, List **otherrelids,
LOCKMODE lockmode);
+static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
+ bool deferrable, bool initdeferred,
+ List **otherrelids);
+static void ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
+ Relation rel, HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode);
static ObjectAddress ATExecValidateConstraint(List **wqueue,
Relation rel, char *constrName,
bool recurse, bool recursing, LOCKMODE lockmode);
@@ -11676,9 +11682,6 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
{
HeapTuple copyTuple;
Form_pg_constraint copy_con;
- HeapTuple tgtuple;
- ScanKeyData tgkey;
- SysScanDesc tgscan;
copyTuple = heap_copytuple(contuple);
copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
@@ -11699,53 +11702,8 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
* Now we need to update the multiple entries in pg_trigger that
* implement the constraint.
*/
- ScanKeyInit(&tgkey,
- Anum_pg_trigger_tgconstraint,
- BTEqualStrategyNumber, F_OIDEQ,
- ObjectIdGetDatum(conoid));
- tgscan = systable_beginscan(tgrel, TriggerConstraintIndexId, true,
- NULL, 1, &tgkey);
- while (HeapTupleIsValid(tgtuple = systable_getnext(tgscan)))
- {
- Form_pg_trigger tgform = (Form_pg_trigger) GETSTRUCT(tgtuple);
- Form_pg_trigger copy_tg;
- HeapTuple tgCopyTuple;
-
- /*
- * Remember OIDs of other relation(s) involved in FK constraint.
- * (Note: it's likely that we could skip forcing a relcache inval
- * for other rels that don't have a trigger whose properties
- * change, but let's be conservative.)
- */
- if (tgform->tgrelid != RelationGetRelid(rel))
- *otherrelids = list_append_unique_oid(*otherrelids,
- tgform->tgrelid);
-
- /*
- * Update deferrability of RI_FKey_noaction_del,
- * RI_FKey_noaction_upd, RI_FKey_check_ins and RI_FKey_check_upd
- * triggers, but not others; see createForeignKeyActionTriggers
- * and CreateFKCheckTrigger.
- */
- if (tgform->tgfoid != F_RI_FKEY_NOACTION_DEL &&
- tgform->tgfoid != F_RI_FKEY_NOACTION_UPD &&
- tgform->tgfoid != F_RI_FKEY_CHECK_INS &&
- tgform->tgfoid != F_RI_FKEY_CHECK_UPD)
- continue;
-
- tgCopyTuple = heap_copytuple(tgtuple);
- copy_tg = (Form_pg_trigger) GETSTRUCT(tgCopyTuple);
-
- copy_tg->tgdeferrable = cmdcon->deferrable;
- copy_tg->tginitdeferred = cmdcon->initdeferred;
- CatalogTupleUpdate(tgrel, &tgCopyTuple->t_self, tgCopyTuple);
-
- InvokeObjectPostAlterHook(TriggerRelationId, tgform->oid, 0);
-
- heap_freetuple(tgCopyTuple);
- }
-
- systable_endscan(tgscan);
+ AlterConstrTriggerDeferrability(conoid, tgrel, rel, cmdcon->deferrable,
+ cmdcon->initdeferred, otherrelids);
}
/*
@@ -11758,36 +11716,120 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
*/
if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)
- {
- ScanKeyData pkey;
- SysScanDesc pscan;
- HeapTuple childtup;
-
- ScanKeyInit(&pkey,
- Anum_pg_constraint_conparentid,
- BTEqualStrategyNumber, F_OIDEQ,
- ObjectIdGetDatum(conoid));
-
- pscan = systable_beginscan(conrel, ConstraintParentIndexId,
- true, NULL, 1, &pkey);
-
- while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
- {
- Form_pg_constraint childcon = (Form_pg_constraint) GETSTRUCT(childtup);
- Relation childrel;
-
- childrel = table_open(childcon->conrelid, lockmode);
- ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, childrel, childtup,
- otherrelids, lockmode);
- table_close(childrel, NoLock);
- }
-
- systable_endscan(pscan);
- }
+ ATExecAlterChildConstr(cmdcon, conrel, tgrel, rel, contuple,
+ otherrelids, lockmode);
return changed;
}
+/*
+ * A subroutine of ATExecAlterConstrRecurse that updated constraint trigger's
+ * deferrability.
+ *
+ * The arguments to this function have the same meaning as the arguments to
+ * ATExecAlterConstrRecurse.
+ */
+static void
+AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
+ bool deferrable, bool initdeferred,
+ List **otherrelids)
+{
+ HeapTuple tgtuple;
+ ScanKeyData tgkey;
+ SysScanDesc tgscan;
+
+ ScanKeyInit(&tgkey,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+ tgscan = systable_beginscan(tgrel, TriggerConstraintIndexId, true,
+ NULL, 1, &tgkey);
+ while (HeapTupleIsValid(tgtuple = systable_getnext(tgscan)))
+ {
+ Form_pg_trigger tgform = (Form_pg_trigger) GETSTRUCT(tgtuple);
+ Form_pg_trigger copy_tg;
+ HeapTuple tgCopyTuple;
+
+ /*
+ * Remember OIDs of other relation(s) involved in FK constraint.
+ * (Note: it's likely that we could skip forcing a relcache inval for
+ * other rels that don't have a trigger whose properties change, but
+ * let's be conservative.)
+ */
+ if (tgform->tgrelid != RelationGetRelid(rel))
+ *otherrelids = list_append_unique_oid(*otherrelids,
+ tgform->tgrelid);
+
+ /*
+ * Update enable status and deferrability of RI_FKey_noaction_del,
+ * RI_FKey_noaction_upd, RI_FKey_check_ins and RI_FKey_check_upd
+ * triggers, but not others; see createForeignKeyActionTriggers and
+ * CreateFKCheckTrigger.
+ */
+ if (tgform->tgfoid != F_RI_FKEY_NOACTION_DEL &&
+ tgform->tgfoid != F_RI_FKEY_NOACTION_UPD &&
+ tgform->tgfoid != F_RI_FKEY_CHECK_INS &&
+ tgform->tgfoid != F_RI_FKEY_CHECK_UPD)
+ continue;
+
+ tgCopyTuple = heap_copytuple(tgtuple);
+ copy_tg = (Form_pg_trigger) GETSTRUCT(tgCopyTuple);
+
+ copy_tg->tgdeferrable = deferrable;
+ copy_tg->tginitdeferred = initdeferred;
+ CatalogTupleUpdate(tgrel, &tgCopyTuple->t_self, tgCopyTuple);
+
+ InvokeObjectPostAlterHook(TriggerRelationId, tgform->oid, 0);
+
+ heap_freetuple(tgCopyTuple);
+ }
+
+ systable_endscan(tgscan);
+}
+
+/*
+ * Invokes ATExecAlterConstrRecurse for each constraint that is a child of the
+ * specified constraint.
+ *
+ * The arguments to this function have the same meaning as the arguments to
+ * ATExecAlterConstrRecurse.
+ */
+static void
+ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
+ Relation rel, HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode)
+{
+ Form_pg_constraint currcon;
+ Oid conoid;
+ ScanKeyData pkey;
+ SysScanDesc pscan;
+ HeapTuple childtup;
+
+ currcon = (Form_pg_constraint) GETSTRUCT(contuple);
+ conoid = currcon->oid;
+
+ ScanKeyInit(&pkey,
+ Anum_pg_constraint_conparentid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+
+ pscan = systable_beginscan(conrel, ConstraintParentIndexId,
+ true, NULL, 1, &pkey);
+
+ while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
+ {
+ Form_pg_constraint childcon = (Form_pg_constraint) GETSTRUCT(childtup);
+ Relation childrel;
+
+ childrel = table_open(childcon->conrelid, lockmode);
+ ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, childrel, childtup,
+ otherrelids, lockmode);
+ table_close(childrel, NoLock);
+ }
+
+ systable_endscan(pscan);
+}
+
/*
* ALTER TABLE VALIDATE CONSTRAINT
*
--
2.43.5
v2-0003-refactor-Change-ATExecAlterConstrRecurse-argument.patchapplication/octet-stream; name=v2-0003-refactor-Change-ATExecAlterConstrRecurse-argument.patchDownload
From 286854fa64f3297b65c6f6c5ecfab6c43af46e2e Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 23 Sep 2024 12:37:05 +0530
Subject: [PATCH v2 3/5] refactor: Change ATExecAlterConstrRecurse() argument.
Instead of passing the Relation of the referencing relation, we will
pass a relid. Opening the relation using the relid again should not
cause significant issues. However, this approach makes recursion more
efficient, especially in the later patch where we will be recreating
the referencing and referred triggers. In addition to that passing
relid of the referred relation too and Oids of triggers.
----
NOTE: This patch is not meant to be committed separately. It should
be squashed with the main patch that adds ENFORCED/NOT ENFORCED.
----
---
src/backend/commands/tablecmds.c | 66 ++++++++++++++++++++++----------
1 file changed, 45 insertions(+), 21 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 3be8580cd8c..f7b29522b71 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -391,14 +391,22 @@ static void AlterSeqNamespaces(Relation classRel, Relation rel,
static ObjectAddress ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd,
bool recurse, bool recursing, LOCKMODE lockmode);
static bool ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode);
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger);
static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
bool deferrable, bool initdeferred,
List **otherrelids);
static void ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode);
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger);
static ObjectAddress ATExecValidateConstraint(List **wqueue,
Relation rel, char *constrName,
bool recurse, bool recursing, LOCKMODE lockmode);
@@ -11622,8 +11630,10 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
currcon->condeferred != cmdcon->initdeferred ||
rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
- if (ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, rel, contuple,
- &otherrelids, lockmode))
+ if (ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, currcon->conrelid,
+ currcon->confrelid, contuple, &otherrelids,
+ lockmode, InvalidOid, InvalidOid,
+ InvalidOid, InvalidOid))
ObjectAddressSet(address, ConstraintRelationId, currcon->oid);
}
@@ -11656,13 +11666,18 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
*/
static bool
ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode)
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger)
{
Form_pg_constraint currcon;
Oid conoid;
Oid refrelid;
bool changed = false;
+ Relation rel;
/* since this function recurses, it could be driven to stack overflow */
check_stack_depth();
@@ -11671,6 +11686,8 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
conoid = currcon->oid;
refrelid = currcon->confrelid;
+ rel = table_open(currcon->conrelid, lockmode);
+
/*
* Update pg_constraint with the flags from cmdcon.
*
@@ -11716,8 +11733,14 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
*/
if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)
- ATExecAlterChildConstr(cmdcon, conrel, tgrel, rel, contuple,
- otherrelids, lockmode);
+ ATExecAlterChildConstr(cmdcon, conrel, tgrel, fkrelid, pkrelid,
+ contuple, otherrelids, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
+
+ table_close(rel, NoLock);
return changed;
}
@@ -11796,8 +11819,12 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
*/
static void
ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode)
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger)
{
Form_pg_constraint currcon;
Oid conoid;
@@ -11817,15 +11844,12 @@ ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
true, NULL, 1, &pkey);
while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
- {
- Form_pg_constraint childcon = (Form_pg_constraint) GETSTRUCT(childtup);
- Relation childrel;
-
- childrel = table_open(childcon->conrelid, lockmode);
- ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, childrel, childtup,
- otherrelids, lockmode);
- table_close(childrel, NoLock);
- }
+ ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, fkrelid, pkrelid,
+ childtup, otherrelids, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
systable_endscan(pscan);
}
--
2.43.5
v2-0004-Remove-hastriggers-flag-check-before-fetching-FK-.patchapplication/octet-stream; name=v2-0004-Remove-hastriggers-flag-check-before-fetching-FK-.patchDownload
From a851f0664fc1ccc0ca1f859b4d66255313f45bc5 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 7 Oct 2024 12:40:07 +0530
Subject: [PATCH v2 4/5] Remove hastriggers flag check before fetching FK
constraints.
With NOT ENFORCED, foreign key constraints will be created without
triggers. Therefore, the criteria for fetching foreign keys based on
the presence of triggers no longer apply.
----
NOTE: This patch is intended to reduce the diff noise from the main
patch and is not meant to be committed separately. It should be
squashed with the main patch that adds ENFORCED/NOT ENFORCED.
----
---
src/backend/utils/cache/relcache.c | 5 -
src/bin/pg_dump/pg_dump.c | 8 +-
src/bin/psql/describe.c | 223 ++++++++++++++---------------
3 files changed, 107 insertions(+), 129 deletions(-)
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 6347b20971c..01f734fa59d 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4670,11 +4670,6 @@ RelationGetFKeyList(Relation relation)
if (relation->rd_fkeyvalid)
return relation->rd_fkeylist;
- /* Fast path: non-partitioned tables without triggers can't have FKs */
- if (!relation->rd_rel->relhastriggers &&
- relation->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
- return NIL;
-
/*
* We build the list we intend to return (in the caller's context) while
* doing the scan. After successfully completing the scan, we copy that
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index d8c6330732e..f5b7ece0d15 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -7825,13 +7825,7 @@ getConstraints(Archive *fout, TableInfo tblinfo[], int numTables)
{
TableInfo *tinfo = &tblinfo[i];
- /*
- * For partitioned tables, foreign keys have no triggers so they must
- * be included anyway in case some foreign keys are defined.
- */
- if ((!tinfo->hastriggers &&
- tinfo->relkind != RELKIND_PARTITIONED_TABLE) ||
- !(tinfo->dobj.dump & DUMP_COMPONENT_DEFINITION))
+ if (!(tinfo->dobj.dump & DUMP_COMPONENT_DEFINITION))
continue;
/* OK, we need info for this table */
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 363a66e7185..16d18fcef34 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2499,135 +2499,124 @@ describeOneTableDetails(const char *schemaname,
}
/*
- * Print foreign-key constraints (there are none if no triggers,
- * except if the table is partitioned, in which case the triggers
- * appear in the partitions)
+ * Print foreign-key constraints
*/
- if (tableinfo.hastriggers ||
- tableinfo.relkind == RELKIND_PARTITIONED_TABLE)
+ if (pset.sversion >= 120000 &&
+ (tableinfo.ispartition || tableinfo.relkind == RELKIND_PARTITIONED_TABLE))
{
- if (pset.sversion >= 120000 &&
- (tableinfo.ispartition || tableinfo.relkind == RELKIND_PARTITIONED_TABLE))
+ /*
+ * Put the constraints defined in this table first, followed
+ * by the constraints defined in ancestor partitioned tables.
+ */
+ printfPQExpBuffer(&buf,
+ "SELECT conrelid = '%s'::pg_catalog.regclass AS sametable,\n"
+ " conname,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef,\n"
+ " conrelid::pg_catalog.regclass AS ontable\n"
+ " FROM pg_catalog.pg_constraint,\n"
+ " pg_catalog.pg_partition_ancestors('%s')\n"
+ " WHERE conrelid = relid AND contype = 'f' AND conparentid = 0\n"
+ "ORDER BY sametable DESC, conname;",
+ oid, oid);
+ }
+ else
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT true as sametable, conname,\n"
+ " pg_catalog.pg_get_constraintdef(r.oid, true) as condef,\n"
+ " conrelid::pg_catalog.regclass AS ontable\n"
+ "FROM pg_catalog.pg_constraint r\n"
+ "WHERE r.conrelid = '%s' AND r.contype = 'f'\n",
+ oid);
+
+ if (pset.sversion >= 120000)
+ appendPQExpBufferStr(&buf, " AND conparentid = 0\n");
+ appendPQExpBufferStr(&buf, "ORDER BY conname");
+ }
+
+ result = PSQLexec(buf.data);
+ if (!result)
+ goto error_return;
+ else
+ tuples = PQntuples(result);
+
+ if (tuples > 0)
+ {
+ int i_sametable = PQfnumber(result, "sametable"),
+ i_conname = PQfnumber(result, "conname"),
+ i_condef = PQfnumber(result, "condef"),
+ i_ontable = PQfnumber(result, "ontable");
+
+ printTableAddFooter(&cont, _("Foreign-key constraints:"));
+ for (i = 0; i < tuples; i++)
{
/*
- * Put the constraints defined in this table first, followed
- * by the constraints defined in ancestor partitioned tables.
+ * Print untranslated constraint name and definition. Use
+ * a "TABLE tab" prefix when the constraint is defined in
+ * a parent partitioned table.
*/
- printfPQExpBuffer(&buf,
- "SELECT conrelid = '%s'::pg_catalog.regclass AS sametable,\n"
- " conname,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef,\n"
- " conrelid::pg_catalog.regclass AS ontable\n"
- " FROM pg_catalog.pg_constraint,\n"
- " pg_catalog.pg_partition_ancestors('%s')\n"
- " WHERE conrelid = relid AND contype = 'f' AND conparentid = 0\n"
- "ORDER BY sametable DESC, conname;",
- oid, oid);
- }
- else
- {
- printfPQExpBuffer(&buf,
- "SELECT true as sametable, conname,\n"
- " pg_catalog.pg_get_constraintdef(r.oid, true) as condef,\n"
- " conrelid::pg_catalog.regclass AS ontable\n"
- "FROM pg_catalog.pg_constraint r\n"
- "WHERE r.conrelid = '%s' AND r.contype = 'f'\n",
- oid);
-
- if (pset.sversion >= 120000)
- appendPQExpBufferStr(&buf, " AND conparentid = 0\n");
- appendPQExpBufferStr(&buf, "ORDER BY conname");
- }
-
- result = PSQLexec(buf.data);
- if (!result)
- goto error_return;
- else
- tuples = PQntuples(result);
-
- if (tuples > 0)
- {
- int i_sametable = PQfnumber(result, "sametable"),
- i_conname = PQfnumber(result, "conname"),
- i_condef = PQfnumber(result, "condef"),
- i_ontable = PQfnumber(result, "ontable");
-
- printTableAddFooter(&cont, _("Foreign-key constraints:"));
- for (i = 0; i < tuples; i++)
- {
- /*
- * Print untranslated constraint name and definition. Use
- * a "TABLE tab" prefix when the constraint is defined in
- * a parent partitioned table.
- */
- if (strcmp(PQgetvalue(result, i, i_sametable), "f") == 0)
- printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
- PQgetvalue(result, i, i_ontable),
- PQgetvalue(result, i, i_conname),
- PQgetvalue(result, i, i_condef));
- else
- printfPQExpBuffer(&buf, " \"%s\" %s",
- PQgetvalue(result, i, i_conname),
- PQgetvalue(result, i, i_condef));
-
- printTableAddFooter(&cont, buf.data);
- }
- }
- PQclear(result);
- }
-
- /* print incoming foreign-key references */
- if (tableinfo.hastriggers ||
- tableinfo.relkind == RELKIND_PARTITIONED_TABLE)
- {
- if (pset.sversion >= 120000)
- {
- printfPQExpBuffer(&buf,
- "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
- " FROM pg_catalog.pg_constraint c\n"
- " WHERE confrelid IN (SELECT pg_catalog.pg_partition_ancestors('%s')\n"
- " UNION ALL VALUES ('%s'::pg_catalog.regclass))\n"
- " AND contype = 'f' AND conparentid = 0\n"
- "ORDER BY conname;",
- oid, oid);
- }
- else
- {
- printfPQExpBuffer(&buf,
- "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
- " FROM pg_catalog.pg_constraint\n"
- " WHERE confrelid = %s AND contype = 'f'\n"
- "ORDER BY conname;",
- oid);
- }
-
- result = PSQLexec(buf.data);
- if (!result)
- goto error_return;
- else
- tuples = PQntuples(result);
-
- if (tuples > 0)
- {
- int i_conname = PQfnumber(result, "conname"),
- i_ontable = PQfnumber(result, "ontable"),
- i_condef = PQfnumber(result, "condef");
-
- printTableAddFooter(&cont, _("Referenced by:"));
- for (i = 0; i < tuples; i++)
- {
+ if (strcmp(PQgetvalue(result, i, i_sametable), "f") == 0)
printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
PQgetvalue(result, i, i_ontable),
PQgetvalue(result, i, i_conname),
PQgetvalue(result, i, i_condef));
+ else
+ printfPQExpBuffer(&buf, " \"%s\" %s",
+ PQgetvalue(result, i, i_conname),
+ PQgetvalue(result, i, i_condef));
- printTableAddFooter(&cont, buf.data);
- }
+ printTableAddFooter(&cont, buf.data);
}
- PQclear(result);
}
+ PQclear(result);
+
+ if (pset.sversion >= 120000)
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
+ " FROM pg_catalog.pg_constraint c\n"
+ " WHERE confrelid IN (SELECT pg_catalog.pg_partition_ancestors('%s')\n"
+ " UNION ALL VALUES ('%s'::pg_catalog.regclass))\n"
+ " AND contype = 'f' AND conparentid = 0\n"
+ "ORDER BY conname;",
+ oid, oid);
+ }
+ else
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
+ " FROM pg_catalog.pg_constraint\n"
+ " WHERE confrelid = %s AND contype = 'f'\n"
+ "ORDER BY conname;",
+ oid);
+ }
+
+ result = PSQLexec(buf.data);
+ if (!result)
+ goto error_return;
+ else
+ tuples = PQntuples(result);
+
+ if (tuples > 0)
+ {
+ int i_conname = PQfnumber(result, "conname"),
+ i_ontable = PQfnumber(result, "ontable"),
+ i_condef = PQfnumber(result, "condef");
+
+ printTableAddFooter(&cont, _("Referenced by:"));
+ for (i = 0; i < tuples; i++)
+ {
+ printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
+ PQgetvalue(result, i, i_ontable),
+ PQgetvalue(result, i, i_conname),
+ PQgetvalue(result, i, i_condef));
+
+ printTableAddFooter(&cont, buf.data);
+ }
+ }
+ PQclear(result);
/* print any row-level policies */
if (pset.sversion >= 90500)
--
2.43.5
v2-0005-Add-support-for-NOT-ENFORCED-in-foreign-key-const.patchapplication/octet-stream; name=v2-0005-Add-support-for-NOT-ENFORCED-in-foreign-key-const.patchDownload
From 2b15a9f6efcdf679108a57f6c9d3d8493eda2253 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Tue, 8 Oct 2024 11:35:17 +0530
Subject: [PATCH v2 5/5] Add support for NOT ENFORCED in foreign key
constraints.
Normally, when a foreign key (FK) constraint is created on a table,
action/check triggers are associated with the constraint to ensure data
integrity. With this patch, when constraints are marked as NOT
ENFORCED, integrity checks are not required, and thus, these triggers
are unnecessary. As a result, when creating a NOT ENFORCED FK
constraint or changing an existing FK constraint to NOT ENFORCED, the
creation of triggers will be skipped, or existing triggers will be
dropped, respectively. And when changing an existing NOT ENFORCED FK
constraint to ENFORCED, those triggers will be created.
---
doc/src/sgml/advanced.sgml | 13 +
doc/src/sgml/ddl.sgml | 12 +-
doc/src/sgml/ref/alter_table.sgml | 5 +
doc/src/sgml/ref/create_table.sgml | 10 +-
src/backend/catalog/pg_constraint.c | 5 +-
src/backend/commands/tablecmds.c | 485 ++++++++++++++++------
src/backend/parser/gram.y | 3 +-
src/backend/parser/parse_utilcmd.c | 6 +-
src/backend/utils/cache/relcache.c | 1 +
src/include/utils/rel.h | 3 +
src/test/regress/expected/foreign_key.out | 118 +++++-
src/test/regress/sql/foreign_key.sql | 87 +++-
12 files changed, 594 insertions(+), 154 deletions(-)
diff --git a/doc/src/sgml/advanced.sgml b/doc/src/sgml/advanced.sgml
index 755c9f14850..b7d90de69d7 100644
--- a/doc/src/sgml/advanced.sgml
+++ b/doc/src/sgml/advanced.sgml
@@ -125,6 +125,19 @@ ERROR: insert or update on table "weather" violates foreign key constraint "wea
DETAIL: Key (city)=(Berkeley) is not present in table "cities".
</screen>
</para>
+ <para>
+ If, for some reason, you do not want to enforce this constraint and wish to
+ avoid the error, you can modify the foreign key constraint to <literal>NOT ENFORCED</literal>,
+ or specify it in the <link linkend="sql-createtable"><command>CREATE TABLE</command></link>.
+ </para>
+ <para>
+ Let's change the constraint to <literal>NOT ENFORCED</literal>, and when you
+ attempt to reinsert the same record, it will not result in any error.
+<programlisting>
+ALTER TABLE weather ALTER CONSTRAINT weather_city_fkey NOT ENFORCED;
+INSERT INTO weather VALUES ('Berkeley', 45, 53, 0.0, '1994-11-28');
+</programlisting>
+ </para>
<para>
The behavior of foreign keys can be finely tuned to your
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index 5f801903ddd..49734cfd785 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -1031,11 +1031,13 @@ CREATE TABLE example (
</indexterm>
<para>
- A foreign key constraint specifies that the values in a column (or
- a group of columns) must match the values appearing in some row
- of another table.
- We say this maintains the <firstterm>referential
- integrity</firstterm> between two related tables.
+ A foreign key constraint with <literal>ENFORCED</literal>, the default
+ setting, specifies that the values in a column (or a group of columns)
+ must match the values appearing in some row of another table. We say this
+ maintains the <firstterm>referential integrity</firstterm> between two
+ related tables. If a foreign key constraint is created with <literal>NOT ENFORCED</literal>,
+ the check for aforementioned <firstterm>referential integrity</firstterm>
+ will not be performed.
</para>
<para>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 88d9169d7a6..ccd9007d347 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -554,6 +554,11 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
This form alters the attributes of a constraint that was previously
created. Currently only foreign key constraints may be altered.
</para>
+ <para>
+ Note that changing constraints to <literal>ENFORCED</literal>, which were
+ previously marked as valid and <literal>NOT ENFORCED</literal>, will
+ trigger validation, similar to <literal>VALIDATE CONSTRAINT</literal>.
+ </para>
</listitem>
</varlistentry>
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index 55b51602136..77452efe740 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -1367,11 +1367,11 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
<term><literal>NOT ENFORCED</literal></term>
<listitem>
<para>
- This is currently only allowed for <literal>CHECK</literal> constraints.
- If the constraint is <literal>NOT ENFORCED</literal>, this clause
- specifies that the constraint check will be skipped. When the constraint
- is <literal>ENFORCED</literal>, check is performed after each statement.
- This is the default.
+ This is currently only allowed for foreign key and <literal>CHECK</literal>
+ constraints. If the constraint is <literal>NOT ENFORCED</literal>, this
+ clause specifies that the constraint check will be skipped. When the
+ constraint is <literal>ENFORCED</literal>, check is performed after each
+ statement. This is the default.
</para>
</listitem>
</varlistentry>
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index 2b461abf47e..c0ff6a8450e 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -99,8 +99,9 @@ CreateConstraintEntry(const char *constraintName,
ObjectAddresses *addrs_auto;
ObjectAddresses *addrs_normal;
- /* Only CHECK constraint can be not enforced */
- Assert(isEnforced || constraintType == CONSTRAINT_CHECK);
+ /* Only CHECK or FOREIGN KEY constraint can be not enforced */
+ Assert(isEnforced || constraintType == CONSTRAINT_CHECK ||
+ constraintType == CONSTRAINT_FOREIGN);
conDesc = table_open(ConstraintRelationId, RowExclusiveLock);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index f7b29522b71..71fd1ccd45a 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -388,19 +388,29 @@ static void AlterIndexNamespaces(Relation classRel, Relation rel,
static void AlterSeqNamespaces(Relation classRel, Relation rel,
Oid oldNspOid, Oid newNspOid, ObjectAddresses *objsMoved,
LOCKMODE lockmode);
-static ObjectAddress ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd,
+static ObjectAddress ATExecAlterConstraint(List **wqueue, Relation rel, AlterTableCmd *cmd,
bool recurse, bool recursing, LOCKMODE lockmode);
-static bool ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
+static bool ATExecAlterConstrRecurse(List **wqueue, Constraint *cmdcon,
+ Relation conrel, Relation tgrel,
const Oid fkrelid, const Oid pkrelid,
HeapTuple contuple, List **otherrelids,
LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
Oid ReferencedParentUpdTrigger,
Oid ReferencingParentInsTrigger,
Oid ReferencingParentUpdTrigger);
-static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
+static void ATExecAlterConstrEnforceability(List **wqueue, Constraint *cmdcon,
+ Relation conrel, Relation tgrel,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger);
+static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Oid fkrel,
bool deferrable, bool initdeferred,
List **otherrelids);
-static void ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
+static void ATExecAlterChildConstr(List **wqueue, Constraint *cmdcon,
+ Relation conrel, Relation tgrel,
const Oid fkrelid, const Oid pkrelid,
HeapTuple contuple, List **otherrelids,
LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
@@ -5377,7 +5387,7 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
lockmode);
break;
case AT_AlterConstraint: /* ALTER CONSTRAINT */
- address = ATExecAlterConstraint(rel, cmd, false, false, lockmode);
+ address = ATExecAlterConstraint(wqueue, rel, cmd, false, false, lockmode);
break;
case AT_ValidateConstraint: /* VALIDATE CONSTRAINT */
address = ATExecValidateConstraint(wqueue, rel, cmd->name, cmd->recurse,
@@ -9498,8 +9508,9 @@ ATAddCheckConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
{
CookedConstraint *ccon = (CookedConstraint *) lfirst(lcon);
- /* Only CHECK constraint can be not enforced */
- Assert(ccon->is_enforced || ccon->contype == CONSTRAINT_CHECK);
+ /* Only CHECK or FOREIGN KEY constraint can be not enforced */
+ Assert(ccon->is_enforced || ccon->contype == CONSTRAINT_CHECK ||
+ ccon->contype == CONSTRAINT_FOREIGN);
if (!ccon->skip_validation)
{
@@ -10273,7 +10284,7 @@ addFkConstraint(addFkConstraintSides fkside,
fkconstraint->deferrable,
fkconstraint->initdeferred,
fkconstraint->initially_valid,
- true, /* Is enforced */
+ fkconstraint->is_enforced,
parentConstr,
RelationGetRelid(rel),
fkattnum,
@@ -10390,20 +10401,22 @@ addFkRecurseReferenced(Constraint *fkconstraint, Relation rel,
Oid parentDelTrigger, Oid parentUpdTrigger,
bool with_period)
{
- Oid deleteTriggerOid,
- updateTriggerOid;
+ Oid deleteTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
Assert(CheckRelationLockedByMe(pkrel, ShareRowExclusiveLock, true));
Assert(CheckRelationLockedByMe(rel, ShareRowExclusiveLock, true));
/*
- * Create the action triggers that enforce the constraint.
+ * Create action triggers to enforce the constraint, or skip them if the
+ * constraint is not enforced.
*/
- createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
- fkconstraint,
- parentConstr, indexOid,
- parentDelTrigger, parentUpdTrigger,
- &deleteTriggerOid, &updateTriggerOid);
+ if (fkconstraint->is_enforced)
+ createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
+ fkconstraint,
+ parentConstr, indexOid,
+ parentDelTrigger, parentUpdTrigger,
+ &deleteTriggerOid, &updateTriggerOid);
/*
* If the referenced table is partitioned, recurse on ourselves to handle
@@ -10524,8 +10537,8 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
Oid parentInsTrigger, Oid parentUpdTrigger,
bool with_period)
{
- Oid insertTriggerOid,
- updateTriggerOid;
+ Oid insertTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
Assert(OidIsValid(parentConstr));
Assert(CheckRelationLockedByMe(rel, ShareRowExclusiveLock, true));
@@ -10537,29 +10550,32 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
errmsg("foreign key constraints are not supported on foreign tables")));
/*
- * Add the check triggers to it and, if necessary, schedule it to be
- * checked in Phase 3.
+ * Add check triggers if the constraint is enforced, and if needed,
+ * schedule them to be checked in Phase 3.
*
* If the relation is partitioned, drill down to do it to its partitions.
*/
- createForeignKeyCheckTriggers(RelationGetRelid(rel),
- RelationGetRelid(pkrel),
- fkconstraint,
- parentConstr,
- indexOid,
- parentInsTrigger, parentUpdTrigger,
- &insertTriggerOid, &updateTriggerOid);
+ if (fkconstraint->is_enforced)
+ createForeignKeyCheckTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
+ fkconstraint,
+ parentConstr,
+ indexOid,
+ parentInsTrigger, parentUpdTrigger,
+ &insertTriggerOid, &updateTriggerOid);
if (rel->rd_rel->relkind == RELKIND_RELATION)
{
/*
* Tell Phase 3 to check that the constraint is satisfied by existing
- * rows. We can skip this during table creation, when requested
- * explicitly by specifying NOT VALID in an ADD FOREIGN KEY command,
- * and when we're recreating a constraint following a SET DATA TYPE
- * operation that did not impugn its validity.
+ * rows. We can skip this during table creation, when constraint is
+ * specified as NOT ENFORCED, when requested explicitly by specifying
+ * NOT VALID in an ADD FOREIGN KEY command, and when we're recreating
+ * a constraint following a SET DATA TYPE operation that did not
+ * impugn its validity.
*/
- if (wqueue && !old_check_ok && !fkconstraint->skip_validation)
+ if (wqueue && !old_check_ok && !fkconstraint->skip_validation &&
+ fkconstraint->is_enforced)
{
NewConstraint *newcon;
AlteredTableInfo *tab;
@@ -10845,6 +10861,7 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
fkconstraint->conname = NameStr(constrForm->conname);
fkconstraint->deferrable = constrForm->condeferrable;
fkconstraint->initdeferred = constrForm->condeferred;
+ fkconstraint->is_enforced = constrForm->conenforced;
fkconstraint->location = -1;
fkconstraint->pktable = NULL;
/* ->fk_attrs determined below */
@@ -10884,9 +10901,10 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
* parent OIDs for similar triggers that will be created on the
* partition in addFkRecurseReferenced().
*/
- GetForeignKeyActionTriggers(trigrel, constrOid,
- constrForm->confrelid, constrForm->conrelid,
- &deleteTriggerOid, &updateTriggerOid);
+ if (constrForm->conenforced)
+ GetForeignKeyActionTriggers(trigrel, constrOid,
+ constrForm->confrelid, constrForm->conrelid,
+ &deleteTriggerOid, &updateTriggerOid);
/* Add this constraint ... */
address = addFkConstraint(addFkReferencedSide,
@@ -11052,17 +11070,18 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
mapped_conkey[i] = attmap->attnums[conkey[i] - 1];
/*
- * Get the "check" triggers belonging to the constraint to pass as
- * parent OIDs for similar triggers that will be created on the
- * partition in addFkRecurseReferencing(). They are also passed to
- * tryAttachPartitionForeignKey() below to simply assign as parents to
- * the partition's existing "check" triggers, that is, if the
- * corresponding constraints is deemed attachable to the parent
- * constraint.
+ * Get the "check" triggers belonging to the constraint, if it is
+ * enforced, to pass as parent OIDs for similar triggers that will be
+ * created on the partition in addFkRecurseReferencing(). They are
+ * also passed to tryAttachPartitionForeignKey() below to simply
+ * assign as parents to the partition's existing "check" triggers,
+ * that is, if the corresponding constraints is deemed attachable to
+ * the parent constraint.
*/
- GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
- constrForm->confrelid, constrForm->conrelid,
- &insertTriggerOid, &updateTriggerOid);
+ if (constrForm->conenforced)
+ GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
+ constrForm->confrelid, constrForm->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
/*
* Before creating a new constraint, see whether any existing FKs are
@@ -11104,6 +11123,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
/* ->conname determined below */
fkconstraint->deferrable = constrForm->condeferrable;
fkconstraint->initdeferred = constrForm->condeferred;
+ fkconstraint->is_enforced = constrForm->conenforced;
fkconstraint->location = -1;
fkconstraint->pktable = NULL;
/* ->fk_attrs determined below */
@@ -11198,8 +11218,6 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
ScanKeyData key;
SysScanDesc scan;
HeapTuple trigtup;
- Oid insertTriggerOid,
- updateTriggerOid;
parentConstrTup = SearchSysCache1(CONSTROID,
ObjectIdGetDatum(parentConstrOid));
@@ -11241,6 +11259,7 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
!partConstr->convalidated ||
partConstr->condeferrable != parentConstr->condeferrable ||
partConstr->condeferred != parentConstr->condeferred ||
+ partConstr->conenforced != parentConstr->conenforced ||
partConstr->confupdtype != parentConstr->confupdtype ||
partConstr->confdeltype != parentConstr->confdeltype ||
partConstr->confmatchtype != parentConstr->confmatchtype)
@@ -11300,18 +11319,25 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
ConstraintSetParentConstraint(fk->conoid, parentConstrOid, partRelid);
/*
- * Like the constraint, attach partition's "check" triggers to the
- * corresponding parent triggers.
+ * Similar to the constraint, attach the partition's "check" triggers to
+ * the corresponding parent triggers if the constraint is enforced.
+ * Non-enforced constraints do not have these triggers.
*/
- GetForeignKeyCheckTriggers(trigrel,
- fk->conoid, fk->confrelid, fk->conrelid,
- &insertTriggerOid, &updateTriggerOid);
- Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
- TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
- partRelid);
- Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
- TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
- partRelid);
+ if (fk->conenforced)
+ {
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
+ partRelid);
+ Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
+ partRelid);
+ }
/*
* If the referenced table is partitioned, then the partition we're
@@ -11525,8 +11551,8 @@ GetForeignKeyCheckTriggers(Relation trigrel,
* InvalidObjectAddress.
*/
static ObjectAddress
-ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
- bool recursing, LOCKMODE lockmode)
+ATExecAlterConstraint(List **wqueue, Relation rel, AlterTableCmd *cmd,
+ bool recurse, bool recursing, LOCKMODE lockmode)
{
Constraint *cmdcon;
Relation conrel;
@@ -11628,12 +11654,14 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
address = InvalidObjectAddress;
if (currcon->condeferrable != cmdcon->deferrable ||
currcon->condeferred != cmdcon->initdeferred ||
+ currcon->conenforced != cmdcon->is_enforced ||
rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
- if (ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, currcon->conrelid,
- currcon->confrelid, contuple, &otherrelids,
- lockmode, InvalidOid, InvalidOid,
- InvalidOid, InvalidOid))
+ if (ATExecAlterConstrRecurse(wqueue, cmdcon, conrel, tgrel,
+ currcon->conrelid, currcon->confrelid,
+ contuple, &otherrelids, lockmode,
+ InvalidOid, InvalidOid, InvalidOid,
+ InvalidOid))
ObjectAddressSet(address, ConstraintRelationId, currcon->oid);
}
@@ -11665,8 +11693,8 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
* but existing releases don't do that.)
*/
static bool
-ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
- const Oid fkrelid, const Oid pkrelid,
+ATExecAlterConstrRecurse(List **wqueue, Constraint *cmdcon, Relation conrel,
+ Relation tgrel, const Oid fkrelid, const Oid pkrelid,
HeapTuple contuple, List **otherrelids,
LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
Oid ReferencedParentUpdTrigger,
@@ -11675,19 +11703,18 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
{
Form_pg_constraint currcon;
Oid conoid;
+ Oid conrelid;
Oid refrelid;
bool changed = false;
- Relation rel;
/* since this function recurses, it could be driven to stack overflow */
check_stack_depth();
currcon = (Form_pg_constraint) GETSTRUCT(contuple);
conoid = currcon->oid;
+ conrelid = currcon->conrelid;
refrelid = currcon->confrelid;
- rel = table_open(currcon->conrelid, lockmode);
-
/*
* Update pg_constraint with the flags from cmdcon.
*
@@ -11695,7 +11722,8 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
* silently do nothing.
*/
if (currcon->condeferrable != cmdcon->deferrable ||
- currcon->condeferred != cmdcon->initdeferred)
+ currcon->condeferred != cmdcon->initdeferred ||
+ currcon->conenforced != cmdcon->is_enforced)
{
HeapTuple copyTuple;
Form_pg_constraint copy_con;
@@ -11704,6 +11732,7 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
copy_con->condeferrable = cmdcon->deferrable;
copy_con->condeferred = cmdcon->initdeferred;
+ copy_con->conenforced = cmdcon->is_enforced;
CatalogTupleUpdate(conrel, ©Tuple->t_self, copyTuple);
InvokeObjectPostAlterHook(ConstraintRelationId,
@@ -11713,38 +11742,214 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
changed = true;
/* Make new constraint flags visible to others */
- CacheInvalidateRelcache(rel);
+ CacheInvalidateRelcacheByRelid(conrelid);
+ }
+ if (currcon->conenforced != cmdcon->is_enforced)
+ {
+ ATExecAlterConstrEnforceability(wqueue, cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, contuple, otherrelids, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
+ }
+ else
+ {
/*
* Now we need to update the multiple entries in pg_trigger that
* implement the constraint.
*/
- AlterConstrTriggerDeferrability(conoid, tgrel, rel, cmdcon->deferrable,
- cmdcon->initdeferred, otherrelids);
+ AlterConstrTriggerDeferrability(conoid, tgrel, conrelid,
+ cmdcon->deferrable,
+ cmdcon->initdeferred,
+ otherrelids);
+
+ /*
+ * If the table at either end of the constraint is partitioned, we
+ * need to recurse and handle every constraint that is a child of this
+ * one.
+ *
+ * (This assumes that the recurse flag is forcibly set for partitioned
+ * tables, and not set for legacy inheritance, though we don't check
+ * for that here.)
+ */
+ if (get_rel_relkind(conrelid) == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)
+ ATExecAlterChildConstr(wqueue, cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, contuple, otherrelids, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
}
- /*
- * If the table at either end of the constraint is partitioned, we need to
- * recurse and handle every constraint that is a child of this one.
- *
- * (This assumes that the recurse flag is forcibly set for partitioned
- * tables, and not set for legacy inheritance, though we don't check for
- * that here.)
- */
- if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
- get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)
- ATExecAlterChildConstr(cmdcon, conrel, tgrel, fkrelid, pkrelid,
- contuple, otherrelids, lockmode,
- ReferencedParentDelTrigger,
- ReferencedParentUpdTrigger,
- ReferencingParentInsTrigger,
- ReferencingParentUpdTrigger);
-
- table_close(rel, NoLock);
-
return changed;
}
+/*
+ * A subroutine of ATExecAlterConstrRecurse that updates the enforceability of
+ * a foreign key constraint. Depending on whether the constraint is being set
+ * to enforced or not enforced, it creates or drops the trigger accordingly.
+ *
+ * The arguments to this function have the same meaning as the arguments to
+ * ATExecAlterConstrRecurse.
+ */
+static void
+ATExecAlterConstrEnforceability(List **wqueue, Constraint *cmdcon,
+ Relation conrel, Relation tgrel,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger)
+{
+ Form_pg_constraint currcon;
+ Oid conoid;
+
+ currcon = (Form_pg_constraint) GETSTRUCT(contuple);
+ conoid = currcon->oid;
+
+ /* Drop triggers */
+ if (!cmdcon->is_enforced)
+ {
+ HeapTuple tgtuple;
+ ScanKeyData tgkey;
+ SysScanDesc tgscan;
+
+ /*
+ * When setting a constraint to NOT ENFORCED, the constraint triggers
+ * need to be dropped. Therefore, we must process the child relations
+ * first, followed by the parent, to account for dependencies.
+ */
+ if (get_rel_relkind(currcon->conrelid) == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE)
+ ATExecAlterChildConstr(wqueue, cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, contuple, otherrelids, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
+
+ ScanKeyInit(&tgkey,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+ tgscan = systable_beginscan(tgrel, TriggerConstraintIndexId, true,
+ NULL, 1, &tgkey);
+ while (HeapTupleIsValid(tgtuple = systable_getnext(tgscan)))
+ {
+ Form_pg_trigger tgform = (Form_pg_trigger) GETSTRUCT(tgtuple);
+ ObjectAddress trigger;
+
+ /*
+ * Delete only RI_FKey_noaction_del, RI_FKey_noaction_upd,
+ * RI_FKey_check_ins and RI_FKey_check_upd triggers, but not
+ * others; see createForeignKeyActionTriggers and
+ * CreateFKCheckTrigger.
+ */
+ if (tgform->tgfoid != F_RI_FKEY_NOACTION_DEL &&
+ tgform->tgfoid != F_RI_FKEY_NOACTION_UPD &&
+ tgform->tgfoid != F_RI_FKEY_CHECK_INS &&
+ tgform->tgfoid != F_RI_FKEY_CHECK_UPD)
+ continue;
+
+ deleteDependencyRecordsFor(TriggerRelationId, tgform->oid,
+ false);
+ /* make dependency deletion visible to performDeletion */
+ CommandCounterIncrement();
+ ObjectAddressSet(trigger, TriggerRelationId, tgform->oid);
+ performDeletion(&trigger, DROP_RESTRICT, 0);
+ /* make trigger drop visible, in case the loop iterates */
+ CommandCounterIncrement();
+ }
+
+ systable_endscan(tgscan);
+ }
+ else /* Create triggers */
+ {
+ Relation rel;
+ Oid ReferencedDelTriggerOid = InvalidOid,
+ ReferencedUpdTriggerOid = InvalidOid,
+ ReferencingInsTriggerOid = InvalidOid,
+ ReferencingUpdTriggerOid = InvalidOid;
+
+ /* Update the foreign key action required for trigger creation. */
+ cmdcon->fk_matchtype = currcon->confmatchtype;
+ cmdcon->fk_upd_action = currcon->confupdtype;
+ cmdcon->fk_del_action = currcon->confdeltype;
+
+ rel = table_open(currcon->conrelid, lockmode);
+
+ /* Create referenced triggers */
+ if (currcon->conrelid == fkrelid)
+ createForeignKeyActionTriggers(rel,
+ currcon->confrelid,
+ cmdcon,
+ conoid,
+ currcon->conindid,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ &ReferencedDelTriggerOid,
+ &ReferencedUpdTriggerOid);
+
+ /* Create referencing triggers */
+ if (currcon->confrelid == pkrelid)
+ {
+ createForeignKeyCheckTriggers(currcon->conrelid,
+ pkrelid,
+ cmdcon,
+ conoid,
+ currcon->conindid,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger,
+ &ReferencingInsTriggerOid,
+ &ReferencingUpdTriggerOid);
+
+ /*
+ * Tell Phase 3 to check that the constraint is satisfied by
+ * existing rows, but skip this step if the rows have not been
+ * validated previously.
+ */
+ if (rel->rd_rel->relkind == RELKIND_RELATION &&
+ wqueue && currcon->convalidated)
+ {
+ NewConstraint *newcon;
+ AlteredTableInfo *tab;
+
+ tab = ATGetQueueEntry(wqueue, rel);
+
+ newcon = (NewConstraint *) palloc0(sizeof(NewConstraint));
+ newcon->name = pstrdup(NameStr(currcon->conname));
+ newcon->conid = currcon->oid;
+ newcon->contype = CONSTR_FOREIGN;
+ newcon->refrelid = pkrelid;
+ newcon->refindid = currcon->conindid;
+ newcon->conwithperiod = currcon->conperiod;
+ newcon->qual = (Node *) cmdcon;
+
+ tab->constraints = lappend(tab->constraints, newcon);
+ }
+ }
+
+ /*
+ * If the table at either end of the constraint is partitioned, we
+ * need to recurse and create triggers for each constraint that is a
+ * child of this one.
+ */
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE)
+ ATExecAlterChildConstr(wqueue, cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, contuple, otherrelids, lockmode,
+ ReferencedDelTriggerOid,
+ ReferencedUpdTriggerOid,
+ ReferencingInsTriggerOid,
+ ReferencingUpdTriggerOid);
+ table_close(rel, NoLock);
+ }
+}
+
/*
* A subroutine of ATExecAlterConstrRecurse that updated constraint trigger's
* deferrability.
@@ -11753,7 +11958,7 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
* ATExecAlterConstrRecurse.
*/
static void
-AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
+AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Oid fkrel,
bool deferrable, bool initdeferred,
List **otherrelids)
{
@@ -11779,7 +11984,7 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
* other rels that don't have a trigger whose properties change, but
* let's be conservative.)
*/
- if (tgform->tgrelid != RelationGetRelid(rel))
+ if (tgform->tgrelid != fkrel)
*otherrelids = list_append_unique_oid(*otherrelids,
tgform->tgrelid);
@@ -11818,8 +12023,8 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
* ATExecAlterConstrRecurse.
*/
static void
-ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
- const Oid fkrelid, const Oid pkrelid,
+ATExecAlterChildConstr(List **wqueue, Constraint *cmdcon, Relation conrel,
+ Relation tgrel, const Oid fkrelid, const Oid pkrelid,
HeapTuple contuple, List **otherrelids,
LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
Oid ReferencedParentUpdTrigger,
@@ -11844,9 +12049,9 @@ ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
true, NULL, 1, &pkey);
while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
- ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, fkrelid, pkrelid,
- childtup, otherrelids, lockmode,
- ReferencedParentDelTrigger,
+ ATExecAlterConstrRecurse(wqueue, cmdcon, conrel, tgrel,
+ fkrelid, pkrelid, childtup, otherrelids,
+ lockmode, ReferencedParentDelTrigger,
ReferencedParentUpdTrigger,
ReferencingParentInsTrigger,
ReferencingParentUpdTrigger);
@@ -11919,25 +12124,33 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
if (con->contype == CONSTRAINT_FOREIGN)
{
- NewConstraint *newcon;
- Constraint *fkconstraint;
+ /*
+ * Queue validation for phase 3 only if constraint is enforced;
+ * otherwise, adding it to the validation queue won't be very
+ * effective, as the verification will be skipped.
+ */
+ if (con->conenforced)
+ {
+ NewConstraint *newcon;
+ Constraint *fkconstraint;
- /* Queue validation for phase 3 */
- fkconstraint = makeNode(Constraint);
- /* for now this is all we need */
- fkconstraint->conname = constrName;
+ /* Queue validation for phase 3 */
+ fkconstraint = makeNode(Constraint);
+ /* for now this is all we need */
+ fkconstraint->conname = constrName;
- newcon = (NewConstraint *) palloc0(sizeof(NewConstraint));
- newcon->name = constrName;
- newcon->contype = CONSTR_FOREIGN;
- newcon->refrelid = con->confrelid;
- newcon->refindid = con->conindid;
- newcon->conid = con->oid;
- newcon->qual = (Node *) fkconstraint;
+ newcon = (NewConstraint *) palloc0(sizeof(NewConstraint));
+ newcon->name = constrName;
+ newcon->contype = CONSTR_FOREIGN;
+ newcon->refrelid = con->confrelid;
+ newcon->refindid = con->conindid;
+ newcon->conid = con->oid;
+ newcon->qual = (Node *) fkconstraint;
- /* Find or create work queue entry for this table */
- tab = ATGetQueueEntry(wqueue, rel);
- tab->constraints = lappend(tab->constraints, newcon);
+ /* Find or create work queue entry for this table */
+ tab = ATGetQueueEntry(wqueue, rel);
+ tab->constraints = lappend(tab->constraints, newcon);
+ }
/*
* We disallow creating invalid foreign keys to or from
@@ -11996,9 +12209,9 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
}
/*
- * Queue validation for phase 3 only if constraint is enforced;
- * otherwise, adding it to the validation queue won't be very
- * effective, as the verification will be skipped.
+ * Queue validation for phase 3 only if the constraint is
+ * enforced, for the same reason outlined for the foreign key
+ * constraint.
*/
if (con->conenforced)
{
@@ -12029,7 +12242,10 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
/*
* Now update the catalog regardless of enforcement; the validated
* flag will not take effect until the constraint is marked as
- * enforced.
+ * enforced. When changing a non-enforced constraint to enforced, the
+ * validation will only occur if the validation flag is true.
+ * Otherwise, the user will need to explicitly perform constraint
+ * validation.
*/
copyTuple = heap_copytuple(tuple);
copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
@@ -19522,8 +19738,6 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
Form_pg_constraint conform;
- Oid insertTriggerOid,
- updateTriggerOid;
contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
if (!HeapTupleIsValid(contup))
@@ -19546,17 +19760,25 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
/*
* Also, look up the partition's "check" triggers corresponding to the
- * constraint being detached and detach them from the parent triggers.
+ * enforced constraint being detached and detach them from the parent
+ * triggers. Non-enforced constraints do not have these triggers;
+ * therefore, this step is not needed.
*/
- GetForeignKeyCheckTriggers(trigrel,
- fk->conoid, fk->confrelid, fk->conrelid,
- &insertTriggerOid, &updateTriggerOid);
- Assert(OidIsValid(insertTriggerOid));
- TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
- RelationGetRelid(partRel));
- Assert(OidIsValid(updateTriggerOid));
- TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
- RelationGetRelid(partRel));
+ if (fk->conenforced)
+ {
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ Assert(OidIsValid(updateTriggerOid));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ }
/*
* Lastly, create the action triggers on the referenced table, using
@@ -19596,6 +19818,7 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
fkconstraint->conname = pstrdup(NameStr(conform->conname));
fkconstraint->deferrable = conform->condeferrable;
fkconstraint->initdeferred = conform->condeferred;
+ fkconstraint->is_enforced = conform->conenforced;
fkconstraint->skip_validation = true;
fkconstraint->initially_valid = true;
/* a few irrelevant fields omitted here */
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 7c632881226..9fb3a0819f3 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -4024,6 +4024,7 @@ ColConstraintElem:
n->fk_del_set_cols = ($5)->deleteAction->cols;
n->skip_validation = false;
n->initially_valid = true;
+ n->is_enforced = true;
$$ = (Node *) n;
}
;
@@ -4285,7 +4286,7 @@ ConstraintElem:
processCASbits($12, @12, "FOREIGN KEY",
&n->deferrable, &n->initdeferred,
&n->skip_validation, NULL,
- NULL,
+ &n->is_enforced,
yyscanner);
n->initially_valid = !n->skip_validation;
$$ = (Node *) n;
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 024cc256b7d..8dad6bfacb9 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -3818,7 +3818,8 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
case CONSTR_ATTR_ENFORCED:
if (lastprimarycon &&
- lastprimarycon->contype != CONSTR_CHECK)
+ lastprimarycon->contype != CONSTR_CHECK &&
+ lastprimarycon->contype != CONSTR_FOREIGN)
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("misplaced ENFORCED clause"),
@@ -3834,7 +3835,8 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
case CONSTR_ATTR_NOT_ENFORCED:
if (lastprimarycon &&
- lastprimarycon->contype != CONSTR_CHECK)
+ lastprimarycon->contype != CONSTR_CHECK &&
+ lastprimarycon->contype != CONSTR_FOREIGN)
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("misplaced NOT ENFORCED clause"),
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 01f734fa59d..e5fdf5ded46 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4701,6 +4701,7 @@ RelationGetFKeyList(Relation relation)
info->conoid = constraint->oid;
info->conrelid = constraint->conrelid;
info->confrelid = constraint->confrelid;
+ info->conenforced = constraint->conenforced;
DeconstructFkConstraintRow(htup, &info->nkeys,
info->conkey,
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 87002049538..2db011ddfb7 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -284,6 +284,9 @@ typedef struct ForeignKeyCacheInfo
/* number of columns in the foreign key */
int nkeys;
+ /* Is enforced ? */
+ bool conenforced;
+
/*
* these arrays each have nkeys valid entries:
*/
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 69994c98e32..f7cad504c14 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -1,12 +1,43 @@
--
-- FOREIGN KEY
--
--- MATCH FULL
+-- NOT ENFORCED
--
-- First test, check and cascade
--
CREATE TABLE PKTABLE ( ptest1 int PRIMARY KEY, ptest2 text );
-CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE, ftest2 int );
+CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE NOT ENFORCED,
+ ftest2 int );
+-- Inserting into the foreign key table will not result in an error, even if
+-- there is no matching key in the referenced table.
+INSERT INTO FKTABLE VALUES (1, 2);
+INSERT INTO FKTABLE VALUES (2, 3);
+-- Check FKTABLE
+SELECT * FROM FKTABLE;
+ ftest1 | ftest2
+--------+--------
+ 1 | 2
+ 2 | 3
+(2 rows)
+
+-- Changing it back to ENFORCED will result in an error due to validation of
+-- existing rows.
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_fkey"
+DETAIL: Key (ftest1)=(1) is not present in table "pktable".
+-- Remove any existing rows that violate the constraint, then attempt to change
+-- it.
+TRUNCATE FKTABLE;
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+-- Any further inserts will fail due to the enforcement.
+INSERT INTO FKTABLE VALUES (3, 4);
+ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_fkey"
+DETAIL: Key (ftest1)=(3) is not present in table "pktable".
+--
+-- MATCH FULL
+--
+-- First test, check and cascade
+--
-- Insert test data into PKTABLE
INSERT INTO PKTABLE VALUES (1, 'Test1');
INSERT INTO PKTABLE VALUES (2, 'Test2');
@@ -351,6 +382,8 @@ INSERT INTO FKTABLE VALUES (1, NULL);
ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL;
ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_ftest2_fkey"
DETAIL: MATCH FULL does not allow mixing of null and nonnull key values.
+-- NOT ENFORCED will bypass the initial check.
+ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL NOT ENFORCED;
DROP TABLE FKTABLE;
DROP TABLE PKTABLE;
-- MATCH SIMPLE
@@ -1276,13 +1309,24 @@ INSERT INTO fktable VALUES (0, 20);
ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_fk_fkey"
DETAIL: Key (fk)=(20) is not present in table "pktable".
COMMIT;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT ENFORCED;
+BEGIN;
+-- doesn't match FK, no error.
+UPDATE pktable SET id = 10 WHERE id = 5;
+-- doesn't match PK, no error.
+INSERT INTO fktable VALUES (0, 20);
+ROLLBACK;
-- try additional syntax
-ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE ENFORCED;
-- illegal option
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE INITIALLY DEFERRED;
ERROR: constraint declared INITIALLY DEFERRED must be DEFERRABLE
LINE 1: ...e ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE INITIALLY ...
^
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORCED;
+ERROR: conflicting constraint properties
+LINE 1: ...fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORC...
+ ^
-- test order of firing of FK triggers when several RI-induced changes need to
-- be made to the same row. This was broken by subtransaction-related
-- changes in 8.0.
@@ -1580,7 +1624,8 @@ ALTER TABLE fk_partitioned_fk DROP COLUMN fdrop1;
CREATE TABLE fk_partitioned_fk_1 (fdrop1 int, fdrop2 int, a int, fdrop3 int, b int);
ALTER TABLE fk_partitioned_fk_1 DROP COLUMN fdrop1, DROP COLUMN fdrop2, DROP COLUMN fdrop3;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FROM (0,0) TO (1000,1000);
-ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk;
+ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
@@ -1665,6 +1710,37 @@ Indexes:
Referenced by:
TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b)
+-- Check the exsting FK trigger
+CREATE TEMP TABLE tmp_trg_info AS SELECT tgtype, tgrelid::regclass, tgconstraint
+FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+SELECT count(1) FROM tmp_trg_info;
+ count
+-------
+ 14
+(1 row)
+
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+-- No triggers
+SELECT count(1) FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+ count
+-------
+ 0
+(1 row)
+
+-- Changing it back to ENFORCED will recreate the necessary triggers.
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+-- Should be exactly the same number of triggers found as before
+SELECT COUNT(1) FROM pg_trigger pgt JOIN tmp_trg_info tt
+ON (pgt.tgtype = tt.tgtype AND pgt.tgrelid = tt.tgrelid AND pgt.tgconstraint = tt.tgconstraint);
+ count
+-------
+ 14
+(1 row)
+
ALTER TABLE fk_partitioned_fk DROP CONSTRAINT fk_partitioned_fk_a_b_fkey;
-- done.
DROP TABLE fk_notpartitioned_pk, fk_partitioned_fk;
@@ -1868,6 +1944,40 @@ Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
Foreign-key constraints:
TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
+DROP TABLE fk_partitioned_fk_2;
+CREATE TABLE fk_partitioned_fk_2 (b int, a int,
+ FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
+BEGIN;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have two constraints
+\d fk_partitioned_fk_2
+ Table "public.fk_partitioned_fk_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ b | integer | | |
+ a | integer | | |
+Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
+Foreign-key constraints:
+ "fk_partitioned_fk_2_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED
+ TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
+
+DROP TABLE fk_partitioned_fk_2;
+ROLLBACK;
+BEGIN;
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have only one constraint
+\d fk_partitioned_fk_2
+ Table "public.fk_partitioned_fk_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ b | integer | | |
+ a | integer | | |
+Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
+Foreign-key constraints:
+ TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED
+
+ROLLBACK;
DROP TABLE fk_partitioned_fk_2;
CREATE TABLE fk_partitioned_fk_4 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE) PARTITION BY RANGE (b, a);
CREATE TABLE fk_partitioned_fk_4_1 PARTITION OF fk_partitioned_fk_4 FOR VALUES FROM (1,1) TO (100,100);
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index 2e710e419c2..33a43e2dd70 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -2,13 +2,39 @@
-- FOREIGN KEY
--
--- MATCH FULL
+-- NOT ENFORCED
--
-- First test, check and cascade
--
CREATE TABLE PKTABLE ( ptest1 int PRIMARY KEY, ptest2 text );
-CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE, ftest2 int );
+CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE NOT ENFORCED,
+ ftest2 int );
+-- Inserting into the foreign key table will not result in an error, even if
+-- there is no matching key in the referenced table.
+INSERT INTO FKTABLE VALUES (1, 2);
+INSERT INTO FKTABLE VALUES (2, 3);
+
+-- Check FKTABLE
+SELECT * FROM FKTABLE;
+
+-- Changing it back to ENFORCED will result in an error due to validation of
+-- existing rows.
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+
+-- Remove any existing rows that violate the constraint, then attempt to change
+-- it.
+TRUNCATE FKTABLE;
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+
+-- Any further inserts will fail due to the enforcement.
+INSERT INTO FKTABLE VALUES (3, 4);
+
+--
+-- MATCH FULL
+--
+-- First test, check and cascade
+--
-- Insert test data into PKTABLE
INSERT INTO PKTABLE VALUES (1, 'Test1');
INSERT INTO PKTABLE VALUES (2, 'Test2');
@@ -230,6 +256,9 @@ INSERT INTO FKTABLE VALUES (1, NULL);
ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL;
+-- NOT ENFORCED will bypass the initial check.
+ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL NOT ENFORCED;
+
DROP TABLE FKTABLE;
DROP TABLE PKTABLE;
@@ -968,10 +997,22 @@ INSERT INTO fktable VALUES (0, 20);
COMMIT;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT ENFORCED;
+
+BEGIN;
+
+-- doesn't match FK, no error.
+UPDATE pktable SET id = 10 WHERE id = 5;
+-- doesn't match PK, no error.
+INSERT INTO fktable VALUES (0, 20);
+
+ROLLBACK;
+
-- try additional syntax
-ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE ENFORCED;
-- illegal option
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORCED;
-- test order of firing of FK triggers when several RI-induced changes need to
-- be made to the same row. This was broken by subtransaction-related
@@ -1182,7 +1223,8 @@ ALTER TABLE fk_partitioned_fk DROP COLUMN fdrop1;
CREATE TABLE fk_partitioned_fk_1 (fdrop1 int, fdrop2 int, a int, fdrop3 int, b int);
ALTER TABLE fk_partitioned_fk_1 DROP COLUMN fdrop1, DROP COLUMN fdrop2, DROP COLUMN fdrop3;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FROM (0,0) TO (1000,1000);
-ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk;
+ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
@@ -1236,6 +1278,27 @@ UPDATE fk_notpartitioned_pk SET b = 1502 WHERE a = 1500;
UPDATE fk_notpartitioned_pk SET b = 2504 WHERE a = 2500;
-- check psql behavior
\d fk_notpartitioned_pk
+
+-- Check the exsting FK trigger
+CREATE TEMP TABLE tmp_trg_info AS SELECT tgtype, tgrelid::regclass, tgconstraint
+FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+SELECT count(1) FROM tmp_trg_info;
+
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+-- No triggers
+SELECT count(1) FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+
+-- Changing it back to ENFORCED will recreate the necessary triggers.
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+
+-- Should be exactly the same number of triggers found as before
+SELECT COUNT(1) FROM pg_trigger pgt JOIN tmp_trg_info tt
+ON (pgt.tgtype = tt.tgtype AND pgt.tgrelid = tt.tgrelid AND pgt.tgconstraint = tt.tgconstraint);
+
ALTER TABLE fk_partitioned_fk DROP CONSTRAINT fk_partitioned_fk_a_b_fkey;
-- done.
DROP TABLE fk_notpartitioned_pk, fk_partitioned_fk;
@@ -1372,6 +1435,22 @@ ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN
\d fk_partitioned_fk_2
DROP TABLE fk_partitioned_fk_2;
+CREATE TABLE fk_partitioned_fk_2 (b int, a int,
+ FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
+BEGIN;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have two constraints
+\d fk_partitioned_fk_2
+DROP TABLE fk_partitioned_fk_2;
+ROLLBACK;
+BEGIN;
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have only one constraint
+\d fk_partitioned_fk_2
+ROLLBACK;
+DROP TABLE fk_partitioned_fk_2;
+
CREATE TABLE fk_partitioned_fk_4 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE) PARTITION BY RANGE (b, a);
CREATE TABLE fk_partitioned_fk_4_1 PARTITION OF fk_partitioned_fk_4 FOR VALUES FROM (1,1) TO (100,100);
CREATE TABLE fk_partitioned_fk_4_2 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE SET NULL);
--
2.43.5
I started reviewing patch 0001 for check constraints. I think it's a
good idea how you structured it so that we can start with this
relatively simple feature and get all the syntax parsing etc. right.
I also looked over the remaining patches a bit. The general structure
looks right to me. But I haven't done a detailed review yet.
The 0001 patch needs a rebase over the recently re-committed patch for
catalogued not-null constraints. This might need a little work to
verify that everything still makes sense.
(I suppose technically we could support not-enforced not-null
constraints. But I would stay away from that for now. That not-null
constraints business is very complicated, don't get dragged into
it. ;-) )
Some more detailed comments on the code:
* src/backend/access/common/tupdesc.c
Try to keep the order of the fields consistent. In tupdesc.h you have
ccenforced before ccnoinherit, here you have it after. Either way is
fine, but let's keep it consistent. (If you change it in tupdesc.h,
also check relcache.c.)
* src/backend/commands/tablecmds.c
cooked->skip_validation = false;
+ cooked->is_enforced = true;
cooked->is_local = true; /* not used for defaults */
cooked->inhcount = 0; /* ditto */
Add a comment like "not used for defaults" to the new line.
Or maybe this should be rewritten slightly. There might be more
fields that are not used for defaults, like "skip_validation"? Maybe
they just shouldn't be set here, seems useless and confusing.
@@ -9481,6 +9484,9 @@ ATAddCheckConstraint(List **wqueue,
AlteredTableInfo *tab, Relation rel,
{
CookedConstraint *ccon = (CookedConstraint *) lfirst(lcon);
+ /* Only CHECK constraint can be not enforced */
+ Assert(ccon->is_enforced || ccon->contype == CONSTRAINT_CHECK);
+
Is this assertion useful, since we are already in a function named
ATAddCheckConstraint()?
@@ -11947,7 +11961,9 @@ ATExecValidateConstraint(List **wqueue, Relation
rel, char *constrName,
}
/*
- * Now update the catalog, while we have the door open.
+ * Now update the catalog regardless of enforcement; the validated
+ * flag will not take effect until the constraint is marked as
+ * enforced.
*/
Can you clarify what you mean here? Is the enforced flag set later?
I don't see that in the code. What is the interaction between
constraint validation and the enforced flag?
* src/backend/commands/typecmds.c
You should also check and error if CONSTR_ATTR_ENFORCED is specified
(even though it's effectively the default). This matches SQL standard
language: "For every <domain constraint> specified: ... If <constraint
characteristics> is specified, then neither ENFORCED nor NOT ENFORCED
shall be specified."
The error code should be something like
ERRCODE_INVALID_OBJECT_DEFINITION instead of
ERRCODE_FEATURE_NOT_SUPPORTED. The former is more for features that
are impossible, the latter for features we haven't gotten to yet.
* src/backend/parser/gram.y
Same as above, in processCASbits(), you should add a similar check for
CAS_ENFORCED, meaning that for example specifying UNIQUE ENFORCED is
not allowed (even though it's the default). This matches SQL standard
language: "If <unique constraint definition> is specified, then
<constraint characteristics> shall not specify a <constraint
enforcement>."
* src/backend/parser/parse_utilcmd.c
@@ -1317,6 +1321,7 @@ expandTableLikeClause(RangeVar *heapRel,
TableLikeClause *table_like_clause)
n->is_no_inherit = ccnoinherit;
n->raw_expr = NULL;
n->cooked_expr = nodeToString(ccbin_node);
+ n->is_enforced = true;
This has the effect that if you use the LIKE clause with INCLUDING
CONSTRAINTS, the new constraint is always ENFORCED. Is this what we
want? Did you have a reason? I'm not sure what the ideal behavior
might be. But if we want it like this, maybe we should document this
or at least put a comment here or something.
* src/backend/utils/adt/ruleutils.c
The syntax requires the NOT ENFORCED clause to be after DEFERRABLE
etc., but this code does it the other way around. You should move the
new code after the switch statement and below the DEFERRABLE stuff.
I wouldn't worry about restricting it based on constraint type. The
DEFERRABLE stuff doesn't do that either. We can assume that the
catalog contents are sane.
* src/include/catalog/pg_constraint.h
There needs to be an update in catalogs.sgml for the new catalog column.
* src/test/regress/sql/constraints.sql
Possible additional test cases:
- trying [NOT] ENFORCED with a domain (CREATE and ALTER cases)
- trying [NOT] ENFORCED with an unsupported constraint type (maybe UNIQUE)
A note for the later patches: With patches 0001 through 0005 applied,
I get compiler warnings:
../src/backend/commands/tablecmds.c:10918:17: error: 'deleteTriggerOid'
may be used uninitialized [-Werror=maybe-uninitialized]
../src/backend/commands/tablecmds.c:10918:17: error: 'updateTriggerOid'
may be used uninitialized [-Werror=maybe-uninitialized]
(both with gcc and clang).
On Tue, Nov 12, 2024 at 3:48 PM Peter Eisentraut <peter@eisentraut.org> wrote:
I started reviewing patch 0001 for check constraints. I think it's a
good idea how you structured it so that we can start with this
relatively simple feature and get all the syntax parsing etc. right.I also looked over the remaining patches a bit. The general structure
looks right to me. But I haven't done a detailed review yet.
Thank you for your feedback and suggestions.
The 0001 patch needs a rebase over the recently re-committed patch for
catalogued not-null constraints. This might need a little work to
verify that everything still makes sense.(I suppose technically we could support not-enforced not-null
constraints. But I would stay away from that for now. That not-null
constraints business is very complicated, don't get dragged into
it. ;-) )
True. I had a quick conversation with Álvaro at PGConf.EU about my
current work and the pending ALTER CONSTRAINT support for CHECK
constraints. He mentioned that we might also need the same support for
NULL constraints. I'll look into that as well while working on ALTER
CONSTRAINT.
Some more detailed comments on the code:
* src/backend/access/common/tupdesc.c
Try to keep the order of the fields consistent. In tupdesc.h you have
ccenforced before ccnoinherit, here you have it after. Either way is
fine, but let's keep it consistent. (If you change it in tupdesc.h,
also check relcache.c.)
Done.
* src/backend/commands/tablecmds.c
cooked->skip_validation = false;
+ cooked->is_enforced = true;
cooked->is_local = true; /* not used for defaults */
cooked->inhcount = 0; /* ditto */Add a comment like "not used for defaults" to the new line.
Or maybe this should be rewritten slightly. There might be more
fields that are not used for defaults, like "skip_validation"? Maybe
they just shouldn't be set here, seems useless and confusing.
Yeah, setting here is confusing, I removed that we do not need.
@@ -9481,6 +9484,9 @@ ATAddCheckConstraint(List **wqueue,
AlteredTableInfo *tab, Relation rel,
{
CookedConstraint *ccon = (CookedConstraint *) lfirst(lcon);+ /* Only CHECK constraint can be not enforced */ + Assert(ccon->is_enforced || ccon->contype == CONSTRAINT_CHECK); +Is this assertion useful, since we are already in a function named
ATAddCheckConstraint()?
Yes, Removed this. Assertion in CreateConstraintEntry() is more than enough.
@@ -11947,7 +11961,9 @@ ATExecValidateConstraint(List **wqueue, Relation
rel, char *constrName,
}/* - * Now update the catalog, while we have the door open. + * Now update the catalog regardless of enforcement; the validated + * flag will not take effect until the constraint is marked as + * enforced. */Can you clarify what you mean here? Is the enforced flag set later?
I don't see that in the code. What is the interaction between
constraint validation and the enforced flag?
I revised the comment in the 0005 patch for clarity. My intent is
that, to trigger validation, the constraint must be enforced.
Additionally, when changing a constraint from non-enforced to
enforced, similar validation is triggered only if the constraint is
valid; otherwise, we simply update the constraint enforceability only.
* src/backend/commands/typecmds.c
You should also check and error if CONSTR_ATTR_ENFORCED is specified
(even though it's effectively the default). This matches SQL standard
language: "For every <domain constraint> specified: ... If <constraint
characteristics> is specified, then neither ENFORCED nor NOT ENFORCED
shall be specified."The error code should be something like
ERRCODE_INVALID_OBJECT_DEFINITION instead of
ERRCODE_FEATURE_NOT_SUPPORTED. The former is more for features that
are impossible, the latter for features we haven't gotten to yet.
Understood. Fixed.
* src/backend/parser/gram.y
Same as above, in processCASbits(), you should add a similar check for
CAS_ENFORCED, meaning that for example specifying UNIQUE ENFORCED is
not allowed (even though it's the default). This matches SQL standard
language: "If <unique constraint definition> is specified, then
<constraint characteristics> shall not specify a <constraint
enforcement>."
Done. In processCASbits(), the error code will be
ERRCODE_FEATURE_NOT_SUPPORTED for consistency with the previous error.
Additionally, is_enforced = true is updated again in the CAS_ENFORCED
check block to align with the existing code style, which I believe is
reasonable.
* src/backend/parser/parse_utilcmd.c
@@ -1317,6 +1321,7 @@ expandTableLikeClause(RangeVar *heapRel,
TableLikeClause *table_like_clause)
n->is_no_inherit = ccnoinherit;
n->raw_expr = NULL;
n->cooked_expr = nodeToString(ccbin_node);
+ n->is_enforced = true;This has the effect that if you use the LIKE clause with INCLUDING
CONSTRAINTS, the new constraint is always ENFORCED. Is this what we
want? Did you have a reason? I'm not sure what the ideal behavior
might be. But if we want it like this, maybe we should document this
or at least put a comment here or something.
You are correct; this is a bug. It has been fixed in the attached
version, and tests have been added for it.
* src/backend/utils/adt/ruleutils.c
The syntax requires the NOT ENFORCED clause to be after DEFERRABLE
etc., but this code does it the other way around. You should move the
new code after the switch statement and below the DEFERRABLE stuff.I wouldn't worry about restricting it based on constraint type. The
DEFERRABLE stuff doesn't do that either. We can assume that the
catalog contents are sane.
Done.
* src/include/catalog/pg_constraint.h
There needs to be an update in catalogs.sgml for the new catalog column.
Done.
* src/test/regress/sql/constraints.sql
Possible additional test cases:
- trying [NOT] ENFORCED with a domain (CREATE and ALTER cases)
- trying [NOT] ENFORCED with an unsupported constraint type (maybe UNIQUE)
Added. I thought about adding tests for all other constraints, but it
seemed excessive, so I decided not to.
A note for the later patches: With patches 0001 through 0005 applied,
I get compiler warnings:../src/backend/commands/tablecmds.c:10918:17: error: 'deleteTriggerOid'
may be used uninitialized [-Werror=maybe-uninitialized]
../src/backend/commands/tablecmds.c:10918:17: error: 'updateTriggerOid'
may be used uninitialized [-Werror=maybe-uninitialized](both with gcc and clang).
For some reason, my GCC 11.5 on the CentOS machine isn’t showing this
warning. However, I found the unassigned variable, fixed it, and tried
compiling on macOS, where it's now clean.
Updated version attached.
Regards,
Amul
Attachments:
v3-0001-Add-support-for-NOT-ENFORCED-in-CHECK-constraints.patchapplication/x-patch; name=v3-0001-Add-support-for-NOT-ENFORCED-in-CHECK-constraints.patchDownload
From fda5036b48abae4db92180a85dcc86a16de2ea2f Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Thu, 14 Nov 2024 18:07:06 +0530
Subject: [PATCH v3 1/5] Add support for NOT ENFORCED in CHECK constraints.
This added basic infrastructure for the NOT ENFORCED/ENFORCED
constraint supports where incluing grammar and catalog flags.
Note that CHECK constraints do not currently support ALTER operations,
so changing the enforceability of an existing constraint isn't
possible without dropping and recreating it. We could consider
adding support for altering CHECK constraints either in this
patch series or as a separatly.
---
doc/src/sgml/catalogs.sgml | 10 +++
doc/src/sgml/ddl.sgml | 8 +-
doc/src/sgml/ref/alter_table.sgml | 14 +--
doc/src/sgml/ref/create_table.sgml | 18 +++-
src/backend/access/common/tupdesc.c | 2 +
src/backend/catalog/heap.c | 13 ++-
src/backend/catalog/index.c | 1 +
src/backend/catalog/pg_constraint.c | 5 ++
src/backend/commands/tablecmds.c | 49 +++++++----
src/backend/commands/trigger.c | 1 +
src/backend/commands/typecmds.c | 15 ++++
src/backend/executor/execMain.c | 8 +-
src/backend/nodes/makefuncs.c | 1 +
src/backend/parser/gram.y | 87 +++++++++++++++----
src/backend/parser/parse_utilcmd.c | 40 +++++++++
src/backend/utils/adt/ruleutils.c | 2 +
src/backend/utils/cache/relcache.c | 1 +
src/include/access/tupdesc.h | 1 +
src/include/catalog/heap.h | 1 +
src/include/catalog/pg_constraint.h | 2 +
src/include/nodes/parsenodes.h | 3 +
src/include/parser/kwlist.h | 1 +
src/test/regress/expected/constraints.out | 49 ++++++++++-
.../regress/expected/create_table_like.out | 10 ++-
src/test/regress/sql/constraints.sql | 28 +++++-
src/test/regress/sql/create_table_like.sql | 3 +-
26 files changed, 318 insertions(+), 55 deletions(-)
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 59bb833f48d..6357771dc57 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2603,6 +2603,16 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l
</para></entry>
</row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>conenforced</structfield> <type>bool</type>
+ </para>
+ <para>
+ Is the constraint enforceable?
+ Currently, can be false only for CHECK constraints
+ </para></entry>
+ </row>
+
<row>
<entry role="catalog_table_entry"><para role="column_definition">
<structfield>conrelid</structfield> <type>oid</type>
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index 3c56610d2ac..9e2708586d5 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -584,7 +584,10 @@ CREATE TABLE products (
consists of the key word <literal>CHECK</literal> followed by an
expression in parentheses. The check constraint expression should
involve the column thus constrained, otherwise the constraint
- would not make too much sense.
+ would not make too much sense. The check constraint expression can be
+ followed by the <literal>ENFORCED</literal>, which is the default, or
+ <literal>NOT ENFORCED</literal> keyword, determining whether the
+ constraint check is performed or skipped after each statement.
</para>
<indexterm>
@@ -720,7 +723,8 @@ CREATE TABLE products (
to implement that. (This approach avoids the dump/restore problem because
<application>pg_dump</application> does not reinstall triggers until after
restoring data, so that the check will not be enforced during a
- dump/restore.)
+ dump/restore.) You can add that CHECK constraint with <literal>NOT ENFORCED</literal>,
+ allowing the underlying constraint rule to be represented without being enforced.
</para>
</note>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index c8f7ab7d956..ca38937cf1e 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -58,7 +58,7 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable>
ALTER [ COLUMN ] <replaceable class="parameter">column_name</replaceable> SET COMPRESSION <replaceable class="parameter">compression_method</replaceable>
ADD <replaceable class="parameter">table_constraint</replaceable> [ NOT VALID ]
ADD <replaceable class="parameter">table_constraint_using_index</replaceable>
- ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+ ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]
VALIDATE CONSTRAINT <replaceable class="parameter">constraint_name</replaceable>
DROP CONSTRAINT [ IF EXISTS ] <replaceable class="parameter">constraint_name</replaceable> [ RESTRICT | CASCADE ]
DISABLE TRIGGER [ <replaceable class="parameter">trigger_name</replaceable> | ALL | USER ]
@@ -108,7 +108,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
PRIMARY KEY <replaceable class="parameter">index_parameters</replaceable> |
REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> ) ] [ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ]
[ ON DELETE <replaceable class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ] }
-[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]
<phrase>and <replaceable class="parameter">table_constraint</replaceable> is:</phrase>
@@ -120,7 +120,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
EXCLUDE [ USING <replaceable class="parameter">index_method</replaceable> ] ( <replaceable class="parameter">exclude_element</replaceable> WITH <replaceable class="parameter">operator</replaceable> [, ... ] ) <replaceable class="parameter">index_parameters</replaceable> [ WHERE ( <replaceable class="parameter">predicate</replaceable> ) ] |
FOREIGN KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">column_name</replaceable> ] ) REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">refcolumn</replaceable> ] ) ]
[ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ] [ ON DELETE <replaceable class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ] }
-[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]
<phrase>and <replaceable class="parameter">table_constraint_using_index</replaceable> is:</phrase>
@@ -1423,9 +1423,11 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</para>
<para>
- Adding a <literal>CHECK</literal> or <literal>NOT NULL</literal> constraint requires
- scanning the table to verify that existing rows meet the constraint,
- but does not require a table rewrite.
+ Adding an enforced <literal>CHECK</literal> or <literal>NOT NULL</literal>
+ constraint requires scanning the table to verify that existing rows meet the
+ constraint, but does not require a table rewrite. If a <literal>CHECK</literal>
+ constraint is added as <literal>NOT ENFORCED</literal>, the validation will
+ not be performed.
</para>
<para>
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index 6262533c57b..97c9af44f50 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -71,7 +71,7 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI
PRIMARY KEY <replaceable class="parameter">index_parameters</replaceable> |
REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> ) ] [ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ]
[ ON DELETE <replaceable class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ] }
-[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]
<phrase>and <replaceable class="parameter">table_constraint</replaceable> is:</phrase>
@@ -84,7 +84,7 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI
FOREIGN KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">column_name</replaceable> ] ) REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">refcolumn</replaceable> ] ) ]
[ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ] [ ON DELETE <replaceable
class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ] }
-[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]
<phrase>and <replaceable class="parameter">like_option</replaceable> is:</phrase>
@@ -1368,6 +1368,20 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</listitem>
</varlistentry>
+ <varlistentry id="sql-createtable-parms-enforce">
+ <term><literal>ENFORCED</literal></term>
+ <term><literal>NOT ENFORCED</literal></term>
+ <listitem>
+ <para>
+ This is currently only allowed for <literal>CHECK</literal> constraints.
+ If the constraint is <literal>NOT ENFORCED</literal>, this clause
+ specifies that the constraint check will be skipped. When the constraint
+ is <literal>ENFORCED</literal>, check is performed after each statement.
+ This is the default.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="sql-createtable-method">
<term><literal>USING <replaceable class="parameter">method</replaceable></literal></term>
<listitem>
diff --git a/src/backend/access/common/tupdesc.c b/src/backend/access/common/tupdesc.c
index 47379fef220..9d118dad4fe 100644
--- a/src/backend/access/common/tupdesc.c
+++ b/src/backend/access/common/tupdesc.c
@@ -225,6 +225,7 @@ CreateTupleDescCopyConstr(TupleDesc tupdesc)
cpy->check[i].ccname = pstrdup(constr->check[i].ccname);
cpy->check[i].ccbin = pstrdup(constr->check[i].ccbin);
cpy->check[i].ccvalid = constr->check[i].ccvalid;
+ cpy->check[i].ccenforced = constr->check[i].ccenforced;
cpy->check[i].ccnoinherit = constr->check[i].ccnoinherit;
}
}
@@ -548,6 +549,7 @@ equalTupleDescs(TupleDesc tupdesc1, TupleDesc tupdesc2)
if (!(strcmp(check1->ccname, check2->ccname) == 0 &&
strcmp(check1->ccbin, check2->ccbin) == 0 &&
check1->ccvalid == check2->ccvalid &&
+ check1->ccenforced == check2->ccenforced &&
check1->ccnoinherit == check2->ccnoinherit))
return false;
}
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 003af4bf21c..9e531232be0 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -103,7 +103,7 @@ static ObjectAddress AddNewRelationType(const char *typeName,
static void RelationRemoveInheritance(Oid relid);
static Oid StoreRelCheck(Relation rel, const char *ccname, Node *expr,
bool is_validated, bool is_local, int16 inhcount,
- bool is_no_inherit, bool is_internal);
+ bool is_no_inherit, bool is_enforced, bool is_internal);
static void StoreConstraints(Relation rel, List *cooked_constraints,
bool is_internal);
static bool MergeWithExistingConstraint(Relation rel, const char *ccname, Node *expr,
@@ -2075,7 +2075,7 @@ SetAttrMissing(Oid relid, char *attname, char *value)
static Oid
StoreRelCheck(Relation rel, const char *ccname, Node *expr,
bool is_validated, bool is_local, int16 inhcount,
- bool is_no_inherit, bool is_internal)
+ bool is_no_inherit, bool is_enforced, bool is_internal)
{
char *ccbin;
List *varList;
@@ -2141,6 +2141,7 @@ StoreRelCheck(Relation rel, const char *ccname, Node *expr,
false, /* Is Deferrable */
false, /* Is Deferred */
is_validated,
+ is_enforced, /* Is Enforced */
InvalidOid, /* no parent constraint */
RelationGetRelid(rel), /* relation */
attNos, /* attrs in the constraint */
@@ -2194,6 +2195,7 @@ StoreRelNotNull(Relation rel, const char *nnname, AttrNumber attnum,
false,
false,
is_validated,
+ true, /* Is Enforced */
InvalidOid,
RelationGetRelid(rel),
&attnum,
@@ -2264,7 +2266,7 @@ StoreConstraints(Relation rel, List *cooked_constraints, bool is_internal)
StoreRelCheck(rel, con->name, con->expr,
!con->skip_validation, con->is_local,
con->inhcount, con->is_no_inherit,
- is_internal);
+ con->is_enforced, is_internal);
numchecks++;
break;
@@ -2406,6 +2408,7 @@ AddRelationNewConstraints(Relation rel,
cooked->attnum = colDef->attnum;
cooked->expr = expr;
cooked->skip_validation = false;
+ cooked->is_enforced = true;
cooked->is_local = is_local;
cooked->inhcount = is_local ? 0 : 1;
cooked->is_no_inherit = false;
@@ -2525,7 +2528,8 @@ AddRelationNewConstraints(Relation rel,
*/
constrOid =
StoreRelCheck(rel, ccname, expr, cdef->initially_valid, is_local,
- is_local ? 0 : 1, cdef->is_no_inherit, is_internal);
+ is_local ? 0 : 1, cdef->is_no_inherit,
+ cdef->is_enforced, is_internal);
numchecks++;
@@ -2536,6 +2540,7 @@ AddRelationNewConstraints(Relation rel,
cooked->attnum = 0;
cooked->expr = expr;
cooked->skip_validation = cdef->skip_validation;
+ cooked->is_enforced = cdef->is_enforced;
cooked->is_local = is_local;
cooked->inhcount = is_local ? 0 : 1;
cooked->is_no_inherit = cdef->is_no_inherit;
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index f9bb721c5fe..92455fc2832 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -1958,6 +1958,7 @@ index_constraint_create(Relation heapRelation,
deferrable,
initdeferred,
true,
+ true, /* Is Enforced */
parentConstraintId,
RelationGetRelid(heapRelation),
indexInfo->ii_IndexAttrNumbers,
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index 9c05a98d28c..f83167a8a69 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -54,6 +54,7 @@ CreateConstraintEntry(const char *constraintName,
bool isDeferrable,
bool isDeferred,
bool isValidated,
+ bool isEnforced,
Oid parentConstrId,
Oid relId,
const int16 *constraintKey,
@@ -99,6 +100,9 @@ CreateConstraintEntry(const char *constraintName,
ObjectAddresses *addrs_auto;
ObjectAddresses *addrs_normal;
+ /* Only CHECK constraint can be not enforced */
+ Assert(isEnforced || constraintType == CONSTRAINT_CHECK);
+
conDesc = table_open(ConstraintRelationId, RowExclusiveLock);
Assert(constraintName);
@@ -183,6 +187,7 @@ CreateConstraintEntry(const char *constraintName,
values[Anum_pg_constraint_condeferrable - 1] = BoolGetDatum(isDeferrable);
values[Anum_pg_constraint_condeferred - 1] = BoolGetDatum(isDeferred);
values[Anum_pg_constraint_convalidated - 1] = BoolGetDatum(isValidated);
+ values[Anum_pg_constraint_conenforced - 1] = BoolGetDatum(isEnforced);
values[Anum_pg_constraint_conrelid - 1] = ObjectIdGetDatum(relId);
values[Anum_pg_constraint_contypid - 1] = ObjectIdGetDatum(domainId);
values[Anum_pg_constraint_conindid - 1] = ObjectIdGetDatum(indexRelId);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index ccd9645e7d2..524c1e5f40b 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -373,7 +373,7 @@ static void RangeVarCallbackForTruncate(const RangeVar *relation,
static List *MergeAttributes(List *columns, const List *supers, char relpersistence,
bool is_partition, List **supconstr,
List **supnotnulls);
-static List *MergeCheckConstraint(List *constraints, const char *name, Node *expr);
+static List *MergeCheckConstraint(List *constraints, const char *name, Node *expr, bool is_enforced);
static void MergeChildAttribute(List *inh_columns, int exist_attno, int newcol_attno, const ColumnDef *newdef);
static ColumnDef *MergeInheritedAttribute(List *inh_columns, int exist_attno, const ColumnDef *newdef);
static void MergeAttributesIntoExisting(Relation child_rel, Relation parent_rel, bool ispartition);
@@ -2885,7 +2885,8 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
name,
RelationGetRelationName(relation))));
- constraints = MergeCheckConstraint(constraints, name, expr);
+ constraints = MergeCheckConstraint(constraints, name, expr,
+ check[i].ccenforced);
}
}
@@ -3099,7 +3100,7 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
* the list.
*/
static List *
-MergeCheckConstraint(List *constraints, const char *name, Node *expr)
+MergeCheckConstraint(List *constraints, const char *name, Node *expr, bool is_enforced)
{
ListCell *lc;
CookedConstraint *newcon;
@@ -3140,6 +3141,7 @@ MergeCheckConstraint(List *constraints, const char *name, Node *expr)
newcon->name = pstrdup(name);
newcon->expr = expr;
newcon->inhcount = 1;
+ newcon->is_enforced = is_enforced;
return lappend(constraints, newcon);
}
@@ -10371,6 +10373,7 @@ addFkConstraint(addFkConstraintSides fkside,
fkconstraint->deferrable,
fkconstraint->initdeferred,
fkconstraint->initially_valid,
+ true, /* Is Enforced */
parentConstr,
RelationGetRelid(rel),
fkattnum,
@@ -12040,22 +12043,29 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
table_close(childrel, NoLock);
}
- /* Queue validation for phase 3 */
- newcon = (NewConstraint *) palloc0(sizeof(NewConstraint));
- newcon->name = constrName;
- newcon->contype = CONSTR_CHECK;
- newcon->refrelid = InvalidOid;
- newcon->refindid = InvalidOid;
- newcon->conid = con->oid;
+ /*
+ * Queue validation for phase 3 only if constraint is enforced;
+ * otherwise, adding it to the validation queue won't be very
+ * effective, as the verification will be skipped.
+ */
+ if (con->conenforced)
+ {
+ newcon = (NewConstraint *) palloc0(sizeof(NewConstraint));
+ newcon->name = constrName;
+ newcon->contype = CONSTR_CHECK;
+ newcon->refrelid = InvalidOid;
+ newcon->refindid = InvalidOid;
+ newcon->conid = con->oid;
- val = SysCacheGetAttrNotNull(CONSTROID, tuple,
- Anum_pg_constraint_conbin);
- conbin = TextDatumGetCString(val);
- newcon->qual = (Node *) stringToNode(conbin);
+ val = SysCacheGetAttrNotNull(CONSTROID, tuple,
+ Anum_pg_constraint_conbin);
+ conbin = TextDatumGetCString(val);
+ newcon->qual = (Node *) stringToNode(conbin);
- /* Find or create work queue entry for this table */
- tab = ATGetQueueEntry(wqueue, rel);
- tab->constraints = lappend(tab->constraints, newcon);
+ /* Find or create work queue entry for this table */
+ tab = ATGetQueueEntry(wqueue, rel);
+ tab->constraints = lappend(tab->constraints, newcon);
+ }
/*
* Invalidate relcache so that others see the new validated
@@ -12065,7 +12075,8 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
}
/*
- * Now update the catalog, while we have the door open.
+ * Update the catalog regardless of enforcement; validation will occur
+ * only when the constraint is enforced.
*/
copyTuple = heap_copytuple(tuple);
copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
@@ -16206,6 +16217,7 @@ constraints_equivalent(HeapTuple a, HeapTuple b, TupleDesc tupleDesc)
if (acon->condeferrable != bcon->condeferrable ||
acon->condeferred != bcon->condeferred ||
+ acon->conenforced != bcon->conenforced ||
strcmp(decompile_conbin(a, tupleDesc),
decompile_conbin(b, tupleDesc)) != 0)
return false;
@@ -20129,6 +20141,7 @@ DetachAddConstraintIfNeeded(List **wqueue, Relation partRel)
n->cooked_expr = nodeToString(make_ands_explicit(constraintExpr));
n->initially_valid = true;
n->skip_validation = true;
+ n->is_enforced = true;
/* It's a re-add, since it nominally already exists */
ATAddCheckNNConstraint(wqueue, tab, partRel, n,
true, false, true, ShareUpdateExclusiveLock);
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 09356e46d16..25b0e612b27 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -810,6 +810,7 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
stmt->deferrable,
stmt->initdeferred,
true,
+ true, /* Is Enforced */
InvalidOid, /* no parent */
RelationGetRelid(rel),
NULL, /* no conkey */
diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c
index 859e2191f08..e047f5329e8 100644
--- a/src/backend/commands/typecmds.c
+++ b/src/backend/commands/typecmds.c
@@ -1011,6 +1011,13 @@ DefineDomain(CreateDomainStmt *stmt)
errmsg("specifying constraint deferrability not supported for domains")));
break;
+ case CONSTR_ATTR_ENFORCED:
+ case CONSTR_ATTR_NOT_ENFORCED:
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("specifying constraint enforceability not supported for domains")));
+ break;
+
default:
elog(ERROR, "unrecognized constraint subtype: %d",
(int) constr->contype);
@@ -2971,6 +2978,12 @@ AlterDomainAddConstraint(List *names, Node *newConstraint,
errmsg("specifying constraint deferrability not supported for domains")));
break;
+ case CONSTR_ATTR_NOT_ENFORCED:
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("specifying not enforced constraint not supported for domains")));
+ break;
+
default:
elog(ERROR, "unrecognized constraint subtype: %d",
(int) constr->contype);
@@ -3601,6 +3614,7 @@ domainAddCheckConstraint(Oid domainOid, Oid domainNamespace, Oid baseTypeOid,
false, /* Is Deferrable */
false, /* Is Deferred */
!constr->skip_validation, /* Is Validated */
+ true, /* Is Enforced */
InvalidOid, /* no parent constraint */
InvalidOid, /* not a relation constraint */
NULL,
@@ -3708,6 +3722,7 @@ domainAddNotNullConstraint(Oid domainOid, Oid domainNamespace, Oid baseTypeOid,
false, /* Is Deferrable */
false, /* Is Deferred */
!constr->skip_validation, /* Is Validated */
+ true, /* Is Enforced */
InvalidOid, /* no parent constraint */
InvalidOid, /* not a relation constraint */
NULL,
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 5ca856fd279..1878f4b8d94 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1751,11 +1751,15 @@ ExecRelCheck(ResultRelInfo *resultRelInfo,
{
oldContext = MemoryContextSwitchTo(estate->es_query_cxt);
resultRelInfo->ri_ConstraintExprs =
- (ExprState **) palloc(ncheck * sizeof(ExprState *));
+ (ExprState **) palloc0(ncheck * sizeof(ExprState *));
for (i = 0; i < ncheck; i++)
{
Expr *checkconstr;
+ /* Skip not enforced constraint */
+ if (!check[i].ccenforced)
+ continue;
+
checkconstr = stringToNode(check[i].ccbin);
resultRelInfo->ri_ConstraintExprs[i] =
ExecPrepareExpr(checkconstr, estate);
@@ -1782,7 +1786,7 @@ ExecRelCheck(ResultRelInfo *resultRelInfo,
* is not to be treated as a failure. Therefore, use ExecCheck not
* ExecQual.
*/
- if (!ExecCheck(checkconstr, econtext))
+ if (checkconstr && !ExecCheck(checkconstr, econtext))
return check[i].ccname;
}
diff --git a/src/backend/nodes/makefuncs.c b/src/backend/nodes/makefuncs.c
index 7e5df7bea4d..72e6a0e89be 100644
--- a/src/backend/nodes/makefuncs.c
+++ b/src/backend/nodes/makefuncs.c
@@ -455,6 +455,7 @@ makeNotNullConstraint(String *colname)
notnull->keys = list_make1(colname);
notnull->skip_validation = false;
notnull->initially_valid = true;
+ notnull->is_enforced = true;
return notnull;
}
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 67eb96396af..d93a92a7cd3 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -143,6 +143,8 @@ typedef struct KeyActions
#define CAS_INITIALLY_DEFERRED 0x08
#define CAS_NOT_VALID 0x10
#define CAS_NO_INHERIT 0x20
+#define CAS_NOT_ENFORCED 0x40
+#define CAS_ENFORCED 0x80
#define parser_yyerror(msg) scanner_yyerror(msg, yyscanner)
@@ -197,7 +199,7 @@ static void SplitColQualList(List *qualList,
core_yyscan_t yyscanner);
static void processCASbits(int cas_bits, int location, const char *constrType,
bool *deferrable, bool *initdeferred, bool *not_valid,
- bool *no_inherit, core_yyscan_t yyscanner);
+ bool *no_inherit, bool *is_enforced, core_yyscan_t yyscanner);
static PartitionStrategy parsePartitionStrategy(char *strategy, int location,
core_yyscan_t yyscanner);
static void preprocess_pubobj_list(List *pubobjspec_list,
@@ -711,9 +713,9 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
DETACH DICTIONARY DISABLE_P DISCARD DISTINCT DO DOCUMENT_P DOMAIN_P
DOUBLE_P DROP
- EACH ELSE EMPTY_P ENABLE_P ENCODING ENCRYPTED END_P ENUM_P ERROR_P ESCAPE
- EVENT EXCEPT EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION
- EXTENSION EXTERNAL EXTRACT
+ EACH ELSE EMPTY_P ENABLE_P ENCODING ENCRYPTED END_P ENFORCED ENUM_P ERROR_P
+ ESCAPE EVENT EXCEPT EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN
+ EXPRESSION EXTENSION EXTERNAL EXTRACT
FALSE_P FAMILY FETCH FILTER FINALIZE FIRST_P FLOAT_P FOLLOWING FOR
FORCE FOREIGN FORMAT FORWARD FREEZE FROM FULL FUNCTION FUNCTIONS
@@ -2658,7 +2660,7 @@ alter_table_cmd:
processCASbits($4, @4, "ALTER CONSTRAINT statement",
&c->deferrable,
&c->initdeferred,
- NULL, NULL, yyscanner);
+ NULL, NULL, &c->is_enforced, yyscanner);
$$ = (Node *) n;
}
/* ALTER TABLE <name> VALIDATE CONSTRAINT ... */
@@ -3963,6 +3965,7 @@ ColConstraintElem:
n->cooked_expr = NULL;
n->skip_validation = false;
n->initially_valid = true;
+ n->is_enforced = true;
$$ = (Node *) n;
}
| DEFAULT b_expr
@@ -4087,6 +4090,22 @@ ConstraintAttr:
n->location = @1;
$$ = (Node *) n;
}
+ | ENFORCED
+ {
+ Constraint *n = makeNode(Constraint);
+
+ n->contype = CONSTR_ATTR_ENFORCED;
+ n->location = @1;
+ $$ = (Node *) n;
+ }
+ | NOT ENFORCED
+ {
+ Constraint *n = makeNode(Constraint);
+
+ n->contype = CONSTR_ATTR_NOT_ENFORCED;
+ n->location = @1;
+ $$ = (Node *) n;
+ }
;
@@ -4149,7 +4168,7 @@ ConstraintElem:
n->cooked_expr = NULL;
processCASbits($5, @5, "CHECK",
NULL, NULL, &n->skip_validation,
- &n->is_no_inherit, yyscanner);
+ &n->is_no_inherit, &n->is_enforced, yyscanner);
n->initially_valid = !n->skip_validation;
$$ = (Node *) n;
}
@@ -4163,7 +4182,7 @@ ConstraintElem:
/* no NOT VALID support yet */
processCASbits($4, @4, "NOT NULL",
NULL, NULL, NULL,
- &n->is_no_inherit, yyscanner);
+ &n->is_no_inherit, NULL, yyscanner);
n->initially_valid = true;
$$ = (Node *) n;
}
@@ -4183,7 +4202,7 @@ ConstraintElem:
n->indexspace = $9;
processCASbits($10, @10, "UNIQUE",
&n->deferrable, &n->initdeferred, NULL,
- NULL, yyscanner);
+ NULL, NULL, yyscanner);
$$ = (Node *) n;
}
| UNIQUE ExistingIndex ConstraintAttributeSpec
@@ -4199,7 +4218,7 @@ ConstraintElem:
n->indexspace = NULL;
processCASbits($3, @3, "UNIQUE",
&n->deferrable, &n->initdeferred, NULL,
- NULL, yyscanner);
+ NULL, NULL, yyscanner);
$$ = (Node *) n;
}
| PRIMARY KEY '(' columnList opt_without_overlaps ')' opt_c_include opt_definition OptConsTableSpace
@@ -4217,7 +4236,7 @@ ConstraintElem:
n->indexspace = $9;
processCASbits($10, @10, "PRIMARY KEY",
&n->deferrable, &n->initdeferred, NULL,
- NULL, yyscanner);
+ NULL, NULL, yyscanner);
$$ = (Node *) n;
}
| PRIMARY KEY ExistingIndex ConstraintAttributeSpec
@@ -4233,7 +4252,7 @@ ConstraintElem:
n->indexspace = NULL;
processCASbits($4, @4, "PRIMARY KEY",
&n->deferrable, &n->initdeferred, NULL,
- NULL, yyscanner);
+ NULL, NULL, yyscanner);
$$ = (Node *) n;
}
| EXCLUDE access_method_clause '(' ExclusionConstraintList ')'
@@ -4253,7 +4272,7 @@ ConstraintElem:
n->where_clause = $9;
processCASbits($10, @10, "EXCLUDE",
&n->deferrable, &n->initdeferred, NULL,
- NULL, yyscanner);
+ NULL, NULL, yyscanner);
$$ = (Node *) n;
}
| FOREIGN KEY '(' columnList optionalPeriodName ')' REFERENCES qualified_name
@@ -4283,6 +4302,7 @@ ConstraintElem:
processCASbits($12, @12, "FOREIGN KEY",
&n->deferrable, &n->initdeferred,
&n->skip_validation, NULL,
+ NULL,
yyscanner);
n->initially_valid = !n->skip_validation;
$$ = (Node *) n;
@@ -4323,7 +4343,7 @@ DomainConstraintElem:
n->cooked_expr = NULL;
processCASbits($5, @5, "CHECK",
NULL, NULL, &n->skip_validation,
- &n->is_no_inherit, yyscanner);
+ &n->is_no_inherit, NULL, yyscanner);
n->initially_valid = !n->skip_validation;
$$ = (Node *) n;
}
@@ -4337,7 +4357,7 @@ DomainConstraintElem:
/* no NOT VALID, NO INHERIT support */
processCASbits($3, @3, "NOT NULL",
NULL, NULL, NULL,
- NULL, yyscanner);
+ NULL, NULL, yyscanner);
n->initially_valid = true;
$$ = (Node *) n;
}
@@ -6000,7 +6020,7 @@ CreateTrigStmt:
n->transitionRels = NIL;
processCASbits($11, @11, "TRIGGER",
&n->deferrable, &n->initdeferred, NULL,
- NULL, yyscanner);
+ NULL, NULL, yyscanner);
n->constrrel = $10;
$$ = (Node *) n;
}
@@ -6169,7 +6189,8 @@ ConstraintAttributeSpec:
parser_errposition(@2)));
/* generic message for other conflicts */
if ((newspec & (CAS_NOT_DEFERRABLE | CAS_DEFERRABLE)) == (CAS_NOT_DEFERRABLE | CAS_DEFERRABLE) ||
- (newspec & (CAS_INITIALLY_IMMEDIATE | CAS_INITIALLY_DEFERRED)) == (CAS_INITIALLY_IMMEDIATE | CAS_INITIALLY_DEFERRED))
+ (newspec & (CAS_INITIALLY_IMMEDIATE | CAS_INITIALLY_DEFERRED)) == (CAS_INITIALLY_IMMEDIATE | CAS_INITIALLY_DEFERRED) ||
+ (newspec & (CAS_NOT_ENFORCED | CAS_ENFORCED)) == (CAS_NOT_ENFORCED | CAS_ENFORCED))
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("conflicting constraint properties"),
@@ -6185,6 +6206,8 @@ ConstraintAttributeElem:
| INITIALLY DEFERRED { $$ = CAS_INITIALLY_DEFERRED; }
| NOT VALID { $$ = CAS_NOT_VALID; }
| NO INHERIT { $$ = CAS_NO_INHERIT; }
+ | NOT ENFORCED { $$ = CAS_NOT_ENFORCED; }
+ | ENFORCED { $$ = CAS_ENFORCED; }
;
@@ -17688,6 +17711,7 @@ unreserved_keyword:
| ENABLE_P
| ENCODING
| ENCRYPTED
+ | ENFORCED
| ENUM_P
| ERROR_P
| ESCAPE
@@ -18265,6 +18289,7 @@ bare_label_keyword:
| ENCODING
| ENCRYPTED
| END_P
+ | ENFORCED
| ENUM_P
| ERROR_P
| ESCAPE
@@ -19404,7 +19429,7 @@ SplitColQualList(List *qualList,
static void
processCASbits(int cas_bits, int location, const char *constrType,
bool *deferrable, bool *initdeferred, bool *not_valid,
- bool *no_inherit, core_yyscan_t yyscanner)
+ bool *no_inherit, bool *is_enforced, core_yyscan_t yyscanner)
{
/* defaults */
if (deferrable)
@@ -19413,6 +19438,8 @@ processCASbits(int cas_bits, int location, const char *constrType,
*initdeferred = false;
if (not_valid)
*not_valid = false;
+ if (is_enforced)
+ *is_enforced = true;
if (cas_bits & (CAS_DEFERRABLE | CAS_INITIALLY_DEFERRED))
{
@@ -19465,6 +19492,32 @@ processCASbits(int cas_bits, int location, const char *constrType,
constrType),
parser_errposition(location)));
}
+
+ if (cas_bits & CAS_NOT_ENFORCED)
+ {
+ if (is_enforced)
+ *is_enforced = false;
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ /* translator: %s is CHECK, UNIQUE, or similar */
+ errmsg("%s constraints cannot be marked NOT ENFORCED",
+ constrType),
+ parser_errposition(location)));
+ }
+
+ if (cas_bits & CAS_ENFORCED)
+ {
+ if (is_enforced)
+ *is_enforced = true;
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ /* translator: %s is CHECK, UNIQUE, or similar */
+ errmsg("%s constraints cannot be marked ENFORCED",
+ constrType),
+ parser_errposition(location)));
+ }
}
/*
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 0f324ee4e31..183f845ba55 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -954,6 +954,8 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column)
case CONSTR_ATTR_NOT_DEFERRABLE:
case CONSTR_ATTR_DEFERRED:
case CONSTR_ATTR_IMMEDIATE:
+ case CONSTR_ATTR_ENFORCED:
+ case CONSTR_ATTR_NOT_ENFORCED:
/* transformConstraintAttrs took care of these */
break;
@@ -1093,6 +1095,8 @@ transformTableConstraint(CreateStmtContext *cxt, Constraint *constraint)
case CONSTR_ATTR_NOT_DEFERRABLE:
case CONSTR_ATTR_DEFERRED:
case CONSTR_ATTR_IMMEDIATE:
+ case CONSTR_ATTR_ENFORCED:
+ case CONSTR_ATTR_NOT_ENFORCED:
elog(ERROR, "invalid context for constraint type %d",
constraint->contype);
break;
@@ -1433,6 +1437,7 @@ expandTableLikeClause(RangeVar *heapRel, TableLikeClause *table_like_clause)
{
char *ccname = constr->check[ccnum].ccname;
char *ccbin = constr->check[ccnum].ccbin;
+ bool ccenforced = constr->check[ccnum].ccenforced;
bool ccnoinherit = constr->check[ccnum].ccnoinherit;
Node *ccbin_node;
bool found_whole_row;
@@ -1462,6 +1467,7 @@ expandTableLikeClause(RangeVar *heapRel, TableLikeClause *table_like_clause)
n->contype = CONSTR_CHECK;
n->conname = pstrdup(ccname);
n->location = -1;
+ n->is_enforced = ccenforced;
n->is_no_inherit = ccnoinherit;
n->raw_expr = NULL;
n->cooked_expr = nodeToString(ccbin_node);
@@ -3859,6 +3865,7 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
Constraint *lastprimarycon = NULL;
bool saw_deferrability = false;
bool saw_initially = false;
+ bool saw_enforced = false;
ListCell *clist;
#define SUPPORTS_ATTRS(node) \
@@ -3954,12 +3961,45 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
lastprimarycon->initdeferred = false;
break;
+ case CONSTR_ATTR_ENFORCED:
+ if (lastprimarycon &&
+ lastprimarycon->contype != CONSTR_CHECK)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("misplaced ENFORCED clause"),
+ parser_errposition(cxt->pstate, con->location)));
+ if (saw_enforced)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("multiple ENFORCED/NOT ENFORCED clauses not allowed"),
+ parser_errposition(cxt->pstate, con->location)));
+ saw_enforced = true;
+ lastprimarycon->is_enforced = true;
+ break;
+
+ case CONSTR_ATTR_NOT_ENFORCED:
+ if (lastprimarycon &&
+ lastprimarycon->contype != CONSTR_CHECK)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("misplaced NOT ENFORCED clause"),
+ parser_errposition(cxt->pstate, con->location)));
+ if (saw_enforced)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("multiple ENFORCED/NOT ENFORCED clauses not allowed"),
+ parser_errposition(cxt->pstate, con->location)));
+ saw_enforced = true;
+ lastprimarycon->is_enforced = false;
+ break;
+
default:
/* Otherwise it's not an attribute */
lastprimarycon = con;
/* reset flags for new primary node */
saw_deferrability = false;
saw_initially = false;
+ saw_enforced = false;
break;
}
}
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index a39068d1bf1..cecc48634e6 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -2591,6 +2591,8 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
appendStringInfoString(&buf, " DEFERRABLE");
if (conForm->condeferred)
appendStringInfoString(&buf, " INITIALLY DEFERRED");
+ if (!conForm->conenforced)
+ appendStringInfoString(&buf, " NOT ENFORCED");
if (!conForm->convalidated)
appendStringInfoString(&buf, " NOT VALID");
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 342467fd186..5d0233d56b2 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4587,6 +4587,7 @@ CheckConstraintFetch(Relation relation)
}
check[found].ccvalid = conform->convalidated;
+ check[found].ccenforced = conform->conenforced;
check[found].ccnoinherit = conform->connoinherit;
check[found].ccname = MemoryContextStrdup(CacheMemoryContext,
NameStr(conform->conname));
diff --git a/src/include/access/tupdesc.h b/src/include/access/tupdesc.h
index 8930a28d660..96bac018083 100644
--- a/src/include/access/tupdesc.h
+++ b/src/include/access/tupdesc.h
@@ -30,6 +30,7 @@ typedef struct ConstrCheck
char *ccname;
char *ccbin; /* nodeToString representation of expr */
bool ccvalid;
+ bool ccenforced;
bool ccnoinherit; /* this is a non-inheritable constraint */
} ConstrCheck;
diff --git a/src/include/catalog/heap.h b/src/include/catalog/heap.h
index 8c278f202b4..3eaf4e96cef 100644
--- a/src/include/catalog/heap.h
+++ b/src/include/catalog/heap.h
@@ -41,6 +41,7 @@ typedef struct CookedConstraint
AttrNumber attnum; /* which attr (only for NOTNULL, DEFAULT) */
Node *expr; /* transformed default or check expr */
bool skip_validation; /* skip validation? (only for CHECK) */
+ bool is_enforced; /* is enforced? (only for CHECK) */
bool is_local; /* constraint has local (non-inherited) def */
int16 inhcount; /* number of times constraint is inherited */
bool is_no_inherit; /* constraint has local def and cannot be
diff --git a/src/include/catalog/pg_constraint.h b/src/include/catalog/pg_constraint.h
index 4b4476738a2..9a74ad72f23 100644
--- a/src/include/catalog/pg_constraint.h
+++ b/src/include/catalog/pg_constraint.h
@@ -52,6 +52,7 @@ CATALOG(pg_constraint,2606,ConstraintRelationId)
bool condeferrable; /* deferrable constraint? */
bool condeferred; /* deferred by default? */
bool convalidated; /* constraint has been validated? */
+ bool conenforced; /* enforced constraint? */
/*
* conrelid and conkey are only meaningful if the constraint applies to a
@@ -223,6 +224,7 @@ extern Oid CreateConstraintEntry(const char *constraintName,
bool isDeferrable,
bool isDeferred,
bool isValidated,
+ bool isEnforced,
Oid parentConstrId,
Oid relId,
const int16 *constraintKey,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 0f9462493e3..cad403fbfb5 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2736,6 +2736,8 @@ typedef enum ConstrType /* types of constraints */
CONSTR_ATTR_NOT_DEFERRABLE,
CONSTR_ATTR_DEFERRED,
CONSTR_ATTR_IMMEDIATE,
+ CONSTR_ATTR_ENFORCED,
+ CONSTR_ATTR_NOT_ENFORCED,
} ConstrType;
/* Foreign key action codes */
@@ -2759,6 +2761,7 @@ typedef struct Constraint
bool initdeferred; /* INITIALLY DEFERRED? */
bool skip_validation; /* skip validation of existing rows? */
bool initially_valid; /* mark the new constraint as valid? */
+ bool is_enforced; /* enforced constraint? */
bool is_no_inherit; /* is constraint non-inheritable? */
Node *raw_expr; /* CHECK or DEFAULT expression, as
* untransformed parse tree */
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 899d64ad55f..f2bc378394e 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -153,6 +153,7 @@ PG_KEYWORD("enable", ENABLE_P, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("encoding", ENCODING, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("encrypted", ENCRYPTED, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("end", END_P, RESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("enforced", ENFORCED, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("enum", ENUM_P, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("error", ERROR_P, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("escape", ESCAPE, UNRESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/test/regress/expected/constraints.out b/src/test/regress/expected/constraints.out
index 71200c90ed3..1fe2a76fa34 100644
--- a/src/test/regress/expected/constraints.out
+++ b/src/test/regress/expected/constraints.out
@@ -87,6 +87,25 @@ SELECT * FROM CHECK_TBL;
6
(3 rows)
+CREATE TABLE NE_CHECK_TBL (x int,
+ CONSTRAINT CHECK_CON CHECK (x > 3) NOT ENFORCED);
+INSERT INTO NE_CHECK_TBL VALUES (5);
+INSERT INTO NE_CHECK_TBL VALUES (4);
+INSERT INTO NE_CHECK_TBL VALUES (3);
+INSERT INTO NE_CHECK_TBL VALUES (2);
+INSERT INTO NE_CHECK_TBL VALUES (6);
+INSERT INTO NE_CHECK_TBL VALUES (1);
+SELECT * FROM NE_CHECK_TBL;
+ x
+---
+ 5
+ 4
+ 3
+ 2
+ 6
+ 1
+(6 rows)
+
CREATE SEQUENCE CHECK_SEQ;
CREATE TABLE CHECK2_TBL (x int, y text, z int,
CONSTRAINT SEQUENCE_CON
@@ -120,7 +139,8 @@ CREATE TABLE INSERT_TBL (x INT DEFAULT nextval('insert_seq'),
y TEXT DEFAULT '-NULL-',
z INT DEFAULT -1 * currval('insert_seq'),
CONSTRAINT INSERT_TBL_CON CHECK (x >= 3 AND y <> 'check failed' AND x < 8),
- CHECK (x + z = 0));
+ CHECK (x + z = 0) ENFORCED, /* no change it is a default */
+ CONSTRAINT NE_INSERT_TBL_CON CHECK (x + z = 1) NOT ENFORCED);
INSERT INTO INSERT_TBL(x,z) VALUES (2, -2);
ERROR: new row for relation "insert_tbl" violates check constraint "insert_tbl_con"
DETAIL: Failing row contains (2, -NULL-, -2).
@@ -715,6 +735,20 @@ SELECT * FROM unique_tbl;
3 | threex
(5 rows)
+-- enforcibility cannot be specified or set for unique constrain
+CREATE TABLE UNIQUE_EN_TBL(i int UNIQUE ENFORCED);
+ERROR: misplaced ENFORCED clause
+LINE 1: CREATE TABLE UNIQUE_EN_TBL(i int UNIQUE ENFORCED);
+ ^
+CREATE TABLE UNIQUE_NOTEN_TBL(i int UNIQUE NOT ENFORCED);
+ERROR: misplaced NOT ENFORCED clause
+LINE 1: CREATE TABLE UNIQUE_NOTEN_TBL(i int UNIQUE NOT ENFORCED);
+ ^
+-- XXX: error message is misleading here
+ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
+ERROR: constraint "unique_tbl_i_key" of relation "unique_tbl" is not a foreign key constraint
+ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORCED;
+ERROR: constraint "unique_tbl_i_key" of relation "unique_tbl" is not a foreign key constraint
DROP TABLE unique_tbl;
--
-- EXCLUDE constraints
@@ -1351,6 +1385,19 @@ ERROR: must be owner of relation constraint_comments_tbl
COMMENT ON CONSTRAINT the_constraint ON DOMAIN constraint_comments_dom IS 'no, another comment';
ERROR: must be owner of type constraint_comments_dom
RESET SESSION AUTHORIZATION;
+-- enforceability not allowed
+CREATE DOMAIN constraint_enforced_dom AS int CONSTRAINT the_constraint CHECK (value > 0) ENFORCED;
+ERROR: specifying constraint enforceability not supported for domains
+CREATE DOMAIN constraint_not_enforced_dom AS int CONSTRAINT the_constraint CHECK (value > 0) NOT ENFORCED;
+ERROR: specifying constraint enforceability not supported for domains
+ALTER DOMAIN constraint_comments_dom ADD CONSTRAINT the_constraint CHECK (value > 0) ENFORCED;
+ERROR: CHECK constraints cannot be marked ENFORCED
+LINE 1: ...om ADD CONSTRAINT the_constraint CHECK (value > 0) ENFORCED;
+ ^
+ALTER DOMAIN constraint_comments_dom ADD CONSTRAINT the_constraint CHECK (value > 0) NOT ENFORCED;
+ERROR: CHECK constraints cannot be marked NOT ENFORCED
+LINE 1: ...m ADD CONSTRAINT the_constraint CHECK (value > 0) NOT ENFORC...
+ ^
DROP TABLE constraint_comments_tbl;
DROP DOMAIN constraint_comments_dom;
DROP ROLE regress_constraint_comments;
diff --git a/src/test/regress/expected/create_table_like.out b/src/test/regress/expected/create_table_like.out
index d091da5a1ef..e0613891351 100644
--- a/src/test/regress/expected/create_table_like.out
+++ b/src/test/regress/expected/create_table_like.out
@@ -315,7 +315,8 @@ Referenced by:
DROP TABLE inhz;
-- including storage and comments
-CREATE TABLE ctlt1 (a text CHECK (length(a) > 2) PRIMARY KEY, b text);
+CREATE TABLE ctlt1 (a text CHECK (length(a) > 2) ENFORCED PRIMARY KEY,
+ b text CHECK (length(b) > 100) NOT ENFORCED);
CREATE INDEX ctlt1_b_key ON ctlt1 (b);
CREATE INDEX ctlt1_fnidx ON ctlt1 ((a || b));
CREATE STATISTICS ctlt1_a_b_stat ON a,b FROM ctlt1;
@@ -366,6 +367,7 @@ CREATE TABLE ctlt1_inh (LIKE ctlt1 INCLUDING CONSTRAINTS INCLUDING COMMENTS) INH
NOTICE: merging column "a" with inherited definition
NOTICE: merging column "b" with inherited definition
NOTICE: merging constraint "ctlt1_a_check" with inherited definition
+NOTICE: merging constraint "ctlt1_b_check" with inherited definition
\d+ ctlt1_inh
Table "public.ctlt1_inh"
Column | Type | Collation | Nullable | Default | Storage | Stats target | Description
@@ -374,6 +376,7 @@ NOTICE: merging constraint "ctlt1_a_check" with inherited definition
b | text | | | | extended | | B
Check constraints:
"ctlt1_a_check" CHECK (length(a) > 2)
+ "ctlt1_b_check" CHECK (length(b) > 100) NOT ENFORCED
Not-null constraints:
"ctlt1_a_not_null" NOT NULL "a" (local, inherited)
Inherits: ctlt1
@@ -395,6 +398,7 @@ NOTICE: merging multiple inherited definitions of column "a"
c | text | | | | external | |
Check constraints:
"ctlt1_a_check" CHECK (length(a) > 2)
+ "ctlt1_b_check" CHECK (length(b) > 100) NOT ENFORCED
"ctlt3_a_check" CHECK (length(a) < 5)
"ctlt3_c_check" CHECK (length(c) < 7)
Not-null constraints:
@@ -415,6 +419,7 @@ Indexes:
"ctlt13_like_expr_idx" btree ((a || c))
Check constraints:
"ctlt1_a_check" CHECK (length(a) > 2)
+ "ctlt1_b_check" CHECK (length(b) > 100) NOT ENFORCED
"ctlt3_a_check" CHECK (length(a) < 5)
"ctlt3_c_check" CHECK (length(c) < 7)
Not-null constraints:
@@ -440,6 +445,7 @@ Indexes:
"ctlt_all_expr_idx" btree ((a || b))
Check constraints:
"ctlt1_a_check" CHECK (length(a) > 2)
+ "ctlt1_b_check" CHECK (length(b) > 100) NOT ENFORCED
Statistics objects:
"public.ctlt_all_a_b_stat" ON a, b FROM ctlt_all
"public.ctlt_all_expr_stat" ON (a || b) FROM ctlt_all
@@ -482,6 +488,7 @@ Indexes:
"pg_attrdef_expr_idx" btree ((a || b))
Check constraints:
"ctlt1_a_check" CHECK (length(a) > 2)
+ "ctlt1_b_check" CHECK (length(b) > 100) NOT ENFORCED
Statistics objects:
"public.pg_attrdef_a_b_stat" ON a, b FROM public.pg_attrdef
"public.pg_attrdef_expr_stat" ON (a || b) FROM public.pg_attrdef
@@ -506,6 +513,7 @@ Indexes:
"ctlt1_expr_idx" btree ((a || b))
Check constraints:
"ctlt1_a_check" CHECK (length(a) > 2)
+ "ctlt1_b_check" CHECK (length(b) > 100) NOT ENFORCED
Statistics objects:
"ctl_schema.ctlt1_a_b_stat" ON a, b FROM ctlt1
"ctl_schema.ctlt1_expr_stat" ON (a || b) FROM ctlt1
diff --git a/src/test/regress/sql/constraints.sql b/src/test/regress/sql/constraints.sql
index e607eb1fddb..32c8b433bf0 100644
--- a/src/test/regress/sql/constraints.sql
+++ b/src/test/regress/sql/constraints.sql
@@ -67,6 +67,18 @@ INSERT INTO CHECK_TBL VALUES (1);
SELECT * FROM CHECK_TBL;
+CREATE TABLE NE_CHECK_TBL (x int,
+ CONSTRAINT CHECK_CON CHECK (x > 3) NOT ENFORCED);
+
+INSERT INTO NE_CHECK_TBL VALUES (5);
+INSERT INTO NE_CHECK_TBL VALUES (4);
+INSERT INTO NE_CHECK_TBL VALUES (3);
+INSERT INTO NE_CHECK_TBL VALUES (2);
+INSERT INTO NE_CHECK_TBL VALUES (6);
+INSERT INTO NE_CHECK_TBL VALUES (1);
+
+SELECT * FROM NE_CHECK_TBL;
+
CREATE SEQUENCE CHECK_SEQ;
CREATE TABLE CHECK2_TBL (x int, y text, z int,
@@ -92,7 +104,8 @@ CREATE TABLE INSERT_TBL (x INT DEFAULT nextval('insert_seq'),
y TEXT DEFAULT '-NULL-',
z INT DEFAULT -1 * currval('insert_seq'),
CONSTRAINT INSERT_TBL_CON CHECK (x >= 3 AND y <> 'check failed' AND x < 8),
- CHECK (x + z = 0));
+ CHECK (x + z = 0) ENFORCED, /* no change it is a default */
+ CONSTRAINT NE_INSERT_TBL_CON CHECK (x + z = 1) NOT ENFORCED);
INSERT INTO INSERT_TBL(x,z) VALUES (2, -2);
@@ -518,6 +531,13 @@ COMMIT;
SELECT * FROM unique_tbl;
+-- enforcibility cannot be specified or set for unique constrain
+CREATE TABLE UNIQUE_EN_TBL(i int UNIQUE ENFORCED);
+CREATE TABLE UNIQUE_NOTEN_TBL(i int UNIQUE NOT ENFORCED);
+-- XXX: error message is misleading here
+ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
+ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORCED;
+
DROP TABLE unique_tbl;
--
@@ -812,6 +832,12 @@ COMMENT ON CONSTRAINT the_constraint ON constraint_comments_tbl IS 'no, the comm
COMMENT ON CONSTRAINT the_constraint ON DOMAIN constraint_comments_dom IS 'no, another comment';
RESET SESSION AUTHORIZATION;
+-- enforceability not allowed
+CREATE DOMAIN constraint_enforced_dom AS int CONSTRAINT the_constraint CHECK (value > 0) ENFORCED;
+CREATE DOMAIN constraint_not_enforced_dom AS int CONSTRAINT the_constraint CHECK (value > 0) NOT ENFORCED;
+ALTER DOMAIN constraint_comments_dom ADD CONSTRAINT the_constraint CHECK (value > 0) ENFORCED;
+ALTER DOMAIN constraint_comments_dom ADD CONSTRAINT the_constraint CHECK (value > 0) NOT ENFORCED;
+
DROP TABLE constraint_comments_tbl;
DROP DOMAIN constraint_comments_dom;
diff --git a/src/test/regress/sql/create_table_like.sql b/src/test/regress/sql/create_table_like.sql
index dea8942c71f..a41f8b83d77 100644
--- a/src/test/regress/sql/create_table_like.sql
+++ b/src/test/regress/sql/create_table_like.sql
@@ -128,7 +128,8 @@ CREATE TABLE inhz (x text REFERENCES inhz, LIKE inhx INCLUDING INDEXES);
DROP TABLE inhz;
-- including storage and comments
-CREATE TABLE ctlt1 (a text CHECK (length(a) > 2) PRIMARY KEY, b text);
+CREATE TABLE ctlt1 (a text CHECK (length(a) > 2) ENFORCED PRIMARY KEY,
+ b text CHECK (length(b) > 100) NOT ENFORCED);
CREATE INDEX ctlt1_b_key ON ctlt1 (b);
CREATE INDEX ctlt1_fnidx ON ctlt1 ((a || b));
CREATE STATISTICS ctlt1_a_b_stat ON a,b FROM ctlt1;
--
2.43.5
v3-0002-refactor-split-ATExecAlterConstrRecurse.patchapplication/x-patch; name=v3-0002-refactor-split-ATExecAlterConstrRecurse.patchDownload
From 3a63694c09cc9810c1e7c54a39c929bbfb5f9a31 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 23 Sep 2024 12:31:07 +0530
Subject: [PATCH v3 2/5] refactor: split ATExecAlterConstrRecurse()
When changing a NOT ENFORCED foreign key constraint to ENFORCED, we
need to create the necessary triggers for enforcing the constraint. We
can set the deferrability at the time of creation and skip the code
used for other constraint alteration operations.
Additionally, move the code that iterates over each child constraint
of the constraint being altered into a separate function.
---
src/backend/commands/tablecmds.c | 194 +++++++++++++++++++------------
1 file changed, 118 insertions(+), 76 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 524c1e5f40b..3d77bb660fb 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -394,6 +394,12 @@ static ObjectAddress ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd,
static bool ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
Relation rel, HeapTuple contuple, List **otherrelids,
LOCKMODE lockmode);
+static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
+ bool deferrable, bool initdeferred,
+ List **otherrelids);
+static void ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
+ Relation rel, HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode);
static ObjectAddress ATExecValidateConstraint(List **wqueue,
Relation rel, char *constrName,
bool recurse, bool recursing, LOCKMODE lockmode);
@@ -11790,9 +11796,6 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
{
HeapTuple copyTuple;
Form_pg_constraint copy_con;
- HeapTuple tgtuple;
- ScanKeyData tgkey;
- SysScanDesc tgscan;
copyTuple = heap_copytuple(contuple);
copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
@@ -11813,53 +11816,8 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
* Now we need to update the multiple entries in pg_trigger that
* implement the constraint.
*/
- ScanKeyInit(&tgkey,
- Anum_pg_trigger_tgconstraint,
- BTEqualStrategyNumber, F_OIDEQ,
- ObjectIdGetDatum(conoid));
- tgscan = systable_beginscan(tgrel, TriggerConstraintIndexId, true,
- NULL, 1, &tgkey);
- while (HeapTupleIsValid(tgtuple = systable_getnext(tgscan)))
- {
- Form_pg_trigger tgform = (Form_pg_trigger) GETSTRUCT(tgtuple);
- Form_pg_trigger copy_tg;
- HeapTuple tgCopyTuple;
-
- /*
- * Remember OIDs of other relation(s) involved in FK constraint.
- * (Note: it's likely that we could skip forcing a relcache inval
- * for other rels that don't have a trigger whose properties
- * change, but let's be conservative.)
- */
- if (tgform->tgrelid != RelationGetRelid(rel))
- *otherrelids = list_append_unique_oid(*otherrelids,
- tgform->tgrelid);
-
- /*
- * Update deferrability of RI_FKey_noaction_del,
- * RI_FKey_noaction_upd, RI_FKey_check_ins and RI_FKey_check_upd
- * triggers, but not others; see createForeignKeyActionTriggers
- * and CreateFKCheckTrigger.
- */
- if (tgform->tgfoid != F_RI_FKEY_NOACTION_DEL &&
- tgform->tgfoid != F_RI_FKEY_NOACTION_UPD &&
- tgform->tgfoid != F_RI_FKEY_CHECK_INS &&
- tgform->tgfoid != F_RI_FKEY_CHECK_UPD)
- continue;
-
- tgCopyTuple = heap_copytuple(tgtuple);
- copy_tg = (Form_pg_trigger) GETSTRUCT(tgCopyTuple);
-
- copy_tg->tgdeferrable = cmdcon->deferrable;
- copy_tg->tginitdeferred = cmdcon->initdeferred;
- CatalogTupleUpdate(tgrel, &tgCopyTuple->t_self, tgCopyTuple);
-
- InvokeObjectPostAlterHook(TriggerRelationId, tgform->oid, 0);
-
- heap_freetuple(tgCopyTuple);
- }
-
- systable_endscan(tgscan);
+ AlterConstrTriggerDeferrability(conoid, tgrel, rel, cmdcon->deferrable,
+ cmdcon->initdeferred, otherrelids);
}
/*
@@ -11872,36 +11830,120 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
*/
if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)
- {
- ScanKeyData pkey;
- SysScanDesc pscan;
- HeapTuple childtup;
-
- ScanKeyInit(&pkey,
- Anum_pg_constraint_conparentid,
- BTEqualStrategyNumber, F_OIDEQ,
- ObjectIdGetDatum(conoid));
-
- pscan = systable_beginscan(conrel, ConstraintParentIndexId,
- true, NULL, 1, &pkey);
-
- while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
- {
- Form_pg_constraint childcon = (Form_pg_constraint) GETSTRUCT(childtup);
- Relation childrel;
-
- childrel = table_open(childcon->conrelid, lockmode);
- ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, childrel, childtup,
- otherrelids, lockmode);
- table_close(childrel, NoLock);
- }
-
- systable_endscan(pscan);
- }
+ ATExecAlterChildConstr(cmdcon, conrel, tgrel, rel, contuple,
+ otherrelids, lockmode);
return changed;
}
+/*
+ * A subroutine of ATExecAlterConstrRecurse that updated constraint trigger's
+ * deferrability.
+ *
+ * The arguments to this function have the same meaning as the arguments to
+ * ATExecAlterConstrRecurse.
+ */
+static void
+AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
+ bool deferrable, bool initdeferred,
+ List **otherrelids)
+{
+ HeapTuple tgtuple;
+ ScanKeyData tgkey;
+ SysScanDesc tgscan;
+
+ ScanKeyInit(&tgkey,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+ tgscan = systable_beginscan(tgrel, TriggerConstraintIndexId, true,
+ NULL, 1, &tgkey);
+ while (HeapTupleIsValid(tgtuple = systable_getnext(tgscan)))
+ {
+ Form_pg_trigger tgform = (Form_pg_trigger) GETSTRUCT(tgtuple);
+ Form_pg_trigger copy_tg;
+ HeapTuple tgCopyTuple;
+
+ /*
+ * Remember OIDs of other relation(s) involved in FK constraint.
+ * (Note: it's likely that we could skip forcing a relcache inval for
+ * other rels that don't have a trigger whose properties change, but
+ * let's be conservative.)
+ */
+ if (tgform->tgrelid != RelationGetRelid(rel))
+ *otherrelids = list_append_unique_oid(*otherrelids,
+ tgform->tgrelid);
+
+ /*
+ * Update enable status and deferrability of RI_FKey_noaction_del,
+ * RI_FKey_noaction_upd, RI_FKey_check_ins and RI_FKey_check_upd
+ * triggers, but not others; see createForeignKeyActionTriggers and
+ * CreateFKCheckTrigger.
+ */
+ if (tgform->tgfoid != F_RI_FKEY_NOACTION_DEL &&
+ tgform->tgfoid != F_RI_FKEY_NOACTION_UPD &&
+ tgform->tgfoid != F_RI_FKEY_CHECK_INS &&
+ tgform->tgfoid != F_RI_FKEY_CHECK_UPD)
+ continue;
+
+ tgCopyTuple = heap_copytuple(tgtuple);
+ copy_tg = (Form_pg_trigger) GETSTRUCT(tgCopyTuple);
+
+ copy_tg->tgdeferrable = deferrable;
+ copy_tg->tginitdeferred = initdeferred;
+ CatalogTupleUpdate(tgrel, &tgCopyTuple->t_self, tgCopyTuple);
+
+ InvokeObjectPostAlterHook(TriggerRelationId, tgform->oid, 0);
+
+ heap_freetuple(tgCopyTuple);
+ }
+
+ systable_endscan(tgscan);
+}
+
+/*
+ * Invokes ATExecAlterConstrRecurse for each constraint that is a child of the
+ * specified constraint.
+ *
+ * The arguments to this function have the same meaning as the arguments to
+ * ATExecAlterConstrRecurse.
+ */
+static void
+ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
+ Relation rel, HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode)
+{
+ Form_pg_constraint currcon;
+ Oid conoid;
+ ScanKeyData pkey;
+ SysScanDesc pscan;
+ HeapTuple childtup;
+
+ currcon = (Form_pg_constraint) GETSTRUCT(contuple);
+ conoid = currcon->oid;
+
+ ScanKeyInit(&pkey,
+ Anum_pg_constraint_conparentid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+
+ pscan = systable_beginscan(conrel, ConstraintParentIndexId,
+ true, NULL, 1, &pkey);
+
+ while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
+ {
+ Form_pg_constraint childcon = (Form_pg_constraint) GETSTRUCT(childtup);
+ Relation childrel;
+
+ childrel = table_open(childcon->conrelid, lockmode);
+ ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, childrel, childtup,
+ otherrelids, lockmode);
+ table_close(childrel, NoLock);
+ }
+
+ systable_endscan(pscan);
+}
+
/*
* ALTER TABLE VALIDATE CONSTRAINT
*
--
2.43.5
v3-0003-refactor-Change-ATExecAlterConstrRecurse-argument.patchapplication/x-patch; name=v3-0003-refactor-Change-ATExecAlterConstrRecurse-argument.patchDownload
From 60c1d9ed73ccd19128f03afc340802aec2a0db6d Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 23 Sep 2024 12:37:05 +0530
Subject: [PATCH v3 3/5] refactor: Change ATExecAlterConstrRecurse() argument.
Instead of passing the Relation of the referencing relation, we will
pass a relid. Opening the relation using the relid again should not
cause significant issues. However, this approach makes recursion more
efficient, especially in the later patch where we will be recreating
the referencing and referred triggers. In addition to that passing
relid of the referred relation too and Oids of triggers.
----
NOTE: This patch is not meant to be committed separately. It should
be squashed with the main patch that adds ENFORCED/NOT ENFORCED.
----
---
src/backend/commands/tablecmds.c | 66 ++++++++++++++++++++++----------
1 file changed, 45 insertions(+), 21 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 3d77bb660fb..2dce6bbcc33 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -392,14 +392,22 @@ static void AlterSeqNamespaces(Relation classRel, Relation rel,
static ObjectAddress ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd,
bool recurse, bool recursing, LOCKMODE lockmode);
static bool ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode);
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger);
static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
bool deferrable, bool initdeferred,
List **otherrelids);
static void ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode);
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger);
static ObjectAddress ATExecValidateConstraint(List **wqueue,
Relation rel, char *constrName,
bool recurse, bool recursing, LOCKMODE lockmode);
@@ -11736,8 +11744,10 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
currcon->condeferred != cmdcon->initdeferred ||
rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
- if (ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, rel, contuple,
- &otherrelids, lockmode))
+ if (ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, currcon->conrelid,
+ currcon->confrelid, contuple, &otherrelids,
+ lockmode, InvalidOid, InvalidOid,
+ InvalidOid, InvalidOid))
ObjectAddressSet(address, ConstraintRelationId, currcon->oid);
}
@@ -11770,13 +11780,18 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
*/
static bool
ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode)
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger)
{
Form_pg_constraint currcon;
Oid conoid;
Oid refrelid;
bool changed = false;
+ Relation rel;
/* since this function recurses, it could be driven to stack overflow */
check_stack_depth();
@@ -11785,6 +11800,8 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
conoid = currcon->oid;
refrelid = currcon->confrelid;
+ rel = table_open(currcon->conrelid, lockmode);
+
/*
* Update pg_constraint with the flags from cmdcon.
*
@@ -11830,8 +11847,14 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
*/
if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)
- ATExecAlterChildConstr(cmdcon, conrel, tgrel, rel, contuple,
- otherrelids, lockmode);
+ ATExecAlterChildConstr(cmdcon, conrel, tgrel, fkrelid, pkrelid,
+ contuple, otherrelids, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
+
+ table_close(rel, NoLock);
return changed;
}
@@ -11910,8 +11933,12 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
*/
static void
ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode)
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger)
{
Form_pg_constraint currcon;
Oid conoid;
@@ -11931,15 +11958,12 @@ ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
true, NULL, 1, &pkey);
while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
- {
- Form_pg_constraint childcon = (Form_pg_constraint) GETSTRUCT(childtup);
- Relation childrel;
-
- childrel = table_open(childcon->conrelid, lockmode);
- ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, childrel, childtup,
- otherrelids, lockmode);
- table_close(childrel, NoLock);
- }
+ ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, fkrelid, pkrelid,
+ childtup, otherrelids, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
systable_endscan(pscan);
}
--
2.43.5
v3-0004-Remove-hastriggers-flag-check-before-fetching-FK-.patchapplication/x-patch; name=v3-0004-Remove-hastriggers-flag-check-before-fetching-FK-.patchDownload
From 5f76d31aaab282b022aa61c4088e0fd839653c1e Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 7 Oct 2024 12:40:07 +0530
Subject: [PATCH v3 4/5] Remove hastriggers flag check before fetching FK
constraints.
With NOT ENFORCED, foreign key constraints will be created without
triggers. Therefore, the criteria for fetching foreign keys based on
the presence of triggers no longer apply.
----
NOTE: This patch is intended to reduce the diff noise from the main
patch and is not meant to be committed separately. It should be
squashed with the main patch that adds ENFORCED/NOT ENFORCED.
----
---
src/backend/utils/cache/relcache.c | 5 -
src/bin/pg_dump/pg_dump.c | 8 +-
src/bin/psql/describe.c | 223 ++++++++++++++---------------
3 files changed, 107 insertions(+), 129 deletions(-)
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 5d0233d56b2..b25dfb09808 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4670,11 +4670,6 @@ RelationGetFKeyList(Relation relation)
if (relation->rd_fkeyvalid)
return relation->rd_fkeylist;
- /* Fast path: non-partitioned tables without triggers can't have FKs */
- if (!relation->rd_rel->relhastriggers &&
- relation->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
- return NIL;
-
/*
* We build the list we intend to return (in the caller's context) while
* doing the scan. After successfully completing the scan, we copy that
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index a8c141b689d..98080ce44e2 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -7836,13 +7836,7 @@ getConstraints(Archive *fout, TableInfo tblinfo[], int numTables)
{
TableInfo *tinfo = &tblinfo[i];
- /*
- * For partitioned tables, foreign keys have no triggers so they must
- * be included anyway in case some foreign keys are defined.
- */
- if ((!tinfo->hastriggers &&
- tinfo->relkind != RELKIND_PARTITIONED_TABLE) ||
- !(tinfo->dobj.dump & DUMP_COMPONENT_DEFINITION))
+ if (!(tinfo->dobj.dump & DUMP_COMPONENT_DEFINITION))
continue;
/* OK, we need info for this table */
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 5bfebad64d5..410b145548e 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2498,135 +2498,124 @@ describeOneTableDetails(const char *schemaname,
}
/*
- * Print foreign-key constraints (there are none if no triggers,
- * except if the table is partitioned, in which case the triggers
- * appear in the partitions)
+ * Print foreign-key constraints
*/
- if (tableinfo.hastriggers ||
- tableinfo.relkind == RELKIND_PARTITIONED_TABLE)
+ if (pset.sversion >= 120000 &&
+ (tableinfo.ispartition || tableinfo.relkind == RELKIND_PARTITIONED_TABLE))
{
- if (pset.sversion >= 120000 &&
- (tableinfo.ispartition || tableinfo.relkind == RELKIND_PARTITIONED_TABLE))
+ /*
+ * Put the constraints defined in this table first, followed
+ * by the constraints defined in ancestor partitioned tables.
+ */
+ printfPQExpBuffer(&buf,
+ "SELECT conrelid = '%s'::pg_catalog.regclass AS sametable,\n"
+ " conname,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef,\n"
+ " conrelid::pg_catalog.regclass AS ontable\n"
+ " FROM pg_catalog.pg_constraint,\n"
+ " pg_catalog.pg_partition_ancestors('%s')\n"
+ " WHERE conrelid = relid AND contype = 'f' AND conparentid = 0\n"
+ "ORDER BY sametable DESC, conname;",
+ oid, oid);
+ }
+ else
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT true as sametable, conname,\n"
+ " pg_catalog.pg_get_constraintdef(r.oid, true) as condef,\n"
+ " conrelid::pg_catalog.regclass AS ontable\n"
+ "FROM pg_catalog.pg_constraint r\n"
+ "WHERE r.conrelid = '%s' AND r.contype = 'f'\n",
+ oid);
+
+ if (pset.sversion >= 120000)
+ appendPQExpBufferStr(&buf, " AND conparentid = 0\n");
+ appendPQExpBufferStr(&buf, "ORDER BY conname");
+ }
+
+ result = PSQLexec(buf.data);
+ if (!result)
+ goto error_return;
+ else
+ tuples = PQntuples(result);
+
+ if (tuples > 0)
+ {
+ int i_sametable = PQfnumber(result, "sametable"),
+ i_conname = PQfnumber(result, "conname"),
+ i_condef = PQfnumber(result, "condef"),
+ i_ontable = PQfnumber(result, "ontable");
+
+ printTableAddFooter(&cont, _("Foreign-key constraints:"));
+ for (i = 0; i < tuples; i++)
{
/*
- * Put the constraints defined in this table first, followed
- * by the constraints defined in ancestor partitioned tables.
+ * Print untranslated constraint name and definition. Use
+ * a "TABLE tab" prefix when the constraint is defined in
+ * a parent partitioned table.
*/
- printfPQExpBuffer(&buf,
- "SELECT conrelid = '%s'::pg_catalog.regclass AS sametable,\n"
- " conname,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef,\n"
- " conrelid::pg_catalog.regclass AS ontable\n"
- " FROM pg_catalog.pg_constraint,\n"
- " pg_catalog.pg_partition_ancestors('%s')\n"
- " WHERE conrelid = relid AND contype = 'f' AND conparentid = 0\n"
- "ORDER BY sametable DESC, conname;",
- oid, oid);
- }
- else
- {
- printfPQExpBuffer(&buf,
- "SELECT true as sametable, conname,\n"
- " pg_catalog.pg_get_constraintdef(r.oid, true) as condef,\n"
- " conrelid::pg_catalog.regclass AS ontable\n"
- "FROM pg_catalog.pg_constraint r\n"
- "WHERE r.conrelid = '%s' AND r.contype = 'f'\n",
- oid);
-
- if (pset.sversion >= 120000)
- appendPQExpBufferStr(&buf, " AND conparentid = 0\n");
- appendPQExpBufferStr(&buf, "ORDER BY conname");
- }
-
- result = PSQLexec(buf.data);
- if (!result)
- goto error_return;
- else
- tuples = PQntuples(result);
-
- if (tuples > 0)
- {
- int i_sametable = PQfnumber(result, "sametable"),
- i_conname = PQfnumber(result, "conname"),
- i_condef = PQfnumber(result, "condef"),
- i_ontable = PQfnumber(result, "ontable");
-
- printTableAddFooter(&cont, _("Foreign-key constraints:"));
- for (i = 0; i < tuples; i++)
- {
- /*
- * Print untranslated constraint name and definition. Use
- * a "TABLE tab" prefix when the constraint is defined in
- * a parent partitioned table.
- */
- if (strcmp(PQgetvalue(result, i, i_sametable), "f") == 0)
- printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
- PQgetvalue(result, i, i_ontable),
- PQgetvalue(result, i, i_conname),
- PQgetvalue(result, i, i_condef));
- else
- printfPQExpBuffer(&buf, " \"%s\" %s",
- PQgetvalue(result, i, i_conname),
- PQgetvalue(result, i, i_condef));
-
- printTableAddFooter(&cont, buf.data);
- }
- }
- PQclear(result);
- }
-
- /* print incoming foreign-key references */
- if (tableinfo.hastriggers ||
- tableinfo.relkind == RELKIND_PARTITIONED_TABLE)
- {
- if (pset.sversion >= 120000)
- {
- printfPQExpBuffer(&buf,
- "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
- " FROM pg_catalog.pg_constraint c\n"
- " WHERE confrelid IN (SELECT pg_catalog.pg_partition_ancestors('%s')\n"
- " UNION ALL VALUES ('%s'::pg_catalog.regclass))\n"
- " AND contype = 'f' AND conparentid = 0\n"
- "ORDER BY conname;",
- oid, oid);
- }
- else
- {
- printfPQExpBuffer(&buf,
- "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
- " FROM pg_catalog.pg_constraint\n"
- " WHERE confrelid = %s AND contype = 'f'\n"
- "ORDER BY conname;",
- oid);
- }
-
- result = PSQLexec(buf.data);
- if (!result)
- goto error_return;
- else
- tuples = PQntuples(result);
-
- if (tuples > 0)
- {
- int i_conname = PQfnumber(result, "conname"),
- i_ontable = PQfnumber(result, "ontable"),
- i_condef = PQfnumber(result, "condef");
-
- printTableAddFooter(&cont, _("Referenced by:"));
- for (i = 0; i < tuples; i++)
- {
+ if (strcmp(PQgetvalue(result, i, i_sametable), "f") == 0)
printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
PQgetvalue(result, i, i_ontable),
PQgetvalue(result, i, i_conname),
PQgetvalue(result, i, i_condef));
+ else
+ printfPQExpBuffer(&buf, " \"%s\" %s",
+ PQgetvalue(result, i, i_conname),
+ PQgetvalue(result, i, i_condef));
- printTableAddFooter(&cont, buf.data);
- }
+ printTableAddFooter(&cont, buf.data);
}
- PQclear(result);
}
+ PQclear(result);
+
+ if (pset.sversion >= 120000)
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
+ " FROM pg_catalog.pg_constraint c\n"
+ " WHERE confrelid IN (SELECT pg_catalog.pg_partition_ancestors('%s')\n"
+ " UNION ALL VALUES ('%s'::pg_catalog.regclass))\n"
+ " AND contype = 'f' AND conparentid = 0\n"
+ "ORDER BY conname;",
+ oid, oid);
+ }
+ else
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
+ " FROM pg_catalog.pg_constraint\n"
+ " WHERE confrelid = %s AND contype = 'f'\n"
+ "ORDER BY conname;",
+ oid);
+ }
+
+ result = PSQLexec(buf.data);
+ if (!result)
+ goto error_return;
+ else
+ tuples = PQntuples(result);
+
+ if (tuples > 0)
+ {
+ int i_conname = PQfnumber(result, "conname"),
+ i_ontable = PQfnumber(result, "ontable"),
+ i_condef = PQfnumber(result, "condef");
+
+ printTableAddFooter(&cont, _("Referenced by:"));
+ for (i = 0; i < tuples; i++)
+ {
+ printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
+ PQgetvalue(result, i, i_ontable),
+ PQgetvalue(result, i, i_conname),
+ PQgetvalue(result, i, i_condef));
+
+ printTableAddFooter(&cont, buf.data);
+ }
+ }
+ PQclear(result);
/* print any row-level policies */
if (pset.sversion >= 90500)
--
2.43.5
v3-0005-Add-support-for-NOT-ENFORCED-in-foreign-key-const.patchapplication/x-patch; name=v3-0005-Add-support-for-NOT-ENFORCED-in-foreign-key-const.patchDownload
From d43a8019574a47d9166700707664b4223455473d Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Thu, 14 Nov 2024 18:24:28 +0530
Subject: [PATCH v3 5/5] Add support for NOT ENFORCED in foreign key
constraints.
Normally, when a foreign key (FK) constraint is created on a table,
action/check triggers are associated with the constraint to ensure data
integrity. With this patch, when constraints are marked as NOT
ENFORCED, integrity checks are not required, and thus, these triggers
are unnecessary. As a result, when creating a NOT ENFORCED FK
constraint or changing an existing FK constraint to NOT ENFORCED, the
creation of triggers will be skipped, or existing triggers will be
dropped, respectively. And when changing an existing NOT ENFORCED FK
constraint to ENFORCED, those triggers will be created.
---
doc/src/sgml/advanced.sgml | 13 +
doc/src/sgml/ddl.sgml | 12 +-
doc/src/sgml/ref/alter_table.sgml | 5 +
doc/src/sgml/ref/create_table.sgml | 10 +-
src/backend/catalog/pg_constraint.c | 5 +-
src/backend/commands/tablecmds.c | 486 ++++++++++++++++------
src/backend/parser/gram.y | 3 +-
src/backend/parser/parse_utilcmd.c | 6 +-
src/backend/utils/cache/relcache.c | 1 +
src/include/utils/rel.h | 3 +
src/test/regress/expected/foreign_key.out | 118 +++++-
src/test/regress/sql/foreign_key.sql | 87 +++-
12 files changed, 594 insertions(+), 155 deletions(-)
diff --git a/doc/src/sgml/advanced.sgml b/doc/src/sgml/advanced.sgml
index 755c9f14850..b7d90de69d7 100644
--- a/doc/src/sgml/advanced.sgml
+++ b/doc/src/sgml/advanced.sgml
@@ -125,6 +125,19 @@ ERROR: insert or update on table "weather" violates foreign key constraint "wea
DETAIL: Key (city)=(Berkeley) is not present in table "cities".
</screen>
</para>
+ <para>
+ If, for some reason, you do not want to enforce this constraint and wish to
+ avoid the error, you can modify the foreign key constraint to <literal>NOT ENFORCED</literal>,
+ or specify it in the <link linkend="sql-createtable"><command>CREATE TABLE</command></link>.
+ </para>
+ <para>
+ Let's change the constraint to <literal>NOT ENFORCED</literal>, and when you
+ attempt to reinsert the same record, it will not result in any error.
+<programlisting>
+ALTER TABLE weather ALTER CONSTRAINT weather_city_fkey NOT ENFORCED;
+INSERT INTO weather VALUES ('Berkeley', 45, 53, 0.0, '1994-11-28');
+</programlisting>
+ </para>
<para>
The behavior of foreign keys can be finely tuned to your
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index 9e2708586d5..a014a3f0f6f 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -1058,11 +1058,13 @@ CREATE TABLE example (
</indexterm>
<para>
- A foreign key constraint specifies that the values in a column (or
- a group of columns) must match the values appearing in some row
- of another table.
- We say this maintains the <firstterm>referential
- integrity</firstterm> between two related tables.
+ A foreign key constraint with <literal>ENFORCED</literal>, the default
+ setting, specifies that the values in a column (or a group of columns)
+ must match the values appearing in some row of another table. We say this
+ maintains the <firstterm>referential integrity</firstterm> between two
+ related tables. If a foreign key constraint is created with <literal>NOT ENFORCED</literal>,
+ the check for aforementioned <firstterm>referential integrity</firstterm>
+ will not be performed.
</para>
<para>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index ca38937cf1e..0e33cf9bde9 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -555,6 +555,11 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
This form alters the attributes of a constraint that was previously
created. Currently only foreign key constraints may be altered.
</para>
+ <para>
+ Note that changing constraints to <literal>ENFORCED</literal>, which were
+ previously marked as valid and <literal>NOT ENFORCED</literal>, will
+ trigger validation, similar to <literal>VALIDATE CONSTRAINT</literal>.
+ </para>
</listitem>
</varlistentry>
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index 97c9af44f50..55d9d7add55 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -1373,11 +1373,11 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
<term><literal>NOT ENFORCED</literal></term>
<listitem>
<para>
- This is currently only allowed for <literal>CHECK</literal> constraints.
- If the constraint is <literal>NOT ENFORCED</literal>, this clause
- specifies that the constraint check will be skipped. When the constraint
- is <literal>ENFORCED</literal>, check is performed after each statement.
- This is the default.
+ This is currently only allowed for foreign key and <literal>CHECK</literal>
+ constraints. If the constraint is <literal>NOT ENFORCED</literal>, this
+ clause specifies that the constraint check will be skipped. When the
+ constraint is <literal>ENFORCED</literal>, check is performed after each
+ statement. This is the default.
</para>
</listitem>
</varlistentry>
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index f83167a8a69..aa93e258e49 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -100,8 +100,9 @@ CreateConstraintEntry(const char *constraintName,
ObjectAddresses *addrs_auto;
ObjectAddresses *addrs_normal;
- /* Only CHECK constraint can be not enforced */
- Assert(isEnforced || constraintType == CONSTRAINT_CHECK);
+ /* Only CHECK or FOREIGN KEY constraint can be not enforced */
+ Assert(isEnforced || constraintType == CONSTRAINT_CHECK ||
+ constraintType == CONSTRAINT_FOREIGN);
conDesc = table_open(ConstraintRelationId, RowExclusiveLock);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 2dce6bbcc33..eb89e2cae6b 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -389,19 +389,29 @@ static void AlterIndexNamespaces(Relation classRel, Relation rel,
static void AlterSeqNamespaces(Relation classRel, Relation rel,
Oid oldNspOid, Oid newNspOid, ObjectAddresses *objsMoved,
LOCKMODE lockmode);
-static ObjectAddress ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd,
+static ObjectAddress ATExecAlterConstraint(List **wqueue, Relation rel, AlterTableCmd *cmd,
bool recurse, bool recursing, LOCKMODE lockmode);
-static bool ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
+static bool ATExecAlterConstrRecurse(List **wqueue, Constraint *cmdcon,
+ Relation conrel, Relation tgrel,
const Oid fkrelid, const Oid pkrelid,
HeapTuple contuple, List **otherrelids,
LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
Oid ReferencedParentUpdTrigger,
Oid ReferencingParentInsTrigger,
Oid ReferencingParentUpdTrigger);
-static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
+static void ATExecAlterConstrEnforceability(List **wqueue, Constraint *cmdcon,
+ Relation conrel, Relation tgrel,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger);
+static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Oid fkrel,
bool deferrable, bool initdeferred,
List **otherrelids);
-static void ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
+static void ATExecAlterChildConstr(List **wqueue, Constraint *cmdcon,
+ Relation conrel, Relation tgrel,
const Oid fkrelid, const Oid pkrelid,
HeapTuple contuple, List **otherrelids,
LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
@@ -5416,7 +5426,7 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
lockmode);
break;
case AT_AlterConstraint: /* ALTER CONSTRAINT */
- address = ATExecAlterConstraint(rel, cmd, false, false, lockmode);
+ address = ATExecAlterConstraint(wqueue, rel, cmd, false, false, lockmode);
break;
case AT_ValidateConstraint: /* VALIDATE CONSTRAINT */
address = ATExecValidateConstraint(wqueue, rel, cmd->name, cmd->recurse,
@@ -10387,7 +10397,7 @@ addFkConstraint(addFkConstraintSides fkside,
fkconstraint->deferrable,
fkconstraint->initdeferred,
fkconstraint->initially_valid,
- true, /* Is Enforced */
+ fkconstraint->is_enforced,
parentConstr,
RelationGetRelid(rel),
fkattnum,
@@ -10504,20 +10514,22 @@ addFkRecurseReferenced(Constraint *fkconstraint, Relation rel,
Oid parentDelTrigger, Oid parentUpdTrigger,
bool with_period)
{
- Oid deleteTriggerOid,
- updateTriggerOid;
+ Oid deleteTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
Assert(CheckRelationLockedByMe(pkrel, ShareRowExclusiveLock, true));
Assert(CheckRelationLockedByMe(rel, ShareRowExclusiveLock, true));
/*
- * Create the action triggers that enforce the constraint.
+ * Create action triggers to enforce the constraint, or skip them if the
+ * constraint is not enforced.
*/
- createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
- fkconstraint,
- parentConstr, indexOid,
- parentDelTrigger, parentUpdTrigger,
- &deleteTriggerOid, &updateTriggerOid);
+ if (fkconstraint->is_enforced)
+ createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
+ fkconstraint,
+ parentConstr, indexOid,
+ parentDelTrigger, parentUpdTrigger,
+ &deleteTriggerOid, &updateTriggerOid);
/*
* If the referenced table is partitioned, recurse on ourselves to handle
@@ -10638,8 +10650,8 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
Oid parentInsTrigger, Oid parentUpdTrigger,
bool with_period)
{
- Oid insertTriggerOid,
- updateTriggerOid;
+ Oid insertTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
Assert(OidIsValid(parentConstr));
Assert(CheckRelationLockedByMe(rel, ShareRowExclusiveLock, true));
@@ -10651,29 +10663,32 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
errmsg("foreign key constraints are not supported on foreign tables")));
/*
- * Add the check triggers to it and, if necessary, schedule it to be
- * checked in Phase 3.
+ * Add check triggers if the constraint is enforced, and if needed,
+ * schedule them to be checked in Phase 3.
*
* If the relation is partitioned, drill down to do it to its partitions.
*/
- createForeignKeyCheckTriggers(RelationGetRelid(rel),
- RelationGetRelid(pkrel),
- fkconstraint,
- parentConstr,
- indexOid,
- parentInsTrigger, parentUpdTrigger,
- &insertTriggerOid, &updateTriggerOid);
+ if (fkconstraint->is_enforced)
+ createForeignKeyCheckTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
+ fkconstraint,
+ parentConstr,
+ indexOid,
+ parentInsTrigger, parentUpdTrigger,
+ &insertTriggerOid, &updateTriggerOid);
if (rel->rd_rel->relkind == RELKIND_RELATION)
{
/*
* Tell Phase 3 to check that the constraint is satisfied by existing
- * rows. We can skip this during table creation, when requested
- * explicitly by specifying NOT VALID in an ADD FOREIGN KEY command,
- * and when we're recreating a constraint following a SET DATA TYPE
- * operation that did not impugn its validity.
+ * rows. We can skip this during table creation, when constraint is
+ * specified as NOT ENFORCED, when requested explicitly by specifying
+ * NOT VALID in an ADD FOREIGN KEY command, and when we're recreating
+ * a constraint following a SET DATA TYPE operation that did not
+ * impugn its validity.
*/
- if (wqueue && !old_check_ok && !fkconstraint->skip_validation)
+ if (wqueue && !old_check_ok && !fkconstraint->skip_validation &&
+ fkconstraint->is_enforced)
{
NewConstraint *newcon;
AlteredTableInfo *tab;
@@ -10908,8 +10923,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
AttrNumber confdelsetcols[INDEX_MAX_KEYS];
Constraint *fkconstraint;
ObjectAddress address;
- Oid deleteTriggerOid,
- updateTriggerOid;
+ Oid deleteTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(constrOid));
if (!HeapTupleIsValid(tuple))
@@ -10959,6 +10974,7 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
fkconstraint->conname = NameStr(constrForm->conname);
fkconstraint->deferrable = constrForm->condeferrable;
fkconstraint->initdeferred = constrForm->condeferred;
+ fkconstraint->is_enforced = constrForm->conenforced;
fkconstraint->location = -1;
fkconstraint->pktable = NULL;
/* ->fk_attrs determined below */
@@ -10998,9 +11014,10 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
* parent OIDs for similar triggers that will be created on the
* partition in addFkRecurseReferenced().
*/
- GetForeignKeyActionTriggers(trigrel, constrOid,
- constrForm->confrelid, constrForm->conrelid,
- &deleteTriggerOid, &updateTriggerOid);
+ if (constrForm->conenforced)
+ GetForeignKeyActionTriggers(trigrel, constrOid,
+ constrForm->confrelid, constrForm->conrelid,
+ &deleteTriggerOid, &updateTriggerOid);
/* Add this constraint ... */
address = addFkConstraint(addFkReferencedSide,
@@ -11166,17 +11183,18 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
mapped_conkey[i] = attmap->attnums[conkey[i] - 1];
/*
- * Get the "check" triggers belonging to the constraint to pass as
- * parent OIDs for similar triggers that will be created on the
- * partition in addFkRecurseReferencing(). They are also passed to
- * tryAttachPartitionForeignKey() below to simply assign as parents to
- * the partition's existing "check" triggers, that is, if the
- * corresponding constraints is deemed attachable to the parent
- * constraint.
+ * Get the "check" triggers belonging to the constraint, if it is
+ * enforced, to pass as parent OIDs for similar triggers that will be
+ * created on the partition in addFkRecurseReferencing(). They are
+ * also passed to tryAttachPartitionForeignKey() below to simply
+ * assign as parents to the partition's existing "check" triggers,
+ * that is, if the corresponding constraints is deemed attachable to
+ * the parent constraint.
*/
- GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
- constrForm->confrelid, constrForm->conrelid,
- &insertTriggerOid, &updateTriggerOid);
+ if (constrForm->conenforced)
+ GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
+ constrForm->confrelid, constrForm->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
/*
* Before creating a new constraint, see whether any existing FKs are
@@ -11218,6 +11236,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
/* ->conname determined below */
fkconstraint->deferrable = constrForm->condeferrable;
fkconstraint->initdeferred = constrForm->condeferred;
+ fkconstraint->is_enforced = constrForm->conenforced;
fkconstraint->location = -1;
fkconstraint->pktable = NULL;
/* ->fk_attrs determined below */
@@ -11312,8 +11331,6 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
ScanKeyData key;
SysScanDesc scan;
HeapTuple trigtup;
- Oid insertTriggerOid,
- updateTriggerOid;
parentConstrTup = SearchSysCache1(CONSTROID,
ObjectIdGetDatum(parentConstrOid));
@@ -11355,6 +11372,7 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
!partConstr->convalidated ||
partConstr->condeferrable != parentConstr->condeferrable ||
partConstr->condeferred != parentConstr->condeferred ||
+ partConstr->conenforced != parentConstr->conenforced ||
partConstr->confupdtype != parentConstr->confupdtype ||
partConstr->confdeltype != parentConstr->confdeltype ||
partConstr->confmatchtype != parentConstr->confmatchtype)
@@ -11414,18 +11432,25 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
ConstraintSetParentConstraint(fk->conoid, parentConstrOid, partRelid);
/*
- * Like the constraint, attach partition's "check" triggers to the
- * corresponding parent triggers.
+ * Similar to the constraint, attach the partition's "check" triggers to
+ * the corresponding parent triggers if the constraint is enforced.
+ * Non-enforced constraints do not have these triggers.
*/
- GetForeignKeyCheckTriggers(trigrel,
- fk->conoid, fk->confrelid, fk->conrelid,
- &insertTriggerOid, &updateTriggerOid);
- Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
- TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
- partRelid);
- Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
- TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
- partRelid);
+ if (fk->conenforced)
+ {
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
+ partRelid);
+ Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
+ partRelid);
+ }
/*
* If the referenced table is partitioned, then the partition we're
@@ -11639,8 +11664,8 @@ GetForeignKeyCheckTriggers(Relation trigrel,
* InvalidObjectAddress.
*/
static ObjectAddress
-ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
- bool recursing, LOCKMODE lockmode)
+ATExecAlterConstraint(List **wqueue, Relation rel, AlterTableCmd *cmd,
+ bool recurse, bool recursing, LOCKMODE lockmode)
{
Constraint *cmdcon;
Relation conrel;
@@ -11742,12 +11767,14 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
address = InvalidObjectAddress;
if (currcon->condeferrable != cmdcon->deferrable ||
currcon->condeferred != cmdcon->initdeferred ||
+ currcon->conenforced != cmdcon->is_enforced ||
rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
- if (ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, currcon->conrelid,
- currcon->confrelid, contuple, &otherrelids,
- lockmode, InvalidOid, InvalidOid,
- InvalidOid, InvalidOid))
+ if (ATExecAlterConstrRecurse(wqueue, cmdcon, conrel, tgrel,
+ currcon->conrelid, currcon->confrelid,
+ contuple, &otherrelids, lockmode,
+ InvalidOid, InvalidOid, InvalidOid,
+ InvalidOid))
ObjectAddressSet(address, ConstraintRelationId, currcon->oid);
}
@@ -11779,8 +11806,8 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
* but existing releases don't do that.)
*/
static bool
-ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
- const Oid fkrelid, const Oid pkrelid,
+ATExecAlterConstrRecurse(List **wqueue, Constraint *cmdcon, Relation conrel,
+ Relation tgrel, const Oid fkrelid, const Oid pkrelid,
HeapTuple contuple, List **otherrelids,
LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
Oid ReferencedParentUpdTrigger,
@@ -11789,19 +11816,18 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
{
Form_pg_constraint currcon;
Oid conoid;
+ Oid conrelid;
Oid refrelid;
bool changed = false;
- Relation rel;
/* since this function recurses, it could be driven to stack overflow */
check_stack_depth();
currcon = (Form_pg_constraint) GETSTRUCT(contuple);
conoid = currcon->oid;
+ conrelid = currcon->conrelid;
refrelid = currcon->confrelid;
- rel = table_open(currcon->conrelid, lockmode);
-
/*
* Update pg_constraint with the flags from cmdcon.
*
@@ -11809,7 +11835,8 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
* silently do nothing.
*/
if (currcon->condeferrable != cmdcon->deferrable ||
- currcon->condeferred != cmdcon->initdeferred)
+ currcon->condeferred != cmdcon->initdeferred ||
+ currcon->conenforced != cmdcon->is_enforced)
{
HeapTuple copyTuple;
Form_pg_constraint copy_con;
@@ -11818,6 +11845,7 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
copy_con->condeferrable = cmdcon->deferrable;
copy_con->condeferred = cmdcon->initdeferred;
+ copy_con->conenforced = cmdcon->is_enforced;
CatalogTupleUpdate(conrel, ©Tuple->t_self, copyTuple);
InvokeObjectPostAlterHook(ConstraintRelationId,
@@ -11827,38 +11855,214 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
changed = true;
/* Make new constraint flags visible to others */
- CacheInvalidateRelcache(rel);
+ CacheInvalidateRelcacheByRelid(conrelid);
+ }
+ if (currcon->conenforced != cmdcon->is_enforced)
+ {
+ ATExecAlterConstrEnforceability(wqueue, cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, contuple, otherrelids, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
+ }
+ else
+ {
/*
* Now we need to update the multiple entries in pg_trigger that
* implement the constraint.
*/
- AlterConstrTriggerDeferrability(conoid, tgrel, rel, cmdcon->deferrable,
- cmdcon->initdeferred, otherrelids);
+ AlterConstrTriggerDeferrability(conoid, tgrel, conrelid,
+ cmdcon->deferrable,
+ cmdcon->initdeferred,
+ otherrelids);
+
+ /*
+ * If the table at either end of the constraint is partitioned, we
+ * need to recurse and handle every constraint that is a child of this
+ * one.
+ *
+ * (This assumes that the recurse flag is forcibly set for partitioned
+ * tables, and not set for legacy inheritance, though we don't check
+ * for that here.)
+ */
+ if (get_rel_relkind(conrelid) == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)
+ ATExecAlterChildConstr(wqueue, cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, contuple, otherrelids, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
}
- /*
- * If the table at either end of the constraint is partitioned, we need to
- * recurse and handle every constraint that is a child of this one.
- *
- * (This assumes that the recurse flag is forcibly set for partitioned
- * tables, and not set for legacy inheritance, though we don't check for
- * that here.)
- */
- if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
- get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)
- ATExecAlterChildConstr(cmdcon, conrel, tgrel, fkrelid, pkrelid,
- contuple, otherrelids, lockmode,
- ReferencedParentDelTrigger,
- ReferencedParentUpdTrigger,
- ReferencingParentInsTrigger,
- ReferencingParentUpdTrigger);
-
- table_close(rel, NoLock);
-
return changed;
}
+/*
+ * A subroutine of ATExecAlterConstrRecurse that updates the enforceability of
+ * a foreign key constraint. Depending on whether the constraint is being set
+ * to enforced or not enforced, it creates or drops the trigger accordingly.
+ *
+ * The arguments to this function have the same meaning as the arguments to
+ * ATExecAlterConstrRecurse.
+ */
+static void
+ATExecAlterConstrEnforceability(List **wqueue, Constraint *cmdcon,
+ Relation conrel, Relation tgrel,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger)
+{
+ Form_pg_constraint currcon;
+ Oid conoid;
+
+ currcon = (Form_pg_constraint) GETSTRUCT(contuple);
+ conoid = currcon->oid;
+
+ /* Drop triggers */
+ if (!cmdcon->is_enforced)
+ {
+ HeapTuple tgtuple;
+ ScanKeyData tgkey;
+ SysScanDesc tgscan;
+
+ /*
+ * When setting a constraint to NOT ENFORCED, the constraint triggers
+ * need to be dropped. Therefore, we must process the child relations
+ * first, followed by the parent, to account for dependencies.
+ */
+ if (get_rel_relkind(currcon->conrelid) == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE)
+ ATExecAlterChildConstr(wqueue, cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, contuple, otherrelids, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
+
+ ScanKeyInit(&tgkey,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+ tgscan = systable_beginscan(tgrel, TriggerConstraintIndexId, true,
+ NULL, 1, &tgkey);
+ while (HeapTupleIsValid(tgtuple = systable_getnext(tgscan)))
+ {
+ Form_pg_trigger tgform = (Form_pg_trigger) GETSTRUCT(tgtuple);
+ ObjectAddress trigger;
+
+ /*
+ * Delete only RI_FKey_noaction_del, RI_FKey_noaction_upd,
+ * RI_FKey_check_ins and RI_FKey_check_upd triggers, but not
+ * others; see createForeignKeyActionTriggers and
+ * CreateFKCheckTrigger.
+ */
+ if (tgform->tgfoid != F_RI_FKEY_NOACTION_DEL &&
+ tgform->tgfoid != F_RI_FKEY_NOACTION_UPD &&
+ tgform->tgfoid != F_RI_FKEY_CHECK_INS &&
+ tgform->tgfoid != F_RI_FKEY_CHECK_UPD)
+ continue;
+
+ deleteDependencyRecordsFor(TriggerRelationId, tgform->oid,
+ false);
+ /* make dependency deletion visible to performDeletion */
+ CommandCounterIncrement();
+ ObjectAddressSet(trigger, TriggerRelationId, tgform->oid);
+ performDeletion(&trigger, DROP_RESTRICT, 0);
+ /* make trigger drop visible, in case the loop iterates */
+ CommandCounterIncrement();
+ }
+
+ systable_endscan(tgscan);
+ }
+ else /* Create triggers */
+ {
+ Relation rel;
+ Oid ReferencedDelTriggerOid = InvalidOid,
+ ReferencedUpdTriggerOid = InvalidOid,
+ ReferencingInsTriggerOid = InvalidOid,
+ ReferencingUpdTriggerOid = InvalidOid;
+
+ /* Update the foreign key action required for trigger creation. */
+ cmdcon->fk_matchtype = currcon->confmatchtype;
+ cmdcon->fk_upd_action = currcon->confupdtype;
+ cmdcon->fk_del_action = currcon->confdeltype;
+
+ rel = table_open(currcon->conrelid, lockmode);
+
+ /* Create referenced triggers */
+ if (currcon->conrelid == fkrelid)
+ createForeignKeyActionTriggers(rel,
+ currcon->confrelid,
+ cmdcon,
+ conoid,
+ currcon->conindid,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ &ReferencedDelTriggerOid,
+ &ReferencedUpdTriggerOid);
+
+ /* Create referencing triggers */
+ if (currcon->confrelid == pkrelid)
+ {
+ createForeignKeyCheckTriggers(currcon->conrelid,
+ pkrelid,
+ cmdcon,
+ conoid,
+ currcon->conindid,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger,
+ &ReferencingInsTriggerOid,
+ &ReferencingUpdTriggerOid);
+
+ /*
+ * Tell Phase 3 to check that the constraint is satisfied by
+ * existing rows, but skip this step if the rows have not been
+ * validated previously.
+ */
+ if (rel->rd_rel->relkind == RELKIND_RELATION &&
+ wqueue && currcon->convalidated)
+ {
+ NewConstraint *newcon;
+ AlteredTableInfo *tab;
+
+ tab = ATGetQueueEntry(wqueue, rel);
+
+ newcon = (NewConstraint *) palloc0(sizeof(NewConstraint));
+ newcon->name = pstrdup(NameStr(currcon->conname));
+ newcon->conid = currcon->oid;
+ newcon->contype = CONSTR_FOREIGN;
+ newcon->refrelid = pkrelid;
+ newcon->refindid = currcon->conindid;
+ newcon->conwithperiod = currcon->conperiod;
+ newcon->qual = (Node *) cmdcon;
+
+ tab->constraints = lappend(tab->constraints, newcon);
+ }
+ }
+
+ /*
+ * If the table at either end of the constraint is partitioned, we
+ * need to recurse and create triggers for each constraint that is a
+ * child of this one.
+ */
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE)
+ ATExecAlterChildConstr(wqueue, cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, contuple, otherrelids, lockmode,
+ ReferencedDelTriggerOid,
+ ReferencedUpdTriggerOid,
+ ReferencingInsTriggerOid,
+ ReferencingUpdTriggerOid);
+ table_close(rel, NoLock);
+ }
+}
+
/*
* A subroutine of ATExecAlterConstrRecurse that updated constraint trigger's
* deferrability.
@@ -11867,7 +12071,7 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
* ATExecAlterConstrRecurse.
*/
static void
-AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
+AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Oid fkrel,
bool deferrable, bool initdeferred,
List **otherrelids)
{
@@ -11893,7 +12097,7 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
* other rels that don't have a trigger whose properties change, but
* let's be conservative.)
*/
- if (tgform->tgrelid != RelationGetRelid(rel))
+ if (tgform->tgrelid != fkrel)
*otherrelids = list_append_unique_oid(*otherrelids,
tgform->tgrelid);
@@ -11932,8 +12136,8 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
* ATExecAlterConstrRecurse.
*/
static void
-ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
- const Oid fkrelid, const Oid pkrelid,
+ATExecAlterChildConstr(List **wqueue, Constraint *cmdcon, Relation conrel,
+ Relation tgrel, const Oid fkrelid, const Oid pkrelid,
HeapTuple contuple, List **otherrelids,
LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
Oid ReferencedParentUpdTrigger,
@@ -11958,9 +12162,9 @@ ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
true, NULL, 1, &pkey);
while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
- ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, fkrelid, pkrelid,
- childtup, otherrelids, lockmode,
- ReferencedParentDelTrigger,
+ ATExecAlterConstrRecurse(wqueue, cmdcon, conrel, tgrel,
+ fkrelid, pkrelid, childtup, otherrelids,
+ lockmode, ReferencedParentDelTrigger,
ReferencedParentUpdTrigger,
ReferencingParentInsTrigger,
ReferencingParentUpdTrigger);
@@ -12033,25 +12237,33 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
if (con->contype == CONSTRAINT_FOREIGN)
{
- NewConstraint *newcon;
- Constraint *fkconstraint;
+ /*
+ * Queue validation for phase 3 only if constraint is enforced;
+ * otherwise, adding it to the validation queue won't be very
+ * effective, as the verification will be skipped.
+ */
+ if (con->conenforced)
+ {
+ NewConstraint *newcon;
+ Constraint *fkconstraint;
- /* Queue validation for phase 3 */
- fkconstraint = makeNode(Constraint);
- /* for now this is all we need */
- fkconstraint->conname = constrName;
+ /* Queue validation for phase 3 */
+ fkconstraint = makeNode(Constraint);
+ /* for now this is all we need */
+ fkconstraint->conname = constrName;
- newcon = (NewConstraint *) palloc0(sizeof(NewConstraint));
- newcon->name = constrName;
- newcon->contype = CONSTR_FOREIGN;
- newcon->refrelid = con->confrelid;
- newcon->refindid = con->conindid;
- newcon->conid = con->oid;
- newcon->qual = (Node *) fkconstraint;
+ newcon = (NewConstraint *) palloc0(sizeof(NewConstraint));
+ newcon->name = constrName;
+ newcon->contype = CONSTR_FOREIGN;
+ newcon->refrelid = con->confrelid;
+ newcon->refindid = con->conindid;
+ newcon->conid = con->oid;
+ newcon->qual = (Node *) fkconstraint;
- /* Find or create work queue entry for this table */
- tab = ATGetQueueEntry(wqueue, rel);
- tab->constraints = lappend(tab->constraints, newcon);
+ /* Find or create work queue entry for this table */
+ tab = ATGetQueueEntry(wqueue, rel);
+ tab->constraints = lappend(tab->constraints, newcon);
+ }
/*
* We disallow creating invalid foreign keys to or from
@@ -12110,9 +12322,9 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
}
/*
- * Queue validation for phase 3 only if constraint is enforced;
- * otherwise, adding it to the validation queue won't be very
- * effective, as the verification will be skipped.
+ * Queue validation for phase 3 only if the constraint is
+ * enforced, for the same reason outlined for the foreign key
+ * constraint.
*/
if (con->conenforced)
{
@@ -12141,8 +12353,11 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
}
/*
- * Update the catalog regardless of enforcement; validation will occur
- * only when the constraint is enforced.
+ * Update the catalog regardless of enforcement; validation will only
+ * occur when the constraint is enforced. Additionally, when changing a
+ * non-enforced constraint to enforced, validation will only take place
+ * if the validation flag is set to true. Otherwise, the user must
+ * explicitly perform constraint validation.
*/
copyTuple = heap_copytuple(tuple);
copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
@@ -19877,8 +20092,6 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
Form_pg_constraint conform;
- Oid insertTriggerOid,
- updateTriggerOid;
contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
if (!HeapTupleIsValid(contup))
@@ -19901,17 +20114,25 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
/*
* Also, look up the partition's "check" triggers corresponding to the
- * constraint being detached and detach them from the parent triggers.
+ * enforced constraint being detached and detach them from the parent
+ * triggers. Non-enforced constraints do not have these triggers;
+ * therefore, this step is not needed.
*/
- GetForeignKeyCheckTriggers(trigrel,
- fk->conoid, fk->confrelid, fk->conrelid,
- &insertTriggerOid, &updateTriggerOid);
- Assert(OidIsValid(insertTriggerOid));
- TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
- RelationGetRelid(partRel));
- Assert(OidIsValid(updateTriggerOid));
- TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
- RelationGetRelid(partRel));
+ if (fk->conenforced)
+ {
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ Assert(OidIsValid(updateTriggerOid));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ }
/*
* Lastly, create the action triggers on the referenced table, using
@@ -19951,6 +20172,7 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
fkconstraint->conname = pstrdup(NameStr(conform->conname));
fkconstraint->deferrable = conform->condeferrable;
fkconstraint->initdeferred = conform->condeferred;
+ fkconstraint->is_enforced = conform->conenforced;
fkconstraint->skip_validation = true;
fkconstraint->initially_valid = true;
/* a few irrelevant fields omitted here */
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index d93a92a7cd3..10d597e323f 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -4027,6 +4027,7 @@ ColConstraintElem:
n->fk_del_set_cols = ($5)->deleteAction->cols;
n->skip_validation = false;
n->initially_valid = true;
+ n->is_enforced = true;
$$ = (Node *) n;
}
;
@@ -4302,7 +4303,7 @@ ConstraintElem:
processCASbits($12, @12, "FOREIGN KEY",
&n->deferrable, &n->initdeferred,
&n->skip_validation, NULL,
- NULL,
+ &n->is_enforced,
yyscanner);
n->initially_valid = !n->skip_validation;
$$ = (Node *) n;
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 183f845ba55..ce66ec3d5d3 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -3963,7 +3963,8 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
case CONSTR_ATTR_ENFORCED:
if (lastprimarycon &&
- lastprimarycon->contype != CONSTR_CHECK)
+ lastprimarycon->contype != CONSTR_CHECK &&
+ lastprimarycon->contype != CONSTR_FOREIGN)
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("misplaced ENFORCED clause"),
@@ -3979,7 +3980,8 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
case CONSTR_ATTR_NOT_ENFORCED:
if (lastprimarycon &&
- lastprimarycon->contype != CONSTR_CHECK)
+ lastprimarycon->contype != CONSTR_CHECK &&
+ lastprimarycon->contype != CONSTR_FOREIGN)
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("misplaced NOT ENFORCED clause"),
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index b25dfb09808..bf15fef94f1 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4701,6 +4701,7 @@ RelationGetFKeyList(Relation relation)
info->conoid = constraint->oid;
info->conrelid = constraint->conrelid;
info->confrelid = constraint->confrelid;
+ info->conenforced = constraint->conenforced;
DeconstructFkConstraintRow(htup, &info->nkeys,
info->conkey,
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 87002049538..2db011ddfb7 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -284,6 +284,9 @@ typedef struct ForeignKeyCacheInfo
/* number of columns in the foreign key */
int nkeys;
+ /* Is enforced ? */
+ bool conenforced;
+
/*
* these arrays each have nkeys valid entries:
*/
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index a5165270c2d..1852776b77b 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -1,12 +1,43 @@
--
-- FOREIGN KEY
--
--- MATCH FULL
+-- NOT ENFORCED
--
-- First test, check and cascade
--
CREATE TABLE PKTABLE ( ptest1 int PRIMARY KEY, ptest2 text );
-CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE, ftest2 int );
+CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE NOT ENFORCED,
+ ftest2 int );
+-- Inserting into the foreign key table will not result in an error, even if
+-- there is no matching key in the referenced table.
+INSERT INTO FKTABLE VALUES (1, 2);
+INSERT INTO FKTABLE VALUES (2, 3);
+-- Check FKTABLE
+SELECT * FROM FKTABLE;
+ ftest1 | ftest2
+--------+--------
+ 1 | 2
+ 2 | 3
+(2 rows)
+
+-- Changing it back to ENFORCED will result in an error due to validation of
+-- existing rows.
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_fkey"
+DETAIL: Key (ftest1)=(1) is not present in table "pktable".
+-- Remove any existing rows that violate the constraint, then attempt to change
+-- it.
+TRUNCATE FKTABLE;
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+-- Any further inserts will fail due to the enforcement.
+INSERT INTO FKTABLE VALUES (3, 4);
+ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_fkey"
+DETAIL: Key (ftest1)=(3) is not present in table "pktable".
+--
+-- MATCH FULL
+--
+-- First test, check and cascade
+--
-- Insert test data into PKTABLE
INSERT INTO PKTABLE VALUES (1, 'Test1');
INSERT INTO PKTABLE VALUES (2, 'Test2');
@@ -351,6 +382,8 @@ INSERT INTO FKTABLE VALUES (1, NULL);
ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL;
ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_ftest2_fkey"
DETAIL: MATCH FULL does not allow mixing of null and nonnull key values.
+-- NOT ENFORCED will bypass the initial check.
+ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL NOT ENFORCED;
DROP TABLE FKTABLE;
DROP TABLE PKTABLE;
-- MATCH SIMPLE
@@ -1276,13 +1309,24 @@ INSERT INTO fktable VALUES (0, 20);
ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_fk_fkey"
DETAIL: Key (fk)=(20) is not present in table "pktable".
COMMIT;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT ENFORCED;
+BEGIN;
+-- doesn't match FK, no error.
+UPDATE pktable SET id = 10 WHERE id = 5;
+-- doesn't match PK, no error.
+INSERT INTO fktable VALUES (0, 20);
+ROLLBACK;
-- try additional syntax
-ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE ENFORCED;
-- illegal option
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE INITIALLY DEFERRED;
ERROR: constraint declared INITIALLY DEFERRED must be DEFERRABLE
LINE 1: ...e ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE INITIALLY ...
^
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORCED;
+ERROR: conflicting constraint properties
+LINE 1: ...fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORC...
+ ^
-- test order of firing of FK triggers when several RI-induced changes need to
-- be made to the same row. This was broken by subtransaction-related
-- changes in 8.0.
@@ -1580,7 +1624,8 @@ ALTER TABLE fk_partitioned_fk DROP COLUMN fdrop1;
CREATE TABLE fk_partitioned_fk_1 (fdrop1 int, fdrop2 int, a int, fdrop3 int, b int);
ALTER TABLE fk_partitioned_fk_1 DROP COLUMN fdrop1, DROP COLUMN fdrop2, DROP COLUMN fdrop3;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FROM (0,0) TO (1000,1000);
-ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk;
+ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
@@ -1665,6 +1710,37 @@ Indexes:
Referenced by:
TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b)
+-- Check the exsting FK trigger
+CREATE TEMP TABLE tmp_trg_info AS SELECT tgtype, tgrelid::regclass, tgconstraint
+FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+SELECT count(1) FROM tmp_trg_info;
+ count
+-------
+ 14
+(1 row)
+
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+-- No triggers
+SELECT count(1) FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+ count
+-------
+ 0
+(1 row)
+
+-- Changing it back to ENFORCED will recreate the necessary triggers.
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+-- Should be exactly the same number of triggers found as before
+SELECT COUNT(1) FROM pg_trigger pgt JOIN tmp_trg_info tt
+ON (pgt.tgtype = tt.tgtype AND pgt.tgrelid = tt.tgrelid AND pgt.tgconstraint = tt.tgconstraint);
+ count
+-------
+ 14
+(1 row)
+
ALTER TABLE fk_partitioned_fk DROP CONSTRAINT fk_partitioned_fk_a_b_fkey;
-- done.
DROP TABLE fk_notpartitioned_pk, fk_partitioned_fk;
@@ -1868,6 +1944,40 @@ Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
Foreign-key constraints:
TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
+DROP TABLE fk_partitioned_fk_2;
+CREATE TABLE fk_partitioned_fk_2 (b int, a int,
+ FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
+BEGIN;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have two constraints
+\d fk_partitioned_fk_2
+ Table "public.fk_partitioned_fk_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ b | integer | | |
+ a | integer | | |
+Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
+Foreign-key constraints:
+ "fk_partitioned_fk_2_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED
+ TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
+
+DROP TABLE fk_partitioned_fk_2;
+ROLLBACK;
+BEGIN;
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have only one constraint
+\d fk_partitioned_fk_2
+ Table "public.fk_partitioned_fk_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ b | integer | | |
+ a | integer | | |
+Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
+Foreign-key constraints:
+ TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED
+
+ROLLBACK;
DROP TABLE fk_partitioned_fk_2;
CREATE TABLE fk_partitioned_fk_4 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE) PARTITION BY RANGE (b, a);
CREATE TABLE fk_partitioned_fk_4_1 PARTITION OF fk_partitioned_fk_4 FOR VALUES FROM (1,1) TO (100,100);
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index 2e710e419c2..33a43e2dd70 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -2,13 +2,39 @@
-- FOREIGN KEY
--
--- MATCH FULL
+-- NOT ENFORCED
--
-- First test, check and cascade
--
CREATE TABLE PKTABLE ( ptest1 int PRIMARY KEY, ptest2 text );
-CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE, ftest2 int );
+CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE NOT ENFORCED,
+ ftest2 int );
+-- Inserting into the foreign key table will not result in an error, even if
+-- there is no matching key in the referenced table.
+INSERT INTO FKTABLE VALUES (1, 2);
+INSERT INTO FKTABLE VALUES (2, 3);
+
+-- Check FKTABLE
+SELECT * FROM FKTABLE;
+
+-- Changing it back to ENFORCED will result in an error due to validation of
+-- existing rows.
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+
+-- Remove any existing rows that violate the constraint, then attempt to change
+-- it.
+TRUNCATE FKTABLE;
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+
+-- Any further inserts will fail due to the enforcement.
+INSERT INTO FKTABLE VALUES (3, 4);
+
+--
+-- MATCH FULL
+--
+-- First test, check and cascade
+--
-- Insert test data into PKTABLE
INSERT INTO PKTABLE VALUES (1, 'Test1');
INSERT INTO PKTABLE VALUES (2, 'Test2');
@@ -230,6 +256,9 @@ INSERT INTO FKTABLE VALUES (1, NULL);
ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL;
+-- NOT ENFORCED will bypass the initial check.
+ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL NOT ENFORCED;
+
DROP TABLE FKTABLE;
DROP TABLE PKTABLE;
@@ -968,10 +997,22 @@ INSERT INTO fktable VALUES (0, 20);
COMMIT;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT ENFORCED;
+
+BEGIN;
+
+-- doesn't match FK, no error.
+UPDATE pktable SET id = 10 WHERE id = 5;
+-- doesn't match PK, no error.
+INSERT INTO fktable VALUES (0, 20);
+
+ROLLBACK;
+
-- try additional syntax
-ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE ENFORCED;
-- illegal option
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORCED;
-- test order of firing of FK triggers when several RI-induced changes need to
-- be made to the same row. This was broken by subtransaction-related
@@ -1182,7 +1223,8 @@ ALTER TABLE fk_partitioned_fk DROP COLUMN fdrop1;
CREATE TABLE fk_partitioned_fk_1 (fdrop1 int, fdrop2 int, a int, fdrop3 int, b int);
ALTER TABLE fk_partitioned_fk_1 DROP COLUMN fdrop1, DROP COLUMN fdrop2, DROP COLUMN fdrop3;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FROM (0,0) TO (1000,1000);
-ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk;
+ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
@@ -1236,6 +1278,27 @@ UPDATE fk_notpartitioned_pk SET b = 1502 WHERE a = 1500;
UPDATE fk_notpartitioned_pk SET b = 2504 WHERE a = 2500;
-- check psql behavior
\d fk_notpartitioned_pk
+
+-- Check the exsting FK trigger
+CREATE TEMP TABLE tmp_trg_info AS SELECT tgtype, tgrelid::regclass, tgconstraint
+FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+SELECT count(1) FROM tmp_trg_info;
+
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+-- No triggers
+SELECT count(1) FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+
+-- Changing it back to ENFORCED will recreate the necessary triggers.
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+
+-- Should be exactly the same number of triggers found as before
+SELECT COUNT(1) FROM pg_trigger pgt JOIN tmp_trg_info tt
+ON (pgt.tgtype = tt.tgtype AND pgt.tgrelid = tt.tgrelid AND pgt.tgconstraint = tt.tgconstraint);
+
ALTER TABLE fk_partitioned_fk DROP CONSTRAINT fk_partitioned_fk_a_b_fkey;
-- done.
DROP TABLE fk_notpartitioned_pk, fk_partitioned_fk;
@@ -1372,6 +1435,22 @@ ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN
\d fk_partitioned_fk_2
DROP TABLE fk_partitioned_fk_2;
+CREATE TABLE fk_partitioned_fk_2 (b int, a int,
+ FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
+BEGIN;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have two constraints
+\d fk_partitioned_fk_2
+DROP TABLE fk_partitioned_fk_2;
+ROLLBACK;
+BEGIN;
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have only one constraint
+\d fk_partitioned_fk_2
+ROLLBACK;
+DROP TABLE fk_partitioned_fk_2;
+
CREATE TABLE fk_partitioned_fk_4 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE) PARTITION BY RANGE (b, a);
CREATE TABLE fk_partitioned_fk_4_1 PARTITION OF fk_partitioned_fk_4 FOR VALUES FROM (1,1) TO (100,100);
CREATE TABLE fk_partitioned_fk_4_2 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE SET NULL);
--
2.43.5
On Thu, Nov 14, 2024 at 7:10 PM Amul Sul <sulamul@gmail.com> wrote:
On Tue, Nov 12, 2024 at 3:48 PM Peter Eisentraut <peter@eisentraut.org> wrote:
Updated version attached.
In the attached version, did minor corrections in the document and
improved test coverage.
Regards,
Amul
Attachments:
v4-0001-Add-support-for-NOT-ENFORCED-in-CHECK-constraints.patchapplication/x-patch; name=v4-0001-Add-support-for-NOT-ENFORCED-in-CHECK-constraints.patchDownload
From b9453f66c4954cba678e1acb872fc2230c7177c5 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Thu, 14 Nov 2024 18:07:06 +0530
Subject: [PATCH v4 1/5] Add support for NOT ENFORCED in CHECK constraints.
This added basic infrastructure for the NOT ENFORCED/ENFORCED
constraint supports where incluing grammar and catalog flags.
Note that CHECK constraints do not currently support ALTER operations,
so changing the enforceability of an existing constraint isn't
possible without dropping and recreating it. We could consider
adding support for altering CHECK constraints either in this
patch series or as a separatly.
---
doc/src/sgml/catalogs.sgml | 10 +++
doc/src/sgml/ddl.sgml | 8 +-
doc/src/sgml/ref/alter_table.sgml | 14 +--
doc/src/sgml/ref/create_table.sgml | 18 +++-
src/backend/access/common/tupdesc.c | 2 +
src/backend/catalog/heap.c | 13 ++-
src/backend/catalog/index.c | 1 +
src/backend/catalog/pg_constraint.c | 5 ++
src/backend/commands/tablecmds.c | 49 +++++++----
src/backend/commands/trigger.c | 1 +
src/backend/commands/typecmds.c | 16 ++++
src/backend/executor/execMain.c | 8 +-
src/backend/nodes/makefuncs.c | 1 +
src/backend/parser/gram.y | 88 +++++++++++++++----
src/backend/parser/parse_utilcmd.c | 40 +++++++++
src/backend/utils/adt/ruleutils.c | 2 +
src/backend/utils/cache/relcache.c | 1 +
src/include/access/tupdesc.h | 1 +
src/include/catalog/heap.h | 1 +
src/include/catalog/pg_constraint.h | 2 +
src/include/nodes/parsenodes.h | 3 +
src/include/parser/kwlist.h | 1 +
src/test/regress/expected/constraints.out | 49 ++++++++++-
.../regress/expected/create_table_like.out | 10 ++-
src/test/regress/sql/constraints.sql | 28 +++++-
src/test/regress/sql/create_table_like.sql | 3 +-
26 files changed, 320 insertions(+), 55 deletions(-)
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 59bb833f48d..6357771dc57 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2603,6 +2603,16 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l
</para></entry>
</row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>conenforced</structfield> <type>bool</type>
+ </para>
+ <para>
+ Is the constraint enforceable?
+ Currently, can be false only for CHECK constraints
+ </para></entry>
+ </row>
+
<row>
<entry role="catalog_table_entry"><para role="column_definition">
<structfield>conrelid</structfield> <type>oid</type>
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index 3c56610d2ac..9e2708586d5 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -584,7 +584,10 @@ CREATE TABLE products (
consists of the key word <literal>CHECK</literal> followed by an
expression in parentheses. The check constraint expression should
involve the column thus constrained, otherwise the constraint
- would not make too much sense.
+ would not make too much sense. The check constraint expression can be
+ followed by the <literal>ENFORCED</literal>, which is the default, or
+ <literal>NOT ENFORCED</literal> keyword, determining whether the
+ constraint check is performed or skipped after each statement.
</para>
<indexterm>
@@ -720,7 +723,8 @@ CREATE TABLE products (
to implement that. (This approach avoids the dump/restore problem because
<application>pg_dump</application> does not reinstall triggers until after
restoring data, so that the check will not be enforced during a
- dump/restore.)
+ dump/restore.) You can add that CHECK constraint with <literal>NOT ENFORCED</literal>,
+ allowing the underlying constraint rule to be represented without being enforced.
</para>
</note>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index c8f7ab7d956..ca38937cf1e 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -58,7 +58,7 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable>
ALTER [ COLUMN ] <replaceable class="parameter">column_name</replaceable> SET COMPRESSION <replaceable class="parameter">compression_method</replaceable>
ADD <replaceable class="parameter">table_constraint</replaceable> [ NOT VALID ]
ADD <replaceable class="parameter">table_constraint_using_index</replaceable>
- ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+ ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]
VALIDATE CONSTRAINT <replaceable class="parameter">constraint_name</replaceable>
DROP CONSTRAINT [ IF EXISTS ] <replaceable class="parameter">constraint_name</replaceable> [ RESTRICT | CASCADE ]
DISABLE TRIGGER [ <replaceable class="parameter">trigger_name</replaceable> | ALL | USER ]
@@ -108,7 +108,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
PRIMARY KEY <replaceable class="parameter">index_parameters</replaceable> |
REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> ) ] [ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ]
[ ON DELETE <replaceable class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ] }
-[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]
<phrase>and <replaceable class="parameter">table_constraint</replaceable> is:</phrase>
@@ -120,7 +120,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
EXCLUDE [ USING <replaceable class="parameter">index_method</replaceable> ] ( <replaceable class="parameter">exclude_element</replaceable> WITH <replaceable class="parameter">operator</replaceable> [, ... ] ) <replaceable class="parameter">index_parameters</replaceable> [ WHERE ( <replaceable class="parameter">predicate</replaceable> ) ] |
FOREIGN KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">column_name</replaceable> ] ) REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">refcolumn</replaceable> ] ) ]
[ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ] [ ON DELETE <replaceable class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ] }
-[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]
<phrase>and <replaceable class="parameter">table_constraint_using_index</replaceable> is:</phrase>
@@ -1423,9 +1423,11 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</para>
<para>
- Adding a <literal>CHECK</literal> or <literal>NOT NULL</literal> constraint requires
- scanning the table to verify that existing rows meet the constraint,
- but does not require a table rewrite.
+ Adding an enforced <literal>CHECK</literal> or <literal>NOT NULL</literal>
+ constraint requires scanning the table to verify that existing rows meet the
+ constraint, but does not require a table rewrite. If a <literal>CHECK</literal>
+ constraint is added as <literal>NOT ENFORCED</literal>, the validation will
+ not be performed.
</para>
<para>
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index 6262533c57b..97c9af44f50 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -71,7 +71,7 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI
PRIMARY KEY <replaceable class="parameter">index_parameters</replaceable> |
REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> ) ] [ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ]
[ ON DELETE <replaceable class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ] }
-[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]
<phrase>and <replaceable class="parameter">table_constraint</replaceable> is:</phrase>
@@ -84,7 +84,7 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI
FOREIGN KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">column_name</replaceable> ] ) REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">refcolumn</replaceable> ] ) ]
[ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ] [ ON DELETE <replaceable
class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ] }
-[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]
<phrase>and <replaceable class="parameter">like_option</replaceable> is:</phrase>
@@ -1368,6 +1368,20 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</listitem>
</varlistentry>
+ <varlistentry id="sql-createtable-parms-enforce">
+ <term><literal>ENFORCED</literal></term>
+ <term><literal>NOT ENFORCED</literal></term>
+ <listitem>
+ <para>
+ This is currently only allowed for <literal>CHECK</literal> constraints.
+ If the constraint is <literal>NOT ENFORCED</literal>, this clause
+ specifies that the constraint check will be skipped. When the constraint
+ is <literal>ENFORCED</literal>, check is performed after each statement.
+ This is the default.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="sql-createtable-method">
<term><literal>USING <replaceable class="parameter">method</replaceable></literal></term>
<listitem>
diff --git a/src/backend/access/common/tupdesc.c b/src/backend/access/common/tupdesc.c
index 47379fef220..9d118dad4fe 100644
--- a/src/backend/access/common/tupdesc.c
+++ b/src/backend/access/common/tupdesc.c
@@ -225,6 +225,7 @@ CreateTupleDescCopyConstr(TupleDesc tupdesc)
cpy->check[i].ccname = pstrdup(constr->check[i].ccname);
cpy->check[i].ccbin = pstrdup(constr->check[i].ccbin);
cpy->check[i].ccvalid = constr->check[i].ccvalid;
+ cpy->check[i].ccenforced = constr->check[i].ccenforced;
cpy->check[i].ccnoinherit = constr->check[i].ccnoinherit;
}
}
@@ -548,6 +549,7 @@ equalTupleDescs(TupleDesc tupdesc1, TupleDesc tupdesc2)
if (!(strcmp(check1->ccname, check2->ccname) == 0 &&
strcmp(check1->ccbin, check2->ccbin) == 0 &&
check1->ccvalid == check2->ccvalid &&
+ check1->ccenforced == check2->ccenforced &&
check1->ccnoinherit == check2->ccnoinherit))
return false;
}
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 003af4bf21c..9e531232be0 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -103,7 +103,7 @@ static ObjectAddress AddNewRelationType(const char *typeName,
static void RelationRemoveInheritance(Oid relid);
static Oid StoreRelCheck(Relation rel, const char *ccname, Node *expr,
bool is_validated, bool is_local, int16 inhcount,
- bool is_no_inherit, bool is_internal);
+ bool is_no_inherit, bool is_enforced, bool is_internal);
static void StoreConstraints(Relation rel, List *cooked_constraints,
bool is_internal);
static bool MergeWithExistingConstraint(Relation rel, const char *ccname, Node *expr,
@@ -2075,7 +2075,7 @@ SetAttrMissing(Oid relid, char *attname, char *value)
static Oid
StoreRelCheck(Relation rel, const char *ccname, Node *expr,
bool is_validated, bool is_local, int16 inhcount,
- bool is_no_inherit, bool is_internal)
+ bool is_no_inherit, bool is_enforced, bool is_internal)
{
char *ccbin;
List *varList;
@@ -2141,6 +2141,7 @@ StoreRelCheck(Relation rel, const char *ccname, Node *expr,
false, /* Is Deferrable */
false, /* Is Deferred */
is_validated,
+ is_enforced, /* Is Enforced */
InvalidOid, /* no parent constraint */
RelationGetRelid(rel), /* relation */
attNos, /* attrs in the constraint */
@@ -2194,6 +2195,7 @@ StoreRelNotNull(Relation rel, const char *nnname, AttrNumber attnum,
false,
false,
is_validated,
+ true, /* Is Enforced */
InvalidOid,
RelationGetRelid(rel),
&attnum,
@@ -2264,7 +2266,7 @@ StoreConstraints(Relation rel, List *cooked_constraints, bool is_internal)
StoreRelCheck(rel, con->name, con->expr,
!con->skip_validation, con->is_local,
con->inhcount, con->is_no_inherit,
- is_internal);
+ con->is_enforced, is_internal);
numchecks++;
break;
@@ -2406,6 +2408,7 @@ AddRelationNewConstraints(Relation rel,
cooked->attnum = colDef->attnum;
cooked->expr = expr;
cooked->skip_validation = false;
+ cooked->is_enforced = true;
cooked->is_local = is_local;
cooked->inhcount = is_local ? 0 : 1;
cooked->is_no_inherit = false;
@@ -2525,7 +2528,8 @@ AddRelationNewConstraints(Relation rel,
*/
constrOid =
StoreRelCheck(rel, ccname, expr, cdef->initially_valid, is_local,
- is_local ? 0 : 1, cdef->is_no_inherit, is_internal);
+ is_local ? 0 : 1, cdef->is_no_inherit,
+ cdef->is_enforced, is_internal);
numchecks++;
@@ -2536,6 +2540,7 @@ AddRelationNewConstraints(Relation rel,
cooked->attnum = 0;
cooked->expr = expr;
cooked->skip_validation = cdef->skip_validation;
+ cooked->is_enforced = cdef->is_enforced;
cooked->is_local = is_local;
cooked->inhcount = is_local ? 0 : 1;
cooked->is_no_inherit = cdef->is_no_inherit;
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index f9bb721c5fe..92455fc2832 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -1958,6 +1958,7 @@ index_constraint_create(Relation heapRelation,
deferrable,
initdeferred,
true,
+ true, /* Is Enforced */
parentConstraintId,
RelationGetRelid(heapRelation),
indexInfo->ii_IndexAttrNumbers,
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index 9c05a98d28c..f83167a8a69 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -54,6 +54,7 @@ CreateConstraintEntry(const char *constraintName,
bool isDeferrable,
bool isDeferred,
bool isValidated,
+ bool isEnforced,
Oid parentConstrId,
Oid relId,
const int16 *constraintKey,
@@ -99,6 +100,9 @@ CreateConstraintEntry(const char *constraintName,
ObjectAddresses *addrs_auto;
ObjectAddresses *addrs_normal;
+ /* Only CHECK constraint can be not enforced */
+ Assert(isEnforced || constraintType == CONSTRAINT_CHECK);
+
conDesc = table_open(ConstraintRelationId, RowExclusiveLock);
Assert(constraintName);
@@ -183,6 +187,7 @@ CreateConstraintEntry(const char *constraintName,
values[Anum_pg_constraint_condeferrable - 1] = BoolGetDatum(isDeferrable);
values[Anum_pg_constraint_condeferred - 1] = BoolGetDatum(isDeferred);
values[Anum_pg_constraint_convalidated - 1] = BoolGetDatum(isValidated);
+ values[Anum_pg_constraint_conenforced - 1] = BoolGetDatum(isEnforced);
values[Anum_pg_constraint_conrelid - 1] = ObjectIdGetDatum(relId);
values[Anum_pg_constraint_contypid - 1] = ObjectIdGetDatum(domainId);
values[Anum_pg_constraint_conindid - 1] = ObjectIdGetDatum(indexRelId);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index ccd9645e7d2..524c1e5f40b 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -373,7 +373,7 @@ static void RangeVarCallbackForTruncate(const RangeVar *relation,
static List *MergeAttributes(List *columns, const List *supers, char relpersistence,
bool is_partition, List **supconstr,
List **supnotnulls);
-static List *MergeCheckConstraint(List *constraints, const char *name, Node *expr);
+static List *MergeCheckConstraint(List *constraints, const char *name, Node *expr, bool is_enforced);
static void MergeChildAttribute(List *inh_columns, int exist_attno, int newcol_attno, const ColumnDef *newdef);
static ColumnDef *MergeInheritedAttribute(List *inh_columns, int exist_attno, const ColumnDef *newdef);
static void MergeAttributesIntoExisting(Relation child_rel, Relation parent_rel, bool ispartition);
@@ -2885,7 +2885,8 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
name,
RelationGetRelationName(relation))));
- constraints = MergeCheckConstraint(constraints, name, expr);
+ constraints = MergeCheckConstraint(constraints, name, expr,
+ check[i].ccenforced);
}
}
@@ -3099,7 +3100,7 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
* the list.
*/
static List *
-MergeCheckConstraint(List *constraints, const char *name, Node *expr)
+MergeCheckConstraint(List *constraints, const char *name, Node *expr, bool is_enforced)
{
ListCell *lc;
CookedConstraint *newcon;
@@ -3140,6 +3141,7 @@ MergeCheckConstraint(List *constraints, const char *name, Node *expr)
newcon->name = pstrdup(name);
newcon->expr = expr;
newcon->inhcount = 1;
+ newcon->is_enforced = is_enforced;
return lappend(constraints, newcon);
}
@@ -10371,6 +10373,7 @@ addFkConstraint(addFkConstraintSides fkside,
fkconstraint->deferrable,
fkconstraint->initdeferred,
fkconstraint->initially_valid,
+ true, /* Is Enforced */
parentConstr,
RelationGetRelid(rel),
fkattnum,
@@ -12040,22 +12043,29 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
table_close(childrel, NoLock);
}
- /* Queue validation for phase 3 */
- newcon = (NewConstraint *) palloc0(sizeof(NewConstraint));
- newcon->name = constrName;
- newcon->contype = CONSTR_CHECK;
- newcon->refrelid = InvalidOid;
- newcon->refindid = InvalidOid;
- newcon->conid = con->oid;
+ /*
+ * Queue validation for phase 3 only if constraint is enforced;
+ * otherwise, adding it to the validation queue won't be very
+ * effective, as the verification will be skipped.
+ */
+ if (con->conenforced)
+ {
+ newcon = (NewConstraint *) palloc0(sizeof(NewConstraint));
+ newcon->name = constrName;
+ newcon->contype = CONSTR_CHECK;
+ newcon->refrelid = InvalidOid;
+ newcon->refindid = InvalidOid;
+ newcon->conid = con->oid;
- val = SysCacheGetAttrNotNull(CONSTROID, tuple,
- Anum_pg_constraint_conbin);
- conbin = TextDatumGetCString(val);
- newcon->qual = (Node *) stringToNode(conbin);
+ val = SysCacheGetAttrNotNull(CONSTROID, tuple,
+ Anum_pg_constraint_conbin);
+ conbin = TextDatumGetCString(val);
+ newcon->qual = (Node *) stringToNode(conbin);
- /* Find or create work queue entry for this table */
- tab = ATGetQueueEntry(wqueue, rel);
- tab->constraints = lappend(tab->constraints, newcon);
+ /* Find or create work queue entry for this table */
+ tab = ATGetQueueEntry(wqueue, rel);
+ tab->constraints = lappend(tab->constraints, newcon);
+ }
/*
* Invalidate relcache so that others see the new validated
@@ -12065,7 +12075,8 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
}
/*
- * Now update the catalog, while we have the door open.
+ * Update the catalog regardless of enforcement; validation will occur
+ * only when the constraint is enforced.
*/
copyTuple = heap_copytuple(tuple);
copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
@@ -16206,6 +16217,7 @@ constraints_equivalent(HeapTuple a, HeapTuple b, TupleDesc tupleDesc)
if (acon->condeferrable != bcon->condeferrable ||
acon->condeferred != bcon->condeferred ||
+ acon->conenforced != bcon->conenforced ||
strcmp(decompile_conbin(a, tupleDesc),
decompile_conbin(b, tupleDesc)) != 0)
return false;
@@ -20129,6 +20141,7 @@ DetachAddConstraintIfNeeded(List **wqueue, Relation partRel)
n->cooked_expr = nodeToString(make_ands_explicit(constraintExpr));
n->initially_valid = true;
n->skip_validation = true;
+ n->is_enforced = true;
/* It's a re-add, since it nominally already exists */
ATAddCheckNNConstraint(wqueue, tab, partRel, n,
true, false, true, ShareUpdateExclusiveLock);
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 09356e46d16..25b0e612b27 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -810,6 +810,7 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
stmt->deferrable,
stmt->initdeferred,
true,
+ true, /* Is Enforced */
InvalidOid, /* no parent */
RelationGetRelid(rel),
NULL, /* no conkey */
diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c
index 859e2191f08..971aa0f4f5a 100644
--- a/src/backend/commands/typecmds.c
+++ b/src/backend/commands/typecmds.c
@@ -1011,6 +1011,13 @@ DefineDomain(CreateDomainStmt *stmt)
errmsg("specifying constraint deferrability not supported for domains")));
break;
+ case CONSTR_ATTR_ENFORCED:
+ case CONSTR_ATTR_NOT_ENFORCED:
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("specifying constraint enforceability not supported for domains")));
+ break;
+
default:
elog(ERROR, "unrecognized constraint subtype: %d",
(int) constr->contype);
@@ -2971,6 +2978,13 @@ AlterDomainAddConstraint(List *names, Node *newConstraint,
errmsg("specifying constraint deferrability not supported for domains")));
break;
+ case CONSTR_ATTR_ENFORCED:
+ case CONSTR_ATTR_NOT_ENFORCED:
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("specifying constraint enforceability not supported for domains")));
+ break;
+
default:
elog(ERROR, "unrecognized constraint subtype: %d",
(int) constr->contype);
@@ -3601,6 +3615,7 @@ domainAddCheckConstraint(Oid domainOid, Oid domainNamespace, Oid baseTypeOid,
false, /* Is Deferrable */
false, /* Is Deferred */
!constr->skip_validation, /* Is Validated */
+ true, /* Is Enforced */
InvalidOid, /* no parent constraint */
InvalidOid, /* not a relation constraint */
NULL,
@@ -3708,6 +3723,7 @@ domainAddNotNullConstraint(Oid domainOid, Oid domainNamespace, Oid baseTypeOid,
false, /* Is Deferrable */
false, /* Is Deferred */
!constr->skip_validation, /* Is Validated */
+ true, /* Is Enforced */
InvalidOid, /* no parent constraint */
InvalidOid, /* not a relation constraint */
NULL,
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 5ca856fd279..1878f4b8d94 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1751,11 +1751,15 @@ ExecRelCheck(ResultRelInfo *resultRelInfo,
{
oldContext = MemoryContextSwitchTo(estate->es_query_cxt);
resultRelInfo->ri_ConstraintExprs =
- (ExprState **) palloc(ncheck * sizeof(ExprState *));
+ (ExprState **) palloc0(ncheck * sizeof(ExprState *));
for (i = 0; i < ncheck; i++)
{
Expr *checkconstr;
+ /* Skip not enforced constraint */
+ if (!check[i].ccenforced)
+ continue;
+
checkconstr = stringToNode(check[i].ccbin);
resultRelInfo->ri_ConstraintExprs[i] =
ExecPrepareExpr(checkconstr, estate);
@@ -1782,7 +1786,7 @@ ExecRelCheck(ResultRelInfo *resultRelInfo,
* is not to be treated as a failure. Therefore, use ExecCheck not
* ExecQual.
*/
- if (!ExecCheck(checkconstr, econtext))
+ if (checkconstr && !ExecCheck(checkconstr, econtext))
return check[i].ccname;
}
diff --git a/src/backend/nodes/makefuncs.c b/src/backend/nodes/makefuncs.c
index 7e5df7bea4d..72e6a0e89be 100644
--- a/src/backend/nodes/makefuncs.c
+++ b/src/backend/nodes/makefuncs.c
@@ -455,6 +455,7 @@ makeNotNullConstraint(String *colname)
notnull->keys = list_make1(colname);
notnull->skip_validation = false;
notnull->initially_valid = true;
+ notnull->is_enforced = true;
return notnull;
}
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 67eb96396af..062b7202a1d 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -143,6 +143,8 @@ typedef struct KeyActions
#define CAS_INITIALLY_DEFERRED 0x08
#define CAS_NOT_VALID 0x10
#define CAS_NO_INHERIT 0x20
+#define CAS_NOT_ENFORCED 0x40
+#define CAS_ENFORCED 0x80
#define parser_yyerror(msg) scanner_yyerror(msg, yyscanner)
@@ -197,7 +199,7 @@ static void SplitColQualList(List *qualList,
core_yyscan_t yyscanner);
static void processCASbits(int cas_bits, int location, const char *constrType,
bool *deferrable, bool *initdeferred, bool *not_valid,
- bool *no_inherit, core_yyscan_t yyscanner);
+ bool *no_inherit, bool *is_enforced, core_yyscan_t yyscanner);
static PartitionStrategy parsePartitionStrategy(char *strategy, int location,
core_yyscan_t yyscanner);
static void preprocess_pubobj_list(List *pubobjspec_list,
@@ -711,9 +713,9 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
DETACH DICTIONARY DISABLE_P DISCARD DISTINCT DO DOCUMENT_P DOMAIN_P
DOUBLE_P DROP
- EACH ELSE EMPTY_P ENABLE_P ENCODING ENCRYPTED END_P ENUM_P ERROR_P ESCAPE
- EVENT EXCEPT EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION
- EXTENSION EXTERNAL EXTRACT
+ EACH ELSE EMPTY_P ENABLE_P ENCODING ENCRYPTED END_P ENFORCED ENUM_P ERROR_P
+ ESCAPE EVENT EXCEPT EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN
+ EXPRESSION EXTENSION EXTERNAL EXTRACT
FALSE_P FAMILY FETCH FILTER FINALIZE FIRST_P FLOAT_P FOLLOWING FOR
FORCE FOREIGN FORMAT FORWARD FREEZE FROM FULL FUNCTION FUNCTIONS
@@ -2658,7 +2660,7 @@ alter_table_cmd:
processCASbits($4, @4, "ALTER CONSTRAINT statement",
&c->deferrable,
&c->initdeferred,
- NULL, NULL, yyscanner);
+ NULL, NULL, &c->is_enforced, yyscanner);
$$ = (Node *) n;
}
/* ALTER TABLE <name> VALIDATE CONSTRAINT ... */
@@ -3963,6 +3965,7 @@ ColConstraintElem:
n->cooked_expr = NULL;
n->skip_validation = false;
n->initially_valid = true;
+ n->is_enforced = true;
$$ = (Node *) n;
}
| DEFAULT b_expr
@@ -4087,6 +4090,22 @@ ConstraintAttr:
n->location = @1;
$$ = (Node *) n;
}
+ | ENFORCED
+ {
+ Constraint *n = makeNode(Constraint);
+
+ n->contype = CONSTR_ATTR_ENFORCED;
+ n->location = @1;
+ $$ = (Node *) n;
+ }
+ | NOT ENFORCED
+ {
+ Constraint *n = makeNode(Constraint);
+
+ n->contype = CONSTR_ATTR_NOT_ENFORCED;
+ n->location = @1;
+ $$ = (Node *) n;
+ }
;
@@ -4149,7 +4168,7 @@ ConstraintElem:
n->cooked_expr = NULL;
processCASbits($5, @5, "CHECK",
NULL, NULL, &n->skip_validation,
- &n->is_no_inherit, yyscanner);
+ &n->is_no_inherit, &n->is_enforced, yyscanner);
n->initially_valid = !n->skip_validation;
$$ = (Node *) n;
}
@@ -4163,7 +4182,7 @@ ConstraintElem:
/* no NOT VALID support yet */
processCASbits($4, @4, "NOT NULL",
NULL, NULL, NULL,
- &n->is_no_inherit, yyscanner);
+ &n->is_no_inherit, NULL, yyscanner);
n->initially_valid = true;
$$ = (Node *) n;
}
@@ -4183,7 +4202,7 @@ ConstraintElem:
n->indexspace = $9;
processCASbits($10, @10, "UNIQUE",
&n->deferrable, &n->initdeferred, NULL,
- NULL, yyscanner);
+ NULL, NULL, yyscanner);
$$ = (Node *) n;
}
| UNIQUE ExistingIndex ConstraintAttributeSpec
@@ -4199,7 +4218,7 @@ ConstraintElem:
n->indexspace = NULL;
processCASbits($3, @3, "UNIQUE",
&n->deferrable, &n->initdeferred, NULL,
- NULL, yyscanner);
+ NULL, NULL, yyscanner);
$$ = (Node *) n;
}
| PRIMARY KEY '(' columnList opt_without_overlaps ')' opt_c_include opt_definition OptConsTableSpace
@@ -4217,7 +4236,7 @@ ConstraintElem:
n->indexspace = $9;
processCASbits($10, @10, "PRIMARY KEY",
&n->deferrable, &n->initdeferred, NULL,
- NULL, yyscanner);
+ NULL, NULL, yyscanner);
$$ = (Node *) n;
}
| PRIMARY KEY ExistingIndex ConstraintAttributeSpec
@@ -4233,7 +4252,7 @@ ConstraintElem:
n->indexspace = NULL;
processCASbits($4, @4, "PRIMARY KEY",
&n->deferrable, &n->initdeferred, NULL,
- NULL, yyscanner);
+ NULL, NULL, yyscanner);
$$ = (Node *) n;
}
| EXCLUDE access_method_clause '(' ExclusionConstraintList ')'
@@ -4253,7 +4272,7 @@ ConstraintElem:
n->where_clause = $9;
processCASbits($10, @10, "EXCLUDE",
&n->deferrable, &n->initdeferred, NULL,
- NULL, yyscanner);
+ NULL, NULL, yyscanner);
$$ = (Node *) n;
}
| FOREIGN KEY '(' columnList optionalPeriodName ')' REFERENCES qualified_name
@@ -4283,6 +4302,7 @@ ConstraintElem:
processCASbits($12, @12, "FOREIGN KEY",
&n->deferrable, &n->initdeferred,
&n->skip_validation, NULL,
+ NULL,
yyscanner);
n->initially_valid = !n->skip_validation;
$$ = (Node *) n;
@@ -4323,8 +4343,9 @@ DomainConstraintElem:
n->cooked_expr = NULL;
processCASbits($5, @5, "CHECK",
NULL, NULL, &n->skip_validation,
- &n->is_no_inherit, yyscanner);
+ &n->is_no_inherit, NULL, yyscanner);
n->initially_valid = !n->skip_validation;
+ n->is_enforced = true;
$$ = (Node *) n;
}
| NOT NULL_P ConstraintAttributeSpec
@@ -4337,7 +4358,7 @@ DomainConstraintElem:
/* no NOT VALID, NO INHERIT support */
processCASbits($3, @3, "NOT NULL",
NULL, NULL, NULL,
- NULL, yyscanner);
+ NULL, NULL, yyscanner);
n->initially_valid = true;
$$ = (Node *) n;
}
@@ -6000,7 +6021,7 @@ CreateTrigStmt:
n->transitionRels = NIL;
processCASbits($11, @11, "TRIGGER",
&n->deferrable, &n->initdeferred, NULL,
- NULL, yyscanner);
+ NULL, NULL, yyscanner);
n->constrrel = $10;
$$ = (Node *) n;
}
@@ -6169,7 +6190,8 @@ ConstraintAttributeSpec:
parser_errposition(@2)));
/* generic message for other conflicts */
if ((newspec & (CAS_NOT_DEFERRABLE | CAS_DEFERRABLE)) == (CAS_NOT_DEFERRABLE | CAS_DEFERRABLE) ||
- (newspec & (CAS_INITIALLY_IMMEDIATE | CAS_INITIALLY_DEFERRED)) == (CAS_INITIALLY_IMMEDIATE | CAS_INITIALLY_DEFERRED))
+ (newspec & (CAS_INITIALLY_IMMEDIATE | CAS_INITIALLY_DEFERRED)) == (CAS_INITIALLY_IMMEDIATE | CAS_INITIALLY_DEFERRED) ||
+ (newspec & (CAS_NOT_ENFORCED | CAS_ENFORCED)) == (CAS_NOT_ENFORCED | CAS_ENFORCED))
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("conflicting constraint properties"),
@@ -6185,6 +6207,8 @@ ConstraintAttributeElem:
| INITIALLY DEFERRED { $$ = CAS_INITIALLY_DEFERRED; }
| NOT VALID { $$ = CAS_NOT_VALID; }
| NO INHERIT { $$ = CAS_NO_INHERIT; }
+ | NOT ENFORCED { $$ = CAS_NOT_ENFORCED; }
+ | ENFORCED { $$ = CAS_ENFORCED; }
;
@@ -17688,6 +17712,7 @@ unreserved_keyword:
| ENABLE_P
| ENCODING
| ENCRYPTED
+ | ENFORCED
| ENUM_P
| ERROR_P
| ESCAPE
@@ -18265,6 +18290,7 @@ bare_label_keyword:
| ENCODING
| ENCRYPTED
| END_P
+ | ENFORCED
| ENUM_P
| ERROR_P
| ESCAPE
@@ -19404,7 +19430,7 @@ SplitColQualList(List *qualList,
static void
processCASbits(int cas_bits, int location, const char *constrType,
bool *deferrable, bool *initdeferred, bool *not_valid,
- bool *no_inherit, core_yyscan_t yyscanner)
+ bool *no_inherit, bool *is_enforced, core_yyscan_t yyscanner)
{
/* defaults */
if (deferrable)
@@ -19413,6 +19439,8 @@ processCASbits(int cas_bits, int location, const char *constrType,
*initdeferred = false;
if (not_valid)
*not_valid = false;
+ if (is_enforced)
+ *is_enforced = true;
if (cas_bits & (CAS_DEFERRABLE | CAS_INITIALLY_DEFERRED))
{
@@ -19465,6 +19493,32 @@ processCASbits(int cas_bits, int location, const char *constrType,
constrType),
parser_errposition(location)));
}
+
+ if (cas_bits & CAS_NOT_ENFORCED)
+ {
+ if (is_enforced)
+ *is_enforced = false;
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ /* translator: %s is CHECK, UNIQUE, or similar */
+ errmsg("%s constraints cannot be marked NOT ENFORCED",
+ constrType),
+ parser_errposition(location)));
+ }
+
+ if (cas_bits & CAS_ENFORCED)
+ {
+ if (is_enforced)
+ *is_enforced = true;
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ /* translator: %s is CHECK, UNIQUE, or similar */
+ errmsg("%s constraints cannot be marked ENFORCED",
+ constrType),
+ parser_errposition(location)));
+ }
}
/*
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 0f324ee4e31..183f845ba55 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -954,6 +954,8 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column)
case CONSTR_ATTR_NOT_DEFERRABLE:
case CONSTR_ATTR_DEFERRED:
case CONSTR_ATTR_IMMEDIATE:
+ case CONSTR_ATTR_ENFORCED:
+ case CONSTR_ATTR_NOT_ENFORCED:
/* transformConstraintAttrs took care of these */
break;
@@ -1093,6 +1095,8 @@ transformTableConstraint(CreateStmtContext *cxt, Constraint *constraint)
case CONSTR_ATTR_NOT_DEFERRABLE:
case CONSTR_ATTR_DEFERRED:
case CONSTR_ATTR_IMMEDIATE:
+ case CONSTR_ATTR_ENFORCED:
+ case CONSTR_ATTR_NOT_ENFORCED:
elog(ERROR, "invalid context for constraint type %d",
constraint->contype);
break;
@@ -1433,6 +1437,7 @@ expandTableLikeClause(RangeVar *heapRel, TableLikeClause *table_like_clause)
{
char *ccname = constr->check[ccnum].ccname;
char *ccbin = constr->check[ccnum].ccbin;
+ bool ccenforced = constr->check[ccnum].ccenforced;
bool ccnoinherit = constr->check[ccnum].ccnoinherit;
Node *ccbin_node;
bool found_whole_row;
@@ -1462,6 +1467,7 @@ expandTableLikeClause(RangeVar *heapRel, TableLikeClause *table_like_clause)
n->contype = CONSTR_CHECK;
n->conname = pstrdup(ccname);
n->location = -1;
+ n->is_enforced = ccenforced;
n->is_no_inherit = ccnoinherit;
n->raw_expr = NULL;
n->cooked_expr = nodeToString(ccbin_node);
@@ -3859,6 +3865,7 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
Constraint *lastprimarycon = NULL;
bool saw_deferrability = false;
bool saw_initially = false;
+ bool saw_enforced = false;
ListCell *clist;
#define SUPPORTS_ATTRS(node) \
@@ -3954,12 +3961,45 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
lastprimarycon->initdeferred = false;
break;
+ case CONSTR_ATTR_ENFORCED:
+ if (lastprimarycon &&
+ lastprimarycon->contype != CONSTR_CHECK)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("misplaced ENFORCED clause"),
+ parser_errposition(cxt->pstate, con->location)));
+ if (saw_enforced)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("multiple ENFORCED/NOT ENFORCED clauses not allowed"),
+ parser_errposition(cxt->pstate, con->location)));
+ saw_enforced = true;
+ lastprimarycon->is_enforced = true;
+ break;
+
+ case CONSTR_ATTR_NOT_ENFORCED:
+ if (lastprimarycon &&
+ lastprimarycon->contype != CONSTR_CHECK)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("misplaced NOT ENFORCED clause"),
+ parser_errposition(cxt->pstate, con->location)));
+ if (saw_enforced)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("multiple ENFORCED/NOT ENFORCED clauses not allowed"),
+ parser_errposition(cxt->pstate, con->location)));
+ saw_enforced = true;
+ lastprimarycon->is_enforced = false;
+ break;
+
default:
/* Otherwise it's not an attribute */
lastprimarycon = con;
/* reset flags for new primary node */
saw_deferrability = false;
saw_initially = false;
+ saw_enforced = false;
break;
}
}
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index a39068d1bf1..cecc48634e6 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -2591,6 +2591,8 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
appendStringInfoString(&buf, " DEFERRABLE");
if (conForm->condeferred)
appendStringInfoString(&buf, " INITIALLY DEFERRED");
+ if (!conForm->conenforced)
+ appendStringInfoString(&buf, " NOT ENFORCED");
if (!conForm->convalidated)
appendStringInfoString(&buf, " NOT VALID");
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 342467fd186..5d0233d56b2 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4587,6 +4587,7 @@ CheckConstraintFetch(Relation relation)
}
check[found].ccvalid = conform->convalidated;
+ check[found].ccenforced = conform->conenforced;
check[found].ccnoinherit = conform->connoinherit;
check[found].ccname = MemoryContextStrdup(CacheMemoryContext,
NameStr(conform->conname));
diff --git a/src/include/access/tupdesc.h b/src/include/access/tupdesc.h
index 8930a28d660..96bac018083 100644
--- a/src/include/access/tupdesc.h
+++ b/src/include/access/tupdesc.h
@@ -30,6 +30,7 @@ typedef struct ConstrCheck
char *ccname;
char *ccbin; /* nodeToString representation of expr */
bool ccvalid;
+ bool ccenforced;
bool ccnoinherit; /* this is a non-inheritable constraint */
} ConstrCheck;
diff --git a/src/include/catalog/heap.h b/src/include/catalog/heap.h
index 8c278f202b4..3eaf4e96cef 100644
--- a/src/include/catalog/heap.h
+++ b/src/include/catalog/heap.h
@@ -41,6 +41,7 @@ typedef struct CookedConstraint
AttrNumber attnum; /* which attr (only for NOTNULL, DEFAULT) */
Node *expr; /* transformed default or check expr */
bool skip_validation; /* skip validation? (only for CHECK) */
+ bool is_enforced; /* is enforced? (only for CHECK) */
bool is_local; /* constraint has local (non-inherited) def */
int16 inhcount; /* number of times constraint is inherited */
bool is_no_inherit; /* constraint has local def and cannot be
diff --git a/src/include/catalog/pg_constraint.h b/src/include/catalog/pg_constraint.h
index 4b4476738a2..9a74ad72f23 100644
--- a/src/include/catalog/pg_constraint.h
+++ b/src/include/catalog/pg_constraint.h
@@ -52,6 +52,7 @@ CATALOG(pg_constraint,2606,ConstraintRelationId)
bool condeferrable; /* deferrable constraint? */
bool condeferred; /* deferred by default? */
bool convalidated; /* constraint has been validated? */
+ bool conenforced; /* enforced constraint? */
/*
* conrelid and conkey are only meaningful if the constraint applies to a
@@ -223,6 +224,7 @@ extern Oid CreateConstraintEntry(const char *constraintName,
bool isDeferrable,
bool isDeferred,
bool isValidated,
+ bool isEnforced,
Oid parentConstrId,
Oid relId,
const int16 *constraintKey,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 0f9462493e3..cad403fbfb5 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2736,6 +2736,8 @@ typedef enum ConstrType /* types of constraints */
CONSTR_ATTR_NOT_DEFERRABLE,
CONSTR_ATTR_DEFERRED,
CONSTR_ATTR_IMMEDIATE,
+ CONSTR_ATTR_ENFORCED,
+ CONSTR_ATTR_NOT_ENFORCED,
} ConstrType;
/* Foreign key action codes */
@@ -2759,6 +2761,7 @@ typedef struct Constraint
bool initdeferred; /* INITIALLY DEFERRED? */
bool skip_validation; /* skip validation of existing rows? */
bool initially_valid; /* mark the new constraint as valid? */
+ bool is_enforced; /* enforced constraint? */
bool is_no_inherit; /* is constraint non-inheritable? */
Node *raw_expr; /* CHECK or DEFAULT expression, as
* untransformed parse tree */
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 899d64ad55f..f2bc378394e 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -153,6 +153,7 @@ PG_KEYWORD("enable", ENABLE_P, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("encoding", ENCODING, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("encrypted", ENCRYPTED, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("end", END_P, RESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("enforced", ENFORCED, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("enum", ENUM_P, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("error", ERROR_P, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("escape", ESCAPE, UNRESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/test/regress/expected/constraints.out b/src/test/regress/expected/constraints.out
index 71200c90ed3..1fe2a76fa34 100644
--- a/src/test/regress/expected/constraints.out
+++ b/src/test/regress/expected/constraints.out
@@ -87,6 +87,25 @@ SELECT * FROM CHECK_TBL;
6
(3 rows)
+CREATE TABLE NE_CHECK_TBL (x int,
+ CONSTRAINT CHECK_CON CHECK (x > 3) NOT ENFORCED);
+INSERT INTO NE_CHECK_TBL VALUES (5);
+INSERT INTO NE_CHECK_TBL VALUES (4);
+INSERT INTO NE_CHECK_TBL VALUES (3);
+INSERT INTO NE_CHECK_TBL VALUES (2);
+INSERT INTO NE_CHECK_TBL VALUES (6);
+INSERT INTO NE_CHECK_TBL VALUES (1);
+SELECT * FROM NE_CHECK_TBL;
+ x
+---
+ 5
+ 4
+ 3
+ 2
+ 6
+ 1
+(6 rows)
+
CREATE SEQUENCE CHECK_SEQ;
CREATE TABLE CHECK2_TBL (x int, y text, z int,
CONSTRAINT SEQUENCE_CON
@@ -120,7 +139,8 @@ CREATE TABLE INSERT_TBL (x INT DEFAULT nextval('insert_seq'),
y TEXT DEFAULT '-NULL-',
z INT DEFAULT -1 * currval('insert_seq'),
CONSTRAINT INSERT_TBL_CON CHECK (x >= 3 AND y <> 'check failed' AND x < 8),
- CHECK (x + z = 0));
+ CHECK (x + z = 0) ENFORCED, /* no change it is a default */
+ CONSTRAINT NE_INSERT_TBL_CON CHECK (x + z = 1) NOT ENFORCED);
INSERT INTO INSERT_TBL(x,z) VALUES (2, -2);
ERROR: new row for relation "insert_tbl" violates check constraint "insert_tbl_con"
DETAIL: Failing row contains (2, -NULL-, -2).
@@ -715,6 +735,20 @@ SELECT * FROM unique_tbl;
3 | threex
(5 rows)
+-- enforcibility cannot be specified or set for unique constrain
+CREATE TABLE UNIQUE_EN_TBL(i int UNIQUE ENFORCED);
+ERROR: misplaced ENFORCED clause
+LINE 1: CREATE TABLE UNIQUE_EN_TBL(i int UNIQUE ENFORCED);
+ ^
+CREATE TABLE UNIQUE_NOTEN_TBL(i int UNIQUE NOT ENFORCED);
+ERROR: misplaced NOT ENFORCED clause
+LINE 1: CREATE TABLE UNIQUE_NOTEN_TBL(i int UNIQUE NOT ENFORCED);
+ ^
+-- XXX: error message is misleading here
+ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
+ERROR: constraint "unique_tbl_i_key" of relation "unique_tbl" is not a foreign key constraint
+ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORCED;
+ERROR: constraint "unique_tbl_i_key" of relation "unique_tbl" is not a foreign key constraint
DROP TABLE unique_tbl;
--
-- EXCLUDE constraints
@@ -1351,6 +1385,19 @@ ERROR: must be owner of relation constraint_comments_tbl
COMMENT ON CONSTRAINT the_constraint ON DOMAIN constraint_comments_dom IS 'no, another comment';
ERROR: must be owner of type constraint_comments_dom
RESET SESSION AUTHORIZATION;
+-- enforceability not allowed
+CREATE DOMAIN constraint_enforced_dom AS int CONSTRAINT the_constraint CHECK (value > 0) ENFORCED;
+ERROR: specifying constraint enforceability not supported for domains
+CREATE DOMAIN constraint_not_enforced_dom AS int CONSTRAINT the_constraint CHECK (value > 0) NOT ENFORCED;
+ERROR: specifying constraint enforceability not supported for domains
+ALTER DOMAIN constraint_comments_dom ADD CONSTRAINT the_constraint CHECK (value > 0) ENFORCED;
+ERROR: CHECK constraints cannot be marked ENFORCED
+LINE 1: ...om ADD CONSTRAINT the_constraint CHECK (value > 0) ENFORCED;
+ ^
+ALTER DOMAIN constraint_comments_dom ADD CONSTRAINT the_constraint CHECK (value > 0) NOT ENFORCED;
+ERROR: CHECK constraints cannot be marked NOT ENFORCED
+LINE 1: ...m ADD CONSTRAINT the_constraint CHECK (value > 0) NOT ENFORC...
+ ^
DROP TABLE constraint_comments_tbl;
DROP DOMAIN constraint_comments_dom;
DROP ROLE regress_constraint_comments;
diff --git a/src/test/regress/expected/create_table_like.out b/src/test/regress/expected/create_table_like.out
index d091da5a1ef..e0613891351 100644
--- a/src/test/regress/expected/create_table_like.out
+++ b/src/test/regress/expected/create_table_like.out
@@ -315,7 +315,8 @@ Referenced by:
DROP TABLE inhz;
-- including storage and comments
-CREATE TABLE ctlt1 (a text CHECK (length(a) > 2) PRIMARY KEY, b text);
+CREATE TABLE ctlt1 (a text CHECK (length(a) > 2) ENFORCED PRIMARY KEY,
+ b text CHECK (length(b) > 100) NOT ENFORCED);
CREATE INDEX ctlt1_b_key ON ctlt1 (b);
CREATE INDEX ctlt1_fnidx ON ctlt1 ((a || b));
CREATE STATISTICS ctlt1_a_b_stat ON a,b FROM ctlt1;
@@ -366,6 +367,7 @@ CREATE TABLE ctlt1_inh (LIKE ctlt1 INCLUDING CONSTRAINTS INCLUDING COMMENTS) INH
NOTICE: merging column "a" with inherited definition
NOTICE: merging column "b" with inherited definition
NOTICE: merging constraint "ctlt1_a_check" with inherited definition
+NOTICE: merging constraint "ctlt1_b_check" with inherited definition
\d+ ctlt1_inh
Table "public.ctlt1_inh"
Column | Type | Collation | Nullable | Default | Storage | Stats target | Description
@@ -374,6 +376,7 @@ NOTICE: merging constraint "ctlt1_a_check" with inherited definition
b | text | | | | extended | | B
Check constraints:
"ctlt1_a_check" CHECK (length(a) > 2)
+ "ctlt1_b_check" CHECK (length(b) > 100) NOT ENFORCED
Not-null constraints:
"ctlt1_a_not_null" NOT NULL "a" (local, inherited)
Inherits: ctlt1
@@ -395,6 +398,7 @@ NOTICE: merging multiple inherited definitions of column "a"
c | text | | | | external | |
Check constraints:
"ctlt1_a_check" CHECK (length(a) > 2)
+ "ctlt1_b_check" CHECK (length(b) > 100) NOT ENFORCED
"ctlt3_a_check" CHECK (length(a) < 5)
"ctlt3_c_check" CHECK (length(c) < 7)
Not-null constraints:
@@ -415,6 +419,7 @@ Indexes:
"ctlt13_like_expr_idx" btree ((a || c))
Check constraints:
"ctlt1_a_check" CHECK (length(a) > 2)
+ "ctlt1_b_check" CHECK (length(b) > 100) NOT ENFORCED
"ctlt3_a_check" CHECK (length(a) < 5)
"ctlt3_c_check" CHECK (length(c) < 7)
Not-null constraints:
@@ -440,6 +445,7 @@ Indexes:
"ctlt_all_expr_idx" btree ((a || b))
Check constraints:
"ctlt1_a_check" CHECK (length(a) > 2)
+ "ctlt1_b_check" CHECK (length(b) > 100) NOT ENFORCED
Statistics objects:
"public.ctlt_all_a_b_stat" ON a, b FROM ctlt_all
"public.ctlt_all_expr_stat" ON (a || b) FROM ctlt_all
@@ -482,6 +488,7 @@ Indexes:
"pg_attrdef_expr_idx" btree ((a || b))
Check constraints:
"ctlt1_a_check" CHECK (length(a) > 2)
+ "ctlt1_b_check" CHECK (length(b) > 100) NOT ENFORCED
Statistics objects:
"public.pg_attrdef_a_b_stat" ON a, b FROM public.pg_attrdef
"public.pg_attrdef_expr_stat" ON (a || b) FROM public.pg_attrdef
@@ -506,6 +513,7 @@ Indexes:
"ctlt1_expr_idx" btree ((a || b))
Check constraints:
"ctlt1_a_check" CHECK (length(a) > 2)
+ "ctlt1_b_check" CHECK (length(b) > 100) NOT ENFORCED
Statistics objects:
"ctl_schema.ctlt1_a_b_stat" ON a, b FROM ctlt1
"ctl_schema.ctlt1_expr_stat" ON (a || b) FROM ctlt1
diff --git a/src/test/regress/sql/constraints.sql b/src/test/regress/sql/constraints.sql
index e607eb1fddb..32c8b433bf0 100644
--- a/src/test/regress/sql/constraints.sql
+++ b/src/test/regress/sql/constraints.sql
@@ -67,6 +67,18 @@ INSERT INTO CHECK_TBL VALUES (1);
SELECT * FROM CHECK_TBL;
+CREATE TABLE NE_CHECK_TBL (x int,
+ CONSTRAINT CHECK_CON CHECK (x > 3) NOT ENFORCED);
+
+INSERT INTO NE_CHECK_TBL VALUES (5);
+INSERT INTO NE_CHECK_TBL VALUES (4);
+INSERT INTO NE_CHECK_TBL VALUES (3);
+INSERT INTO NE_CHECK_TBL VALUES (2);
+INSERT INTO NE_CHECK_TBL VALUES (6);
+INSERT INTO NE_CHECK_TBL VALUES (1);
+
+SELECT * FROM NE_CHECK_TBL;
+
CREATE SEQUENCE CHECK_SEQ;
CREATE TABLE CHECK2_TBL (x int, y text, z int,
@@ -92,7 +104,8 @@ CREATE TABLE INSERT_TBL (x INT DEFAULT nextval('insert_seq'),
y TEXT DEFAULT '-NULL-',
z INT DEFAULT -1 * currval('insert_seq'),
CONSTRAINT INSERT_TBL_CON CHECK (x >= 3 AND y <> 'check failed' AND x < 8),
- CHECK (x + z = 0));
+ CHECK (x + z = 0) ENFORCED, /* no change it is a default */
+ CONSTRAINT NE_INSERT_TBL_CON CHECK (x + z = 1) NOT ENFORCED);
INSERT INTO INSERT_TBL(x,z) VALUES (2, -2);
@@ -518,6 +531,13 @@ COMMIT;
SELECT * FROM unique_tbl;
+-- enforcibility cannot be specified or set for unique constrain
+CREATE TABLE UNIQUE_EN_TBL(i int UNIQUE ENFORCED);
+CREATE TABLE UNIQUE_NOTEN_TBL(i int UNIQUE NOT ENFORCED);
+-- XXX: error message is misleading here
+ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
+ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORCED;
+
DROP TABLE unique_tbl;
--
@@ -812,6 +832,12 @@ COMMENT ON CONSTRAINT the_constraint ON constraint_comments_tbl IS 'no, the comm
COMMENT ON CONSTRAINT the_constraint ON DOMAIN constraint_comments_dom IS 'no, another comment';
RESET SESSION AUTHORIZATION;
+-- enforceability not allowed
+CREATE DOMAIN constraint_enforced_dom AS int CONSTRAINT the_constraint CHECK (value > 0) ENFORCED;
+CREATE DOMAIN constraint_not_enforced_dom AS int CONSTRAINT the_constraint CHECK (value > 0) NOT ENFORCED;
+ALTER DOMAIN constraint_comments_dom ADD CONSTRAINT the_constraint CHECK (value > 0) ENFORCED;
+ALTER DOMAIN constraint_comments_dom ADD CONSTRAINT the_constraint CHECK (value > 0) NOT ENFORCED;
+
DROP TABLE constraint_comments_tbl;
DROP DOMAIN constraint_comments_dom;
diff --git a/src/test/regress/sql/create_table_like.sql b/src/test/regress/sql/create_table_like.sql
index dea8942c71f..a41f8b83d77 100644
--- a/src/test/regress/sql/create_table_like.sql
+++ b/src/test/regress/sql/create_table_like.sql
@@ -128,7 +128,8 @@ CREATE TABLE inhz (x text REFERENCES inhz, LIKE inhx INCLUDING INDEXES);
DROP TABLE inhz;
-- including storage and comments
-CREATE TABLE ctlt1 (a text CHECK (length(a) > 2) PRIMARY KEY, b text);
+CREATE TABLE ctlt1 (a text CHECK (length(a) > 2) ENFORCED PRIMARY KEY,
+ b text CHECK (length(b) > 100) NOT ENFORCED);
CREATE INDEX ctlt1_b_key ON ctlt1 (b);
CREATE INDEX ctlt1_fnidx ON ctlt1 ((a || b));
CREATE STATISTICS ctlt1_a_b_stat ON a,b FROM ctlt1;
--
2.43.5
v4-0002-refactor-split-ATExecAlterConstrRecurse.patchapplication/x-patch; name=v4-0002-refactor-split-ATExecAlterConstrRecurse.patchDownload
From 216023f61ce89931c399d0f7964a0c2ac4e09efe Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 23 Sep 2024 12:31:07 +0530
Subject: [PATCH v4 2/5] refactor: split ATExecAlterConstrRecurse()
When changing a NOT ENFORCED foreign key constraint to ENFORCED, we
need to create the necessary triggers for enforcing the constraint. We
can set the deferrability at the time of creation and skip the code
used for other constraint alteration operations.
Additionally, move the code that iterates over each child constraint
of the constraint being altered into a separate function.
---
src/backend/commands/tablecmds.c | 194 +++++++++++++++++++------------
1 file changed, 118 insertions(+), 76 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 524c1e5f40b..3d77bb660fb 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -394,6 +394,12 @@ static ObjectAddress ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd,
static bool ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
Relation rel, HeapTuple contuple, List **otherrelids,
LOCKMODE lockmode);
+static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
+ bool deferrable, bool initdeferred,
+ List **otherrelids);
+static void ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
+ Relation rel, HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode);
static ObjectAddress ATExecValidateConstraint(List **wqueue,
Relation rel, char *constrName,
bool recurse, bool recursing, LOCKMODE lockmode);
@@ -11790,9 +11796,6 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
{
HeapTuple copyTuple;
Form_pg_constraint copy_con;
- HeapTuple tgtuple;
- ScanKeyData tgkey;
- SysScanDesc tgscan;
copyTuple = heap_copytuple(contuple);
copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
@@ -11813,53 +11816,8 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
* Now we need to update the multiple entries in pg_trigger that
* implement the constraint.
*/
- ScanKeyInit(&tgkey,
- Anum_pg_trigger_tgconstraint,
- BTEqualStrategyNumber, F_OIDEQ,
- ObjectIdGetDatum(conoid));
- tgscan = systable_beginscan(tgrel, TriggerConstraintIndexId, true,
- NULL, 1, &tgkey);
- while (HeapTupleIsValid(tgtuple = systable_getnext(tgscan)))
- {
- Form_pg_trigger tgform = (Form_pg_trigger) GETSTRUCT(tgtuple);
- Form_pg_trigger copy_tg;
- HeapTuple tgCopyTuple;
-
- /*
- * Remember OIDs of other relation(s) involved in FK constraint.
- * (Note: it's likely that we could skip forcing a relcache inval
- * for other rels that don't have a trigger whose properties
- * change, but let's be conservative.)
- */
- if (tgform->tgrelid != RelationGetRelid(rel))
- *otherrelids = list_append_unique_oid(*otherrelids,
- tgform->tgrelid);
-
- /*
- * Update deferrability of RI_FKey_noaction_del,
- * RI_FKey_noaction_upd, RI_FKey_check_ins and RI_FKey_check_upd
- * triggers, but not others; see createForeignKeyActionTriggers
- * and CreateFKCheckTrigger.
- */
- if (tgform->tgfoid != F_RI_FKEY_NOACTION_DEL &&
- tgform->tgfoid != F_RI_FKEY_NOACTION_UPD &&
- tgform->tgfoid != F_RI_FKEY_CHECK_INS &&
- tgform->tgfoid != F_RI_FKEY_CHECK_UPD)
- continue;
-
- tgCopyTuple = heap_copytuple(tgtuple);
- copy_tg = (Form_pg_trigger) GETSTRUCT(tgCopyTuple);
-
- copy_tg->tgdeferrable = cmdcon->deferrable;
- copy_tg->tginitdeferred = cmdcon->initdeferred;
- CatalogTupleUpdate(tgrel, &tgCopyTuple->t_self, tgCopyTuple);
-
- InvokeObjectPostAlterHook(TriggerRelationId, tgform->oid, 0);
-
- heap_freetuple(tgCopyTuple);
- }
-
- systable_endscan(tgscan);
+ AlterConstrTriggerDeferrability(conoid, tgrel, rel, cmdcon->deferrable,
+ cmdcon->initdeferred, otherrelids);
}
/*
@@ -11872,36 +11830,120 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
*/
if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)
- {
- ScanKeyData pkey;
- SysScanDesc pscan;
- HeapTuple childtup;
-
- ScanKeyInit(&pkey,
- Anum_pg_constraint_conparentid,
- BTEqualStrategyNumber, F_OIDEQ,
- ObjectIdGetDatum(conoid));
-
- pscan = systable_beginscan(conrel, ConstraintParentIndexId,
- true, NULL, 1, &pkey);
-
- while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
- {
- Form_pg_constraint childcon = (Form_pg_constraint) GETSTRUCT(childtup);
- Relation childrel;
-
- childrel = table_open(childcon->conrelid, lockmode);
- ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, childrel, childtup,
- otherrelids, lockmode);
- table_close(childrel, NoLock);
- }
-
- systable_endscan(pscan);
- }
+ ATExecAlterChildConstr(cmdcon, conrel, tgrel, rel, contuple,
+ otherrelids, lockmode);
return changed;
}
+/*
+ * A subroutine of ATExecAlterConstrRecurse that updated constraint trigger's
+ * deferrability.
+ *
+ * The arguments to this function have the same meaning as the arguments to
+ * ATExecAlterConstrRecurse.
+ */
+static void
+AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
+ bool deferrable, bool initdeferred,
+ List **otherrelids)
+{
+ HeapTuple tgtuple;
+ ScanKeyData tgkey;
+ SysScanDesc tgscan;
+
+ ScanKeyInit(&tgkey,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+ tgscan = systable_beginscan(tgrel, TriggerConstraintIndexId, true,
+ NULL, 1, &tgkey);
+ while (HeapTupleIsValid(tgtuple = systable_getnext(tgscan)))
+ {
+ Form_pg_trigger tgform = (Form_pg_trigger) GETSTRUCT(tgtuple);
+ Form_pg_trigger copy_tg;
+ HeapTuple tgCopyTuple;
+
+ /*
+ * Remember OIDs of other relation(s) involved in FK constraint.
+ * (Note: it's likely that we could skip forcing a relcache inval for
+ * other rels that don't have a trigger whose properties change, but
+ * let's be conservative.)
+ */
+ if (tgform->tgrelid != RelationGetRelid(rel))
+ *otherrelids = list_append_unique_oid(*otherrelids,
+ tgform->tgrelid);
+
+ /*
+ * Update enable status and deferrability of RI_FKey_noaction_del,
+ * RI_FKey_noaction_upd, RI_FKey_check_ins and RI_FKey_check_upd
+ * triggers, but not others; see createForeignKeyActionTriggers and
+ * CreateFKCheckTrigger.
+ */
+ if (tgform->tgfoid != F_RI_FKEY_NOACTION_DEL &&
+ tgform->tgfoid != F_RI_FKEY_NOACTION_UPD &&
+ tgform->tgfoid != F_RI_FKEY_CHECK_INS &&
+ tgform->tgfoid != F_RI_FKEY_CHECK_UPD)
+ continue;
+
+ tgCopyTuple = heap_copytuple(tgtuple);
+ copy_tg = (Form_pg_trigger) GETSTRUCT(tgCopyTuple);
+
+ copy_tg->tgdeferrable = deferrable;
+ copy_tg->tginitdeferred = initdeferred;
+ CatalogTupleUpdate(tgrel, &tgCopyTuple->t_self, tgCopyTuple);
+
+ InvokeObjectPostAlterHook(TriggerRelationId, tgform->oid, 0);
+
+ heap_freetuple(tgCopyTuple);
+ }
+
+ systable_endscan(tgscan);
+}
+
+/*
+ * Invokes ATExecAlterConstrRecurse for each constraint that is a child of the
+ * specified constraint.
+ *
+ * The arguments to this function have the same meaning as the arguments to
+ * ATExecAlterConstrRecurse.
+ */
+static void
+ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
+ Relation rel, HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode)
+{
+ Form_pg_constraint currcon;
+ Oid conoid;
+ ScanKeyData pkey;
+ SysScanDesc pscan;
+ HeapTuple childtup;
+
+ currcon = (Form_pg_constraint) GETSTRUCT(contuple);
+ conoid = currcon->oid;
+
+ ScanKeyInit(&pkey,
+ Anum_pg_constraint_conparentid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+
+ pscan = systable_beginscan(conrel, ConstraintParentIndexId,
+ true, NULL, 1, &pkey);
+
+ while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
+ {
+ Form_pg_constraint childcon = (Form_pg_constraint) GETSTRUCT(childtup);
+ Relation childrel;
+
+ childrel = table_open(childcon->conrelid, lockmode);
+ ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, childrel, childtup,
+ otherrelids, lockmode);
+ table_close(childrel, NoLock);
+ }
+
+ systable_endscan(pscan);
+}
+
/*
* ALTER TABLE VALIDATE CONSTRAINT
*
--
2.43.5
v4-0003-refactor-Change-ATExecAlterConstrRecurse-argument.patchapplication/x-patch; name=v4-0003-refactor-Change-ATExecAlterConstrRecurse-argument.patchDownload
From 94f5d71b02357fb200d492e3d2c16a0a52021e4c Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 23 Sep 2024 12:37:05 +0530
Subject: [PATCH v4 3/5] refactor: Change ATExecAlterConstrRecurse() argument.
Instead of passing the Relation of the referencing relation, we will
pass a relid. Opening the relation using the relid again should not
cause significant issues. However, this approach makes recursion more
efficient, especially in the later patch where we will be recreating
the referencing and referred triggers. In addition to that passing
relid of the referred relation too and Oids of triggers.
----
NOTE: This patch is not meant to be committed separately. It should
be squashed with the main patch that adds ENFORCED/NOT ENFORCED.
----
---
src/backend/commands/tablecmds.c | 66 ++++++++++++++++++++++----------
1 file changed, 45 insertions(+), 21 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 3d77bb660fb..2dce6bbcc33 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -392,14 +392,22 @@ static void AlterSeqNamespaces(Relation classRel, Relation rel,
static ObjectAddress ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd,
bool recurse, bool recursing, LOCKMODE lockmode);
static bool ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode);
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger);
static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
bool deferrable, bool initdeferred,
List **otherrelids);
static void ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode);
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger);
static ObjectAddress ATExecValidateConstraint(List **wqueue,
Relation rel, char *constrName,
bool recurse, bool recursing, LOCKMODE lockmode);
@@ -11736,8 +11744,10 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
currcon->condeferred != cmdcon->initdeferred ||
rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
- if (ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, rel, contuple,
- &otherrelids, lockmode))
+ if (ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, currcon->conrelid,
+ currcon->confrelid, contuple, &otherrelids,
+ lockmode, InvalidOid, InvalidOid,
+ InvalidOid, InvalidOid))
ObjectAddressSet(address, ConstraintRelationId, currcon->oid);
}
@@ -11770,13 +11780,18 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
*/
static bool
ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode)
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger)
{
Form_pg_constraint currcon;
Oid conoid;
Oid refrelid;
bool changed = false;
+ Relation rel;
/* since this function recurses, it could be driven to stack overflow */
check_stack_depth();
@@ -11785,6 +11800,8 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
conoid = currcon->oid;
refrelid = currcon->confrelid;
+ rel = table_open(currcon->conrelid, lockmode);
+
/*
* Update pg_constraint with the flags from cmdcon.
*
@@ -11830,8 +11847,14 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
*/
if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)
- ATExecAlterChildConstr(cmdcon, conrel, tgrel, rel, contuple,
- otherrelids, lockmode);
+ ATExecAlterChildConstr(cmdcon, conrel, tgrel, fkrelid, pkrelid,
+ contuple, otherrelids, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
+
+ table_close(rel, NoLock);
return changed;
}
@@ -11910,8 +11933,12 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
*/
static void
ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode)
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger)
{
Form_pg_constraint currcon;
Oid conoid;
@@ -11931,15 +11958,12 @@ ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
true, NULL, 1, &pkey);
while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
- {
- Form_pg_constraint childcon = (Form_pg_constraint) GETSTRUCT(childtup);
- Relation childrel;
-
- childrel = table_open(childcon->conrelid, lockmode);
- ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, childrel, childtup,
- otherrelids, lockmode);
- table_close(childrel, NoLock);
- }
+ ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, fkrelid, pkrelid,
+ childtup, otherrelids, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
systable_endscan(pscan);
}
--
2.43.5
v4-0004-Remove-hastriggers-flag-check-before-fetching-FK-.patchapplication/x-patch; name=v4-0004-Remove-hastriggers-flag-check-before-fetching-FK-.patchDownload
From 4d2cfa217f0b7a672b6860ba117619a0fffcb32f Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 7 Oct 2024 12:40:07 +0530
Subject: [PATCH v4 4/5] Remove hastriggers flag check before fetching FK
constraints.
With NOT ENFORCED, foreign key constraints will be created without
triggers. Therefore, the criteria for fetching foreign keys based on
the presence of triggers no longer apply.
----
NOTE: This patch is intended to reduce the diff noise from the main
patch and is not meant to be committed separately. It should be
squashed with the main patch that adds ENFORCED/NOT ENFORCED.
----
---
src/backend/utils/cache/relcache.c | 5 -
src/bin/pg_dump/pg_dump.c | 8 +-
src/bin/psql/describe.c | 223 ++++++++++++++---------------
3 files changed, 107 insertions(+), 129 deletions(-)
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 5d0233d56b2..b25dfb09808 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4670,11 +4670,6 @@ RelationGetFKeyList(Relation relation)
if (relation->rd_fkeyvalid)
return relation->rd_fkeylist;
- /* Fast path: non-partitioned tables without triggers can't have FKs */
- if (!relation->rd_rel->relhastriggers &&
- relation->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
- return NIL;
-
/*
* We build the list we intend to return (in the caller's context) while
* doing the scan. After successfully completing the scan, we copy that
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index a8c141b689d..98080ce44e2 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -7836,13 +7836,7 @@ getConstraints(Archive *fout, TableInfo tblinfo[], int numTables)
{
TableInfo *tinfo = &tblinfo[i];
- /*
- * For partitioned tables, foreign keys have no triggers so they must
- * be included anyway in case some foreign keys are defined.
- */
- if ((!tinfo->hastriggers &&
- tinfo->relkind != RELKIND_PARTITIONED_TABLE) ||
- !(tinfo->dobj.dump & DUMP_COMPONENT_DEFINITION))
+ if (!(tinfo->dobj.dump & DUMP_COMPONENT_DEFINITION))
continue;
/* OK, we need info for this table */
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 5bfebad64d5..410b145548e 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2498,135 +2498,124 @@ describeOneTableDetails(const char *schemaname,
}
/*
- * Print foreign-key constraints (there are none if no triggers,
- * except if the table is partitioned, in which case the triggers
- * appear in the partitions)
+ * Print foreign-key constraints
*/
- if (tableinfo.hastriggers ||
- tableinfo.relkind == RELKIND_PARTITIONED_TABLE)
+ if (pset.sversion >= 120000 &&
+ (tableinfo.ispartition || tableinfo.relkind == RELKIND_PARTITIONED_TABLE))
{
- if (pset.sversion >= 120000 &&
- (tableinfo.ispartition || tableinfo.relkind == RELKIND_PARTITIONED_TABLE))
+ /*
+ * Put the constraints defined in this table first, followed
+ * by the constraints defined in ancestor partitioned tables.
+ */
+ printfPQExpBuffer(&buf,
+ "SELECT conrelid = '%s'::pg_catalog.regclass AS sametable,\n"
+ " conname,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef,\n"
+ " conrelid::pg_catalog.regclass AS ontable\n"
+ " FROM pg_catalog.pg_constraint,\n"
+ " pg_catalog.pg_partition_ancestors('%s')\n"
+ " WHERE conrelid = relid AND contype = 'f' AND conparentid = 0\n"
+ "ORDER BY sametable DESC, conname;",
+ oid, oid);
+ }
+ else
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT true as sametable, conname,\n"
+ " pg_catalog.pg_get_constraintdef(r.oid, true) as condef,\n"
+ " conrelid::pg_catalog.regclass AS ontable\n"
+ "FROM pg_catalog.pg_constraint r\n"
+ "WHERE r.conrelid = '%s' AND r.contype = 'f'\n",
+ oid);
+
+ if (pset.sversion >= 120000)
+ appendPQExpBufferStr(&buf, " AND conparentid = 0\n");
+ appendPQExpBufferStr(&buf, "ORDER BY conname");
+ }
+
+ result = PSQLexec(buf.data);
+ if (!result)
+ goto error_return;
+ else
+ tuples = PQntuples(result);
+
+ if (tuples > 0)
+ {
+ int i_sametable = PQfnumber(result, "sametable"),
+ i_conname = PQfnumber(result, "conname"),
+ i_condef = PQfnumber(result, "condef"),
+ i_ontable = PQfnumber(result, "ontable");
+
+ printTableAddFooter(&cont, _("Foreign-key constraints:"));
+ for (i = 0; i < tuples; i++)
{
/*
- * Put the constraints defined in this table first, followed
- * by the constraints defined in ancestor partitioned tables.
+ * Print untranslated constraint name and definition. Use
+ * a "TABLE tab" prefix when the constraint is defined in
+ * a parent partitioned table.
*/
- printfPQExpBuffer(&buf,
- "SELECT conrelid = '%s'::pg_catalog.regclass AS sametable,\n"
- " conname,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef,\n"
- " conrelid::pg_catalog.regclass AS ontable\n"
- " FROM pg_catalog.pg_constraint,\n"
- " pg_catalog.pg_partition_ancestors('%s')\n"
- " WHERE conrelid = relid AND contype = 'f' AND conparentid = 0\n"
- "ORDER BY sametable DESC, conname;",
- oid, oid);
- }
- else
- {
- printfPQExpBuffer(&buf,
- "SELECT true as sametable, conname,\n"
- " pg_catalog.pg_get_constraintdef(r.oid, true) as condef,\n"
- " conrelid::pg_catalog.regclass AS ontable\n"
- "FROM pg_catalog.pg_constraint r\n"
- "WHERE r.conrelid = '%s' AND r.contype = 'f'\n",
- oid);
-
- if (pset.sversion >= 120000)
- appendPQExpBufferStr(&buf, " AND conparentid = 0\n");
- appendPQExpBufferStr(&buf, "ORDER BY conname");
- }
-
- result = PSQLexec(buf.data);
- if (!result)
- goto error_return;
- else
- tuples = PQntuples(result);
-
- if (tuples > 0)
- {
- int i_sametable = PQfnumber(result, "sametable"),
- i_conname = PQfnumber(result, "conname"),
- i_condef = PQfnumber(result, "condef"),
- i_ontable = PQfnumber(result, "ontable");
-
- printTableAddFooter(&cont, _("Foreign-key constraints:"));
- for (i = 0; i < tuples; i++)
- {
- /*
- * Print untranslated constraint name and definition. Use
- * a "TABLE tab" prefix when the constraint is defined in
- * a parent partitioned table.
- */
- if (strcmp(PQgetvalue(result, i, i_sametable), "f") == 0)
- printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
- PQgetvalue(result, i, i_ontable),
- PQgetvalue(result, i, i_conname),
- PQgetvalue(result, i, i_condef));
- else
- printfPQExpBuffer(&buf, " \"%s\" %s",
- PQgetvalue(result, i, i_conname),
- PQgetvalue(result, i, i_condef));
-
- printTableAddFooter(&cont, buf.data);
- }
- }
- PQclear(result);
- }
-
- /* print incoming foreign-key references */
- if (tableinfo.hastriggers ||
- tableinfo.relkind == RELKIND_PARTITIONED_TABLE)
- {
- if (pset.sversion >= 120000)
- {
- printfPQExpBuffer(&buf,
- "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
- " FROM pg_catalog.pg_constraint c\n"
- " WHERE confrelid IN (SELECT pg_catalog.pg_partition_ancestors('%s')\n"
- " UNION ALL VALUES ('%s'::pg_catalog.regclass))\n"
- " AND contype = 'f' AND conparentid = 0\n"
- "ORDER BY conname;",
- oid, oid);
- }
- else
- {
- printfPQExpBuffer(&buf,
- "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
- " FROM pg_catalog.pg_constraint\n"
- " WHERE confrelid = %s AND contype = 'f'\n"
- "ORDER BY conname;",
- oid);
- }
-
- result = PSQLexec(buf.data);
- if (!result)
- goto error_return;
- else
- tuples = PQntuples(result);
-
- if (tuples > 0)
- {
- int i_conname = PQfnumber(result, "conname"),
- i_ontable = PQfnumber(result, "ontable"),
- i_condef = PQfnumber(result, "condef");
-
- printTableAddFooter(&cont, _("Referenced by:"));
- for (i = 0; i < tuples; i++)
- {
+ if (strcmp(PQgetvalue(result, i, i_sametable), "f") == 0)
printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
PQgetvalue(result, i, i_ontable),
PQgetvalue(result, i, i_conname),
PQgetvalue(result, i, i_condef));
+ else
+ printfPQExpBuffer(&buf, " \"%s\" %s",
+ PQgetvalue(result, i, i_conname),
+ PQgetvalue(result, i, i_condef));
- printTableAddFooter(&cont, buf.data);
- }
+ printTableAddFooter(&cont, buf.data);
}
- PQclear(result);
}
+ PQclear(result);
+
+ if (pset.sversion >= 120000)
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
+ " FROM pg_catalog.pg_constraint c\n"
+ " WHERE confrelid IN (SELECT pg_catalog.pg_partition_ancestors('%s')\n"
+ " UNION ALL VALUES ('%s'::pg_catalog.regclass))\n"
+ " AND contype = 'f' AND conparentid = 0\n"
+ "ORDER BY conname;",
+ oid, oid);
+ }
+ else
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
+ " FROM pg_catalog.pg_constraint\n"
+ " WHERE confrelid = %s AND contype = 'f'\n"
+ "ORDER BY conname;",
+ oid);
+ }
+
+ result = PSQLexec(buf.data);
+ if (!result)
+ goto error_return;
+ else
+ tuples = PQntuples(result);
+
+ if (tuples > 0)
+ {
+ int i_conname = PQfnumber(result, "conname"),
+ i_ontable = PQfnumber(result, "ontable"),
+ i_condef = PQfnumber(result, "condef");
+
+ printTableAddFooter(&cont, _("Referenced by:"));
+ for (i = 0; i < tuples; i++)
+ {
+ printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
+ PQgetvalue(result, i, i_ontable),
+ PQgetvalue(result, i, i_conname),
+ PQgetvalue(result, i, i_condef));
+
+ printTableAddFooter(&cont, buf.data);
+ }
+ }
+ PQclear(result);
/* print any row-level policies */
if (pset.sversion >= 90500)
--
2.43.5
v4-0005-Add-support-for-NOT-ENFORCED-in-foreign-key-const.patchapplication/x-patch; name=v4-0005-Add-support-for-NOT-ENFORCED-in-foreign-key-const.patchDownload
From 5545e9159437f3556ae84367acd2257571a12f7c Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Fri, 15 Nov 2024 15:43:20 +0530
Subject: [PATCH v4 5/5] Add support for NOT ENFORCED in foreign key
constraints.
Normally, when a foreign key (FK) constraint is created on a table,
action/check triggers are associated with the constraint to ensure data
integrity. With this patch, when constraints are marked as NOT
ENFORCED, integrity checks are not required, and thus, these triggers
are unnecessary. As a result, when creating a NOT ENFORCED FK
constraint or changing an existing FK constraint to NOT ENFORCED, the
creation of triggers will be skipped, or existing triggers will be
dropped, respectively. And when changing an existing NOT ENFORCED FK
constraint to ENFORCED, those triggers will be created.
---
doc/src/sgml/advanced.sgml | 13 +
doc/src/sgml/catalogs.sgml | 2 +-
doc/src/sgml/ddl.sgml | 12 +-
doc/src/sgml/ref/alter_table.sgml | 5 +
doc/src/sgml/ref/create_table.sgml | 10 +-
src/backend/catalog/pg_constraint.c | 5 +-
src/backend/commands/tablecmds.c | 486 ++++++++++++++++------
src/backend/parser/gram.y | 3 +-
src/backend/parser/parse_utilcmd.c | 6 +-
src/backend/utils/cache/relcache.c | 1 +
src/include/utils/rel.h | 3 +
src/test/regress/expected/foreign_key.out | 122 +++++-
src/test/regress/sql/foreign_key.sql | 88 +++-
13 files changed, 600 insertions(+), 156 deletions(-)
diff --git a/doc/src/sgml/advanced.sgml b/doc/src/sgml/advanced.sgml
index 755c9f14850..b7d90de69d7 100644
--- a/doc/src/sgml/advanced.sgml
+++ b/doc/src/sgml/advanced.sgml
@@ -125,6 +125,19 @@ ERROR: insert or update on table "weather" violates foreign key constraint "wea
DETAIL: Key (city)=(Berkeley) is not present in table "cities".
</screen>
</para>
+ <para>
+ If, for some reason, you do not want to enforce this constraint and wish to
+ avoid the error, you can modify the foreign key constraint to <literal>NOT ENFORCED</literal>,
+ or specify it in the <link linkend="sql-createtable"><command>CREATE TABLE</command></link>.
+ </para>
+ <para>
+ Let's change the constraint to <literal>NOT ENFORCED</literal>, and when you
+ attempt to reinsert the same record, it will not result in any error.
+<programlisting>
+ALTER TABLE weather ALTER CONSTRAINT weather_city_fkey NOT ENFORCED;
+INSERT INTO weather VALUES ('Berkeley', 45, 53, 0.0, '1994-11-28');
+</programlisting>
+ </para>
<para>
The behavior of foreign keys can be finely tuned to your
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 6357771dc57..45b3ea91c87 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2609,7 +2609,7 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l
</para>
<para>
Is the constraint enforceable?
- Currently, can be false only for CHECK constraints
+ Currently, can be false only for foreign keys and CHECK constraints
</para></entry>
</row>
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index 9e2708586d5..a014a3f0f6f 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -1058,11 +1058,13 @@ CREATE TABLE example (
</indexterm>
<para>
- A foreign key constraint specifies that the values in a column (or
- a group of columns) must match the values appearing in some row
- of another table.
- We say this maintains the <firstterm>referential
- integrity</firstterm> between two related tables.
+ A foreign key constraint with <literal>ENFORCED</literal>, the default
+ setting, specifies that the values in a column (or a group of columns)
+ must match the values appearing in some row of another table. We say this
+ maintains the <firstterm>referential integrity</firstterm> between two
+ related tables. If a foreign key constraint is created with <literal>NOT ENFORCED</literal>,
+ the check for aforementioned <firstterm>referential integrity</firstterm>
+ will not be performed.
</para>
<para>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index ca38937cf1e..0e33cf9bde9 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -555,6 +555,11 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
This form alters the attributes of a constraint that was previously
created. Currently only foreign key constraints may be altered.
</para>
+ <para>
+ Note that changing constraints to <literal>ENFORCED</literal>, which were
+ previously marked as valid and <literal>NOT ENFORCED</literal>, will
+ trigger validation, similar to <literal>VALIDATE CONSTRAINT</literal>.
+ </para>
</listitem>
</varlistentry>
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index 97c9af44f50..55d9d7add55 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -1373,11 +1373,11 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
<term><literal>NOT ENFORCED</literal></term>
<listitem>
<para>
- This is currently only allowed for <literal>CHECK</literal> constraints.
- If the constraint is <literal>NOT ENFORCED</literal>, this clause
- specifies that the constraint check will be skipped. When the constraint
- is <literal>ENFORCED</literal>, check is performed after each statement.
- This is the default.
+ This is currently only allowed for foreign key and <literal>CHECK</literal>
+ constraints. If the constraint is <literal>NOT ENFORCED</literal>, this
+ clause specifies that the constraint check will be skipped. When the
+ constraint is <literal>ENFORCED</literal>, check is performed after each
+ statement. This is the default.
</para>
</listitem>
</varlistentry>
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index f83167a8a69..aa93e258e49 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -100,8 +100,9 @@ CreateConstraintEntry(const char *constraintName,
ObjectAddresses *addrs_auto;
ObjectAddresses *addrs_normal;
- /* Only CHECK constraint can be not enforced */
- Assert(isEnforced || constraintType == CONSTRAINT_CHECK);
+ /* Only CHECK or FOREIGN KEY constraint can be not enforced */
+ Assert(isEnforced || constraintType == CONSTRAINT_CHECK ||
+ constraintType == CONSTRAINT_FOREIGN);
conDesc = table_open(ConstraintRelationId, RowExclusiveLock);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 2dce6bbcc33..347409f3268 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -389,19 +389,29 @@ static void AlterIndexNamespaces(Relation classRel, Relation rel,
static void AlterSeqNamespaces(Relation classRel, Relation rel,
Oid oldNspOid, Oid newNspOid, ObjectAddresses *objsMoved,
LOCKMODE lockmode);
-static ObjectAddress ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd,
+static ObjectAddress ATExecAlterConstraint(List **wqueue, Relation rel, AlterTableCmd *cmd,
bool recurse, bool recursing, LOCKMODE lockmode);
-static bool ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
+static bool ATExecAlterConstrRecurse(List **wqueue, Constraint *cmdcon,
+ Relation conrel, Relation tgrel,
const Oid fkrelid, const Oid pkrelid,
HeapTuple contuple, List **otherrelids,
LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
Oid ReferencedParentUpdTrigger,
Oid ReferencingParentInsTrigger,
Oid ReferencingParentUpdTrigger);
-static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
+static void ATExecAlterConstrEnforceability(List **wqueue, Constraint *cmdcon,
+ Relation conrel, Relation tgrel,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger);
+static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Oid fkrel,
bool deferrable, bool initdeferred,
List **otherrelids);
-static void ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
+static void ATExecAlterChildConstr(List **wqueue, Constraint *cmdcon,
+ Relation conrel, Relation tgrel,
const Oid fkrelid, const Oid pkrelid,
HeapTuple contuple, List **otherrelids,
LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
@@ -5416,7 +5426,7 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
lockmode);
break;
case AT_AlterConstraint: /* ALTER CONSTRAINT */
- address = ATExecAlterConstraint(rel, cmd, false, false, lockmode);
+ address = ATExecAlterConstraint(wqueue, rel, cmd, false, false, lockmode);
break;
case AT_ValidateConstraint: /* VALIDATE CONSTRAINT */
address = ATExecValidateConstraint(wqueue, rel, cmd->name, cmd->recurse,
@@ -10387,7 +10397,7 @@ addFkConstraint(addFkConstraintSides fkside,
fkconstraint->deferrable,
fkconstraint->initdeferred,
fkconstraint->initially_valid,
- true, /* Is Enforced */
+ fkconstraint->is_enforced,
parentConstr,
RelationGetRelid(rel),
fkattnum,
@@ -10504,20 +10514,22 @@ addFkRecurseReferenced(Constraint *fkconstraint, Relation rel,
Oid parentDelTrigger, Oid parentUpdTrigger,
bool with_period)
{
- Oid deleteTriggerOid,
- updateTriggerOid;
+ Oid deleteTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
Assert(CheckRelationLockedByMe(pkrel, ShareRowExclusiveLock, true));
Assert(CheckRelationLockedByMe(rel, ShareRowExclusiveLock, true));
/*
- * Create the action triggers that enforce the constraint.
+ * Create action triggers to enforce the constraint, or skip them if the
+ * constraint is not enforced.
*/
- createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
- fkconstraint,
- parentConstr, indexOid,
- parentDelTrigger, parentUpdTrigger,
- &deleteTriggerOid, &updateTriggerOid);
+ if (fkconstraint->is_enforced)
+ createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
+ fkconstraint,
+ parentConstr, indexOid,
+ parentDelTrigger, parentUpdTrigger,
+ &deleteTriggerOid, &updateTriggerOid);
/*
* If the referenced table is partitioned, recurse on ourselves to handle
@@ -10638,8 +10650,8 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
Oid parentInsTrigger, Oid parentUpdTrigger,
bool with_period)
{
- Oid insertTriggerOid,
- updateTriggerOid;
+ Oid insertTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
Assert(OidIsValid(parentConstr));
Assert(CheckRelationLockedByMe(rel, ShareRowExclusiveLock, true));
@@ -10651,29 +10663,32 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
errmsg("foreign key constraints are not supported on foreign tables")));
/*
- * Add the check triggers to it and, if necessary, schedule it to be
- * checked in Phase 3.
+ * Add check triggers if the constraint is enforced, and if needed,
+ * schedule them to be checked in Phase 3.
*
* If the relation is partitioned, drill down to do it to its partitions.
*/
- createForeignKeyCheckTriggers(RelationGetRelid(rel),
- RelationGetRelid(pkrel),
- fkconstraint,
- parentConstr,
- indexOid,
- parentInsTrigger, parentUpdTrigger,
- &insertTriggerOid, &updateTriggerOid);
+ if (fkconstraint->is_enforced)
+ createForeignKeyCheckTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
+ fkconstraint,
+ parentConstr,
+ indexOid,
+ parentInsTrigger, parentUpdTrigger,
+ &insertTriggerOid, &updateTriggerOid);
if (rel->rd_rel->relkind == RELKIND_RELATION)
{
/*
* Tell Phase 3 to check that the constraint is satisfied by existing
- * rows. We can skip this during table creation, when requested
- * explicitly by specifying NOT VALID in an ADD FOREIGN KEY command,
- * and when we're recreating a constraint following a SET DATA TYPE
- * operation that did not impugn its validity.
+ * rows. We can skip this during table creation, when constraint is
+ * specified as NOT ENFORCED, when requested explicitly by specifying
+ * NOT VALID in an ADD FOREIGN KEY command, and when we're recreating
+ * a constraint following a SET DATA TYPE operation that did not
+ * impugn its validity.
*/
- if (wqueue && !old_check_ok && !fkconstraint->skip_validation)
+ if (wqueue && !old_check_ok && !fkconstraint->skip_validation &&
+ fkconstraint->is_enforced)
{
NewConstraint *newcon;
AlteredTableInfo *tab;
@@ -10908,8 +10923,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
AttrNumber confdelsetcols[INDEX_MAX_KEYS];
Constraint *fkconstraint;
ObjectAddress address;
- Oid deleteTriggerOid,
- updateTriggerOid;
+ Oid deleteTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(constrOid));
if (!HeapTupleIsValid(tuple))
@@ -10971,6 +10986,7 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
fkconstraint->old_pktable_oid = InvalidOid;
fkconstraint->skip_validation = false;
fkconstraint->initially_valid = true;
+ fkconstraint->is_enforced = constrForm->conenforced;
/* set up colnames that are used to generate the constraint name */
for (int i = 0; i < numfks; i++)
@@ -10998,9 +11014,10 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
* parent OIDs for similar triggers that will be created on the
* partition in addFkRecurseReferenced().
*/
- GetForeignKeyActionTriggers(trigrel, constrOid,
- constrForm->confrelid, constrForm->conrelid,
- &deleteTriggerOid, &updateTriggerOid);
+ if (constrForm->conenforced)
+ GetForeignKeyActionTriggers(trigrel, constrOid,
+ constrForm->confrelid, constrForm->conrelid,
+ &deleteTriggerOid, &updateTriggerOid);
/* Add this constraint ... */
address = addFkConstraint(addFkReferencedSide,
@@ -11166,17 +11183,18 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
mapped_conkey[i] = attmap->attnums[conkey[i] - 1];
/*
- * Get the "check" triggers belonging to the constraint to pass as
- * parent OIDs for similar triggers that will be created on the
- * partition in addFkRecurseReferencing(). They are also passed to
- * tryAttachPartitionForeignKey() below to simply assign as parents to
- * the partition's existing "check" triggers, that is, if the
- * corresponding constraints is deemed attachable to the parent
- * constraint.
+ * Get the "check" triggers belonging to the constraint, if it is
+ * enforced, to pass as parent OIDs for similar triggers that will be
+ * created on the partition in addFkRecurseReferencing(). They are
+ * also passed to tryAttachPartitionForeignKey() below to simply
+ * assign as parents to the partition's existing "check" triggers,
+ * that is, if the corresponding constraints is deemed attachable to
+ * the parent constraint.
*/
- GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
- constrForm->confrelid, constrForm->conrelid,
- &insertTriggerOid, &updateTriggerOid);
+ if (constrForm->conenforced)
+ GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
+ constrForm->confrelid, constrForm->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
/*
* Before creating a new constraint, see whether any existing FKs are
@@ -11230,6 +11248,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
fkconstraint->old_pktable_oid = InvalidOid;
fkconstraint->skip_validation = false;
fkconstraint->initially_valid = constrForm->convalidated;
+ fkconstraint->is_enforced = constrForm->conenforced;
for (int i = 0; i < numfks; i++)
{
Form_pg_attribute att;
@@ -11312,8 +11331,6 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
ScanKeyData key;
SysScanDesc scan;
HeapTuple trigtup;
- Oid insertTriggerOid,
- updateTriggerOid;
parentConstrTup = SearchSysCache1(CONSTROID,
ObjectIdGetDatum(parentConstrOid));
@@ -11355,6 +11372,7 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
!partConstr->convalidated ||
partConstr->condeferrable != parentConstr->condeferrable ||
partConstr->condeferred != parentConstr->condeferred ||
+ partConstr->conenforced != parentConstr->conenforced ||
partConstr->confupdtype != parentConstr->confupdtype ||
partConstr->confdeltype != parentConstr->confdeltype ||
partConstr->confmatchtype != parentConstr->confmatchtype)
@@ -11414,18 +11432,25 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
ConstraintSetParentConstraint(fk->conoid, parentConstrOid, partRelid);
/*
- * Like the constraint, attach partition's "check" triggers to the
- * corresponding parent triggers.
+ * Similar to the constraint, attach the partition's "check" triggers to
+ * the corresponding parent triggers if the constraint is enforced.
+ * Non-enforced constraints do not have these triggers.
*/
- GetForeignKeyCheckTriggers(trigrel,
- fk->conoid, fk->confrelid, fk->conrelid,
- &insertTriggerOid, &updateTriggerOid);
- Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
- TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
- partRelid);
- Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
- TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
- partRelid);
+ if (fk->conenforced)
+ {
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
+ partRelid);
+ Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
+ partRelid);
+ }
/*
* If the referenced table is partitioned, then the partition we're
@@ -11639,8 +11664,8 @@ GetForeignKeyCheckTriggers(Relation trigrel,
* InvalidObjectAddress.
*/
static ObjectAddress
-ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
- bool recursing, LOCKMODE lockmode)
+ATExecAlterConstraint(List **wqueue, Relation rel, AlterTableCmd *cmd,
+ bool recurse, bool recursing, LOCKMODE lockmode)
{
Constraint *cmdcon;
Relation conrel;
@@ -11742,12 +11767,14 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
address = InvalidObjectAddress;
if (currcon->condeferrable != cmdcon->deferrable ||
currcon->condeferred != cmdcon->initdeferred ||
+ currcon->conenforced != cmdcon->is_enforced ||
rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
- if (ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, currcon->conrelid,
- currcon->confrelid, contuple, &otherrelids,
- lockmode, InvalidOid, InvalidOid,
- InvalidOid, InvalidOid))
+ if (ATExecAlterConstrRecurse(wqueue, cmdcon, conrel, tgrel,
+ currcon->conrelid, currcon->confrelid,
+ contuple, &otherrelids, lockmode,
+ InvalidOid, InvalidOid, InvalidOid,
+ InvalidOid))
ObjectAddressSet(address, ConstraintRelationId, currcon->oid);
}
@@ -11779,8 +11806,8 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
* but existing releases don't do that.)
*/
static bool
-ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
- const Oid fkrelid, const Oid pkrelid,
+ATExecAlterConstrRecurse(List **wqueue, Constraint *cmdcon, Relation conrel,
+ Relation tgrel, const Oid fkrelid, const Oid pkrelid,
HeapTuple contuple, List **otherrelids,
LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
Oid ReferencedParentUpdTrigger,
@@ -11789,19 +11816,18 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
{
Form_pg_constraint currcon;
Oid conoid;
+ Oid conrelid;
Oid refrelid;
bool changed = false;
- Relation rel;
/* since this function recurses, it could be driven to stack overflow */
check_stack_depth();
currcon = (Form_pg_constraint) GETSTRUCT(contuple);
conoid = currcon->oid;
+ conrelid = currcon->conrelid;
refrelid = currcon->confrelid;
- rel = table_open(currcon->conrelid, lockmode);
-
/*
* Update pg_constraint with the flags from cmdcon.
*
@@ -11809,7 +11835,8 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
* silently do nothing.
*/
if (currcon->condeferrable != cmdcon->deferrable ||
- currcon->condeferred != cmdcon->initdeferred)
+ currcon->condeferred != cmdcon->initdeferred ||
+ currcon->conenforced != cmdcon->is_enforced)
{
HeapTuple copyTuple;
Form_pg_constraint copy_con;
@@ -11818,6 +11845,7 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
copy_con->condeferrable = cmdcon->deferrable;
copy_con->condeferred = cmdcon->initdeferred;
+ copy_con->conenforced = cmdcon->is_enforced;
CatalogTupleUpdate(conrel, ©Tuple->t_self, copyTuple);
InvokeObjectPostAlterHook(ConstraintRelationId,
@@ -11827,38 +11855,214 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
changed = true;
/* Make new constraint flags visible to others */
- CacheInvalidateRelcache(rel);
+ CacheInvalidateRelcacheByRelid(conrelid);
+ }
+ if (currcon->conenforced != cmdcon->is_enforced)
+ {
+ ATExecAlterConstrEnforceability(wqueue, cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, contuple, otherrelids, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
+ }
+ else
+ {
/*
* Now we need to update the multiple entries in pg_trigger that
* implement the constraint.
*/
- AlterConstrTriggerDeferrability(conoid, tgrel, rel, cmdcon->deferrable,
- cmdcon->initdeferred, otherrelids);
+ AlterConstrTriggerDeferrability(conoid, tgrel, conrelid,
+ cmdcon->deferrable,
+ cmdcon->initdeferred,
+ otherrelids);
+
+ /*
+ * If the table at either end of the constraint is partitioned, we
+ * need to recurse and handle every constraint that is a child of this
+ * one.
+ *
+ * (This assumes that the recurse flag is forcibly set for partitioned
+ * tables, and not set for legacy inheritance, though we don't check
+ * for that here.)
+ */
+ if (get_rel_relkind(conrelid) == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)
+ ATExecAlterChildConstr(wqueue, cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, contuple, otherrelids, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
}
- /*
- * If the table at either end of the constraint is partitioned, we need to
- * recurse and handle every constraint that is a child of this one.
- *
- * (This assumes that the recurse flag is forcibly set for partitioned
- * tables, and not set for legacy inheritance, though we don't check for
- * that here.)
- */
- if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
- get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)
- ATExecAlterChildConstr(cmdcon, conrel, tgrel, fkrelid, pkrelid,
- contuple, otherrelids, lockmode,
- ReferencedParentDelTrigger,
- ReferencedParentUpdTrigger,
- ReferencingParentInsTrigger,
- ReferencingParentUpdTrigger);
-
- table_close(rel, NoLock);
-
return changed;
}
+/*
+ * A subroutine of ATExecAlterConstrRecurse that updates the enforceability of
+ * a foreign key constraint. Depending on whether the constraint is being set
+ * to enforced or not enforced, it creates or drops the trigger accordingly.
+ *
+ * The arguments to this function have the same meaning as the arguments to
+ * ATExecAlterConstrRecurse.
+ */
+static void
+ATExecAlterConstrEnforceability(List **wqueue, Constraint *cmdcon,
+ Relation conrel, Relation tgrel,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger)
+{
+ Form_pg_constraint currcon;
+ Oid conoid;
+
+ currcon = (Form_pg_constraint) GETSTRUCT(contuple);
+ conoid = currcon->oid;
+
+ /* Drop triggers */
+ if (!cmdcon->is_enforced)
+ {
+ HeapTuple tgtuple;
+ ScanKeyData tgkey;
+ SysScanDesc tgscan;
+
+ /*
+ * When setting a constraint to NOT ENFORCED, the constraint triggers
+ * need to be dropped. Therefore, we must process the child relations
+ * first, followed by the parent, to account for dependencies.
+ */
+ if (get_rel_relkind(currcon->conrelid) == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE)
+ ATExecAlterChildConstr(wqueue, cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, contuple, otherrelids, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
+
+ ScanKeyInit(&tgkey,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+ tgscan = systable_beginscan(tgrel, TriggerConstraintIndexId, true,
+ NULL, 1, &tgkey);
+ while (HeapTupleIsValid(tgtuple = systable_getnext(tgscan)))
+ {
+ Form_pg_trigger tgform = (Form_pg_trigger) GETSTRUCT(tgtuple);
+ ObjectAddress trigger;
+
+ /*
+ * Delete only RI_FKey_noaction_del, RI_FKey_noaction_upd,
+ * RI_FKey_check_ins and RI_FKey_check_upd triggers, but not
+ * others; see createForeignKeyActionTriggers and
+ * CreateFKCheckTrigger.
+ */
+ if (tgform->tgfoid != F_RI_FKEY_NOACTION_DEL &&
+ tgform->tgfoid != F_RI_FKEY_NOACTION_UPD &&
+ tgform->tgfoid != F_RI_FKEY_CHECK_INS &&
+ tgform->tgfoid != F_RI_FKEY_CHECK_UPD)
+ continue;
+
+ deleteDependencyRecordsFor(TriggerRelationId, tgform->oid,
+ false);
+ /* make dependency deletion visible to performDeletion */
+ CommandCounterIncrement();
+ ObjectAddressSet(trigger, TriggerRelationId, tgform->oid);
+ performDeletion(&trigger, DROP_RESTRICT, 0);
+ /* make trigger drop visible, in case the loop iterates */
+ CommandCounterIncrement();
+ }
+
+ systable_endscan(tgscan);
+ }
+ else /* Create triggers */
+ {
+ Relation rel;
+ Oid ReferencedDelTriggerOid = InvalidOid,
+ ReferencedUpdTriggerOid = InvalidOid,
+ ReferencingInsTriggerOid = InvalidOid,
+ ReferencingUpdTriggerOid = InvalidOid;
+
+ /* Update the foreign key action required for trigger creation. */
+ cmdcon->fk_matchtype = currcon->confmatchtype;
+ cmdcon->fk_upd_action = currcon->confupdtype;
+ cmdcon->fk_del_action = currcon->confdeltype;
+
+ rel = table_open(currcon->conrelid, lockmode);
+
+ /* Create referenced triggers */
+ if (currcon->conrelid == fkrelid)
+ createForeignKeyActionTriggers(rel,
+ currcon->confrelid,
+ cmdcon,
+ conoid,
+ currcon->conindid,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ &ReferencedDelTriggerOid,
+ &ReferencedUpdTriggerOid);
+
+ /* Create referencing triggers */
+ if (currcon->confrelid == pkrelid)
+ {
+ createForeignKeyCheckTriggers(currcon->conrelid,
+ pkrelid,
+ cmdcon,
+ conoid,
+ currcon->conindid,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger,
+ &ReferencingInsTriggerOid,
+ &ReferencingUpdTriggerOid);
+
+ /*
+ * Tell Phase 3 to check that the constraint is satisfied by
+ * existing rows, but skip this step if the rows have not been
+ * validated previously.
+ */
+ if (rel->rd_rel->relkind == RELKIND_RELATION &&
+ wqueue && currcon->convalidated)
+ {
+ NewConstraint *newcon;
+ AlteredTableInfo *tab;
+
+ tab = ATGetQueueEntry(wqueue, rel);
+
+ newcon = (NewConstraint *) palloc0(sizeof(NewConstraint));
+ newcon->name = pstrdup(NameStr(currcon->conname));
+ newcon->conid = currcon->oid;
+ newcon->contype = CONSTR_FOREIGN;
+ newcon->refrelid = pkrelid;
+ newcon->refindid = currcon->conindid;
+ newcon->conwithperiod = currcon->conperiod;
+ newcon->qual = (Node *) cmdcon;
+
+ tab->constraints = lappend(tab->constraints, newcon);
+ }
+ }
+
+ /*
+ * If the table at either end of the constraint is partitioned, we
+ * need to recurse and create triggers for each constraint that is a
+ * child of this one.
+ */
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE)
+ ATExecAlterChildConstr(wqueue, cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, contuple, otherrelids, lockmode,
+ ReferencedDelTriggerOid,
+ ReferencedUpdTriggerOid,
+ ReferencingInsTriggerOid,
+ ReferencingUpdTriggerOid);
+ table_close(rel, NoLock);
+ }
+}
+
/*
* A subroutine of ATExecAlterConstrRecurse that updated constraint trigger's
* deferrability.
@@ -11867,7 +12071,7 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
* ATExecAlterConstrRecurse.
*/
static void
-AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
+AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Oid fkrel,
bool deferrable, bool initdeferred,
List **otherrelids)
{
@@ -11893,7 +12097,7 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
* other rels that don't have a trigger whose properties change, but
* let's be conservative.)
*/
- if (tgform->tgrelid != RelationGetRelid(rel))
+ if (tgform->tgrelid != fkrel)
*otherrelids = list_append_unique_oid(*otherrelids,
tgform->tgrelid);
@@ -11932,8 +12136,8 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
* ATExecAlterConstrRecurse.
*/
static void
-ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
- const Oid fkrelid, const Oid pkrelid,
+ATExecAlterChildConstr(List **wqueue, Constraint *cmdcon, Relation conrel,
+ Relation tgrel, const Oid fkrelid, const Oid pkrelid,
HeapTuple contuple, List **otherrelids,
LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
Oid ReferencedParentUpdTrigger,
@@ -11958,9 +12162,9 @@ ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
true, NULL, 1, &pkey);
while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
- ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, fkrelid, pkrelid,
- childtup, otherrelids, lockmode,
- ReferencedParentDelTrigger,
+ ATExecAlterConstrRecurse(wqueue, cmdcon, conrel, tgrel,
+ fkrelid, pkrelid, childtup, otherrelids,
+ lockmode, ReferencedParentDelTrigger,
ReferencedParentUpdTrigger,
ReferencingParentInsTrigger,
ReferencingParentUpdTrigger);
@@ -12033,25 +12237,33 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
if (con->contype == CONSTRAINT_FOREIGN)
{
- NewConstraint *newcon;
- Constraint *fkconstraint;
+ /*
+ * Queue validation for phase 3 only if constraint is enforced;
+ * otherwise, adding it to the validation queue won't be very
+ * effective, as the verification will be skipped.
+ */
+ if (con->conenforced)
+ {
+ NewConstraint *newcon;
+ Constraint *fkconstraint;
- /* Queue validation for phase 3 */
- fkconstraint = makeNode(Constraint);
- /* for now this is all we need */
- fkconstraint->conname = constrName;
+ /* Queue validation for phase 3 */
+ fkconstraint = makeNode(Constraint);
+ /* for now this is all we need */
+ fkconstraint->conname = constrName;
- newcon = (NewConstraint *) palloc0(sizeof(NewConstraint));
- newcon->name = constrName;
- newcon->contype = CONSTR_FOREIGN;
- newcon->refrelid = con->confrelid;
- newcon->refindid = con->conindid;
- newcon->conid = con->oid;
- newcon->qual = (Node *) fkconstraint;
+ newcon = (NewConstraint *) palloc0(sizeof(NewConstraint));
+ newcon->name = constrName;
+ newcon->contype = CONSTR_FOREIGN;
+ newcon->refrelid = con->confrelid;
+ newcon->refindid = con->conindid;
+ newcon->conid = con->oid;
+ newcon->qual = (Node *) fkconstraint;
- /* Find or create work queue entry for this table */
- tab = ATGetQueueEntry(wqueue, rel);
- tab->constraints = lappend(tab->constraints, newcon);
+ /* Find or create work queue entry for this table */
+ tab = ATGetQueueEntry(wqueue, rel);
+ tab->constraints = lappend(tab->constraints, newcon);
+ }
/*
* We disallow creating invalid foreign keys to or from
@@ -12110,9 +12322,9 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
}
/*
- * Queue validation for phase 3 only if constraint is enforced;
- * otherwise, adding it to the validation queue won't be very
- * effective, as the verification will be skipped.
+ * Queue validation for phase 3 only if the constraint is
+ * enforced, for the same reason outlined for the foreign key
+ * constraint.
*/
if (con->conenforced)
{
@@ -12141,8 +12353,11 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
}
/*
- * Update the catalog regardless of enforcement; validation will occur
- * only when the constraint is enforced.
+ * Update the catalog regardless of enforcement; validation will only
+ * occur when the constraint is enforced. Additionally, when changing a
+ * non-enforced constraint to enforced, validation will only take place
+ * if the validation flag is set to true. Otherwise, the user must
+ * explicitly perform constraint validation.
*/
copyTuple = heap_copytuple(tuple);
copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
@@ -19877,8 +20092,6 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
Form_pg_constraint conform;
- Oid insertTriggerOid,
- updateTriggerOid;
contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
if (!HeapTupleIsValid(contup))
@@ -19901,17 +20114,25 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
/*
* Also, look up the partition's "check" triggers corresponding to the
- * constraint being detached and detach them from the parent triggers.
+ * enforced constraint being detached and detach them from the parent
+ * triggers. Non-enforced constraints do not have these triggers;
+ * therefore, this step is not needed.
*/
- GetForeignKeyCheckTriggers(trigrel,
- fk->conoid, fk->confrelid, fk->conrelid,
- &insertTriggerOid, &updateTriggerOid);
- Assert(OidIsValid(insertTriggerOid));
- TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
- RelationGetRelid(partRel));
- Assert(OidIsValid(updateTriggerOid));
- TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
- RelationGetRelid(partRel));
+ if (fk->conenforced)
+ {
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ Assert(OidIsValid(updateTriggerOid));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ }
/*
* Lastly, create the action triggers on the referenced table, using
@@ -19953,6 +20174,7 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
fkconstraint->initdeferred = conform->condeferred;
fkconstraint->skip_validation = true;
fkconstraint->initially_valid = true;
+ fkconstraint->is_enforced = conform->conenforced;
/* a few irrelevant fields omitted here */
fkconstraint->pktable = NULL;
fkconstraint->fk_attrs = NIL;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 062b7202a1d..7bc7b1e086d 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -4027,6 +4027,7 @@ ColConstraintElem:
n->fk_del_set_cols = ($5)->deleteAction->cols;
n->skip_validation = false;
n->initially_valid = true;
+ n->is_enforced = true;
$$ = (Node *) n;
}
;
@@ -4302,7 +4303,7 @@ ConstraintElem:
processCASbits($12, @12, "FOREIGN KEY",
&n->deferrable, &n->initdeferred,
&n->skip_validation, NULL,
- NULL,
+ &n->is_enforced,
yyscanner);
n->initially_valid = !n->skip_validation;
$$ = (Node *) n;
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 183f845ba55..ce66ec3d5d3 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -3963,7 +3963,8 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
case CONSTR_ATTR_ENFORCED:
if (lastprimarycon &&
- lastprimarycon->contype != CONSTR_CHECK)
+ lastprimarycon->contype != CONSTR_CHECK &&
+ lastprimarycon->contype != CONSTR_FOREIGN)
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("misplaced ENFORCED clause"),
@@ -3979,7 +3980,8 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
case CONSTR_ATTR_NOT_ENFORCED:
if (lastprimarycon &&
- lastprimarycon->contype != CONSTR_CHECK)
+ lastprimarycon->contype != CONSTR_CHECK &&
+ lastprimarycon->contype != CONSTR_FOREIGN)
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("misplaced NOT ENFORCED clause"),
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index b25dfb09808..bf15fef94f1 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4701,6 +4701,7 @@ RelationGetFKeyList(Relation relation)
info->conoid = constraint->oid;
info->conrelid = constraint->conrelid;
info->confrelid = constraint->confrelid;
+ info->conenforced = constraint->conenforced;
DeconstructFkConstraintRow(htup, &info->nkeys,
info->conkey,
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 87002049538..2db011ddfb7 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -284,6 +284,9 @@ typedef struct ForeignKeyCacheInfo
/* number of columns in the foreign key */
int nkeys;
+ /* Is enforced ? */
+ bool conenforced;
+
/*
* these arrays each have nkeys valid entries:
*/
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index a5165270c2d..1be03398e62 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -1,12 +1,43 @@
--
-- FOREIGN KEY
--
--- MATCH FULL
+-- NOT ENFORCED
--
-- First test, check and cascade
--
CREATE TABLE PKTABLE ( ptest1 int PRIMARY KEY, ptest2 text );
-CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE, ftest2 int );
+CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE NOT ENFORCED,
+ ftest2 int );
+-- Inserting into the foreign key table will not result in an error, even if
+-- there is no matching key in the referenced table.
+INSERT INTO FKTABLE VALUES (1, 2);
+INSERT INTO FKTABLE VALUES (2, 3);
+-- Check FKTABLE
+SELECT * FROM FKTABLE;
+ ftest1 | ftest2
+--------+--------
+ 1 | 2
+ 2 | 3
+(2 rows)
+
+-- Changing it back to ENFORCED will result in an error due to validation of
+-- existing rows.
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_fkey"
+DETAIL: Key (ftest1)=(1) is not present in table "pktable".
+-- Remove any existing rows that violate the constraint, then attempt to change
+-- it.
+TRUNCATE FKTABLE;
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+-- Any further inserts will fail due to the enforcement.
+INSERT INTO FKTABLE VALUES (3, 4);
+ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_fkey"
+DETAIL: Key (ftest1)=(3) is not present in table "pktable".
+--
+-- MATCH FULL
+--
+-- First test, check and cascade
+--
-- Insert test data into PKTABLE
INSERT INTO PKTABLE VALUES (1, 'Test1');
INSERT INTO PKTABLE VALUES (2, 'Test2');
@@ -351,6 +382,8 @@ INSERT INTO FKTABLE VALUES (1, NULL);
ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL;
ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_ftest2_fkey"
DETAIL: MATCH FULL does not allow mixing of null and nonnull key values.
+-- NOT ENFORCED will bypass the initial check.
+ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL NOT ENFORCED;
DROP TABLE FKTABLE;
DROP TABLE PKTABLE;
-- MATCH SIMPLE
@@ -1276,13 +1309,28 @@ INSERT INTO fktable VALUES (0, 20);
ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_fk_fkey"
DETAIL: Key (fk)=(20) is not present in table "pktable".
COMMIT;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT ENFORCED;
+BEGIN;
+-- doesn't match FK, no error.
+UPDATE pktable SET id = 10 WHERE id = 5;
+-- doesn't match PK, no error.
+INSERT INTO fktable VALUES (0, 20);
+ROLLBACK;
-- try additional syntax
-ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE ENFORCED;
-- illegal option
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE INITIALLY DEFERRED;
ERROR: constraint declared INITIALLY DEFERRED must be DEFERRABLE
LINE 1: ...e ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE INITIALLY ...
^
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORCED;
+ERROR: conflicting constraint properties
+LINE 1: ...fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORC...
+ ^
+CREATE TEMP TABLE fktable2 (fk int references pktable ENFORCED NOT ENFORCED);
+ERROR: multiple ENFORCED/NOT ENFORCED clauses not allowed
+LINE 1: ...ABLE fktable2 (fk int references pktable ENFORCED NOT ENFORC...
+ ^
-- test order of firing of FK triggers when several RI-induced changes need to
-- be made to the same row. This was broken by subtransaction-related
-- changes in 8.0.
@@ -1580,7 +1628,8 @@ ALTER TABLE fk_partitioned_fk DROP COLUMN fdrop1;
CREATE TABLE fk_partitioned_fk_1 (fdrop1 int, fdrop2 int, a int, fdrop3 int, b int);
ALTER TABLE fk_partitioned_fk_1 DROP COLUMN fdrop1, DROP COLUMN fdrop2, DROP COLUMN fdrop3;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FROM (0,0) TO (1000,1000);
-ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk;
+ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
@@ -1665,6 +1714,37 @@ Indexes:
Referenced by:
TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b)
+-- Check the exsting FK trigger
+CREATE TEMP TABLE tmp_trg_info AS SELECT tgtype, tgrelid::regclass, tgconstraint
+FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+SELECT count(1) FROM tmp_trg_info;
+ count
+-------
+ 14
+(1 row)
+
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+-- No triggers
+SELECT count(1) FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+ count
+-------
+ 0
+(1 row)
+
+-- Changing it back to ENFORCED will recreate the necessary triggers.
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+-- Should be exactly the same number of triggers found as before
+SELECT COUNT(1) FROM pg_trigger pgt JOIN tmp_trg_info tt
+ON (pgt.tgtype = tt.tgtype AND pgt.tgrelid = tt.tgrelid AND pgt.tgconstraint = tt.tgconstraint);
+ count
+-------
+ 14
+(1 row)
+
ALTER TABLE fk_partitioned_fk DROP CONSTRAINT fk_partitioned_fk_a_b_fkey;
-- done.
DROP TABLE fk_notpartitioned_pk, fk_partitioned_fk;
@@ -1868,6 +1948,40 @@ Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
Foreign-key constraints:
TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
+DROP TABLE fk_partitioned_fk_2;
+CREATE TABLE fk_partitioned_fk_2 (b int, a int,
+ FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
+BEGIN;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have two constraints
+\d fk_partitioned_fk_2
+ Table "public.fk_partitioned_fk_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ b | integer | | |
+ a | integer | | |
+Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
+Foreign-key constraints:
+ "fk_partitioned_fk_2_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED
+ TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
+
+DROP TABLE fk_partitioned_fk_2;
+ROLLBACK;
+BEGIN;
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have only one constraint
+\d fk_partitioned_fk_2
+ Table "public.fk_partitioned_fk_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ b | integer | | |
+ a | integer | | |
+Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
+Foreign-key constraints:
+ TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED
+
+ROLLBACK;
DROP TABLE fk_partitioned_fk_2;
CREATE TABLE fk_partitioned_fk_4 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE) PARTITION BY RANGE (b, a);
CREATE TABLE fk_partitioned_fk_4_1 PARTITION OF fk_partitioned_fk_4 FOR VALUES FROM (1,1) TO (100,100);
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index 2e710e419c2..d8aa9039b78 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -2,13 +2,39 @@
-- FOREIGN KEY
--
--- MATCH FULL
+-- NOT ENFORCED
--
-- First test, check and cascade
--
CREATE TABLE PKTABLE ( ptest1 int PRIMARY KEY, ptest2 text );
-CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE, ftest2 int );
+CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE NOT ENFORCED,
+ ftest2 int );
+-- Inserting into the foreign key table will not result in an error, even if
+-- there is no matching key in the referenced table.
+INSERT INTO FKTABLE VALUES (1, 2);
+INSERT INTO FKTABLE VALUES (2, 3);
+
+-- Check FKTABLE
+SELECT * FROM FKTABLE;
+
+-- Changing it back to ENFORCED will result in an error due to validation of
+-- existing rows.
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+
+-- Remove any existing rows that violate the constraint, then attempt to change
+-- it.
+TRUNCATE FKTABLE;
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+
+-- Any further inserts will fail due to the enforcement.
+INSERT INTO FKTABLE VALUES (3, 4);
+
+--
+-- MATCH FULL
+--
+-- First test, check and cascade
+--
-- Insert test data into PKTABLE
INSERT INTO PKTABLE VALUES (1, 'Test1');
INSERT INTO PKTABLE VALUES (2, 'Test2');
@@ -230,6 +256,9 @@ INSERT INTO FKTABLE VALUES (1, NULL);
ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL;
+-- NOT ENFORCED will bypass the initial check.
+ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL NOT ENFORCED;
+
DROP TABLE FKTABLE;
DROP TABLE PKTABLE;
@@ -968,10 +997,23 @@ INSERT INTO fktable VALUES (0, 20);
COMMIT;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT ENFORCED;
+
+BEGIN;
+
+-- doesn't match FK, no error.
+UPDATE pktable SET id = 10 WHERE id = 5;
+-- doesn't match PK, no error.
+INSERT INTO fktable VALUES (0, 20);
+
+ROLLBACK;
+
-- try additional syntax
-ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE ENFORCED;
-- illegal option
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORCED;
+CREATE TEMP TABLE fktable2 (fk int references pktable ENFORCED NOT ENFORCED);
-- test order of firing of FK triggers when several RI-induced changes need to
-- be made to the same row. This was broken by subtransaction-related
@@ -1182,7 +1224,8 @@ ALTER TABLE fk_partitioned_fk DROP COLUMN fdrop1;
CREATE TABLE fk_partitioned_fk_1 (fdrop1 int, fdrop2 int, a int, fdrop3 int, b int);
ALTER TABLE fk_partitioned_fk_1 DROP COLUMN fdrop1, DROP COLUMN fdrop2, DROP COLUMN fdrop3;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FROM (0,0) TO (1000,1000);
-ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk;
+ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
@@ -1236,6 +1279,27 @@ UPDATE fk_notpartitioned_pk SET b = 1502 WHERE a = 1500;
UPDATE fk_notpartitioned_pk SET b = 2504 WHERE a = 2500;
-- check psql behavior
\d fk_notpartitioned_pk
+
+-- Check the exsting FK trigger
+CREATE TEMP TABLE tmp_trg_info AS SELECT tgtype, tgrelid::regclass, tgconstraint
+FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+SELECT count(1) FROM tmp_trg_info;
+
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+-- No triggers
+SELECT count(1) FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+
+-- Changing it back to ENFORCED will recreate the necessary triggers.
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+
+-- Should be exactly the same number of triggers found as before
+SELECT COUNT(1) FROM pg_trigger pgt JOIN tmp_trg_info tt
+ON (pgt.tgtype = tt.tgtype AND pgt.tgrelid = tt.tgrelid AND pgt.tgconstraint = tt.tgconstraint);
+
ALTER TABLE fk_partitioned_fk DROP CONSTRAINT fk_partitioned_fk_a_b_fkey;
-- done.
DROP TABLE fk_notpartitioned_pk, fk_partitioned_fk;
@@ -1372,6 +1436,22 @@ ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN
\d fk_partitioned_fk_2
DROP TABLE fk_partitioned_fk_2;
+CREATE TABLE fk_partitioned_fk_2 (b int, a int,
+ FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
+BEGIN;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have two constraints
+\d fk_partitioned_fk_2
+DROP TABLE fk_partitioned_fk_2;
+ROLLBACK;
+BEGIN;
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have only one constraint
+\d fk_partitioned_fk_2
+ROLLBACK;
+DROP TABLE fk_partitioned_fk_2;
+
CREATE TABLE fk_partitioned_fk_4 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE) PARTITION BY RANGE (b, a);
CREATE TABLE fk_partitioned_fk_4_1 PARTITION OF fk_partitioned_fk_4 FOR VALUES FROM (1,1) TO (100,100);
CREATE TABLE fk_partitioned_fk_4_2 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE SET NULL);
--
2.43.5
On Fri, Nov 15, 2024 at 6:21 PM Amul Sul <sulamul@gmail.com> wrote:
Updated version attached.
hi.
i only played around with
v4-0001-Add-support-for-NOT-ENFORCED-in-CHECK-constraints.patch.
create table t(a int);
alter table t ADD CONSTRAINT the_constraint CHECK (a > 0) NOT ENFORCED;
insert into t select -1;
select conname, contype,condeferrable,condeferred, convalidated,
conenforced,conkey,connoinherit
from pg_constraint
where conrelid = 't'::regclass;
pg_constraint->convalidated should be set to false for NOT ENFORCED constraint?
Am I missing something?
<varlistentry id="sql-createtable-parms-enforce">
<term><literal>ENFORCED</literal></term>
<term><literal>NOT ENFORCED</literal></term>
<listitem>
<para>
This is currently only allowed for <literal>CHECK</literal> constraints.
If the constraint is <literal>NOT ENFORCED</literal>, this clause
specifies that the constraint check will be skipped. When the constraint
is <literal>ENFORCED</literal>, check is performed after each statement.
This is the default.
</para>
</listitem>
</varlistentry>
"This is the default." kind of ambiguous?
I think you mean: by default, all constraints are implicit ENFORCED.
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("misplaced ENFORCED clause"),
+ parser_errposition(cxt->pstate, con->location)));
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("misplaced NOT ENFORCED clause"),
+ parser_errposition(cxt->pstate, con->location)));
https://www.merriam-webster.com/dictionary/misplace
says:
"to put in a wrong or inappropriate place"
I found the "misplaced" error message is not helpful.
for example:
CREATE TABLE UNIQUE_EN_TBL(i int UNIQUE ENFORCED);
ERROR: misplaced ENFORCED clause
the error message only tells us thatspecify ENFORCED is wrong.
but didn't say why it's wrong.
we can saying that
"ENFORCED clauses can only be used for CHECK constraints"
------------------------------------------------------------------
the following queries is a bug?
drop table t;
create table t(a int);
alter table t ADD CONSTRAINT cc CHECK (a > 0) NOT ENFORCED;
insert into t select -1;
alter table t add constraint cc1 check (a > 1) not ENFORCED not ENFORCED;
ERROR: check constraint "cc1" of relation "t" is violated by some row
alter table t add constraint cc1 check (a > 1) not ENFORCED;
ERROR: check constraint "cc1" of relation "t" is violated by some row
------------------------------------------------------------------
drop table t;
create table t(a int);
alter table t ADD CONSTRAINT cc CHECK (a > 0) NOT ENFORCED not enforced;
seems not easy to make it fail with alter table multiple "not enforced".
I guess it should be fine.
since we disallow a mix of "not enforced" and "enforced".
alter table t ADD CONSTRAINT cc CHECK (a > 0) NOT ENFORCED enforced;
------------------------------------------------------------------
typedef struct Constraint
{
NodeTag type;
ConstrType contype; /* see above */
char *conname; /* Constraint name, or NULL if unnamed */
bool deferrable; /* DEFERRABLE? */
bool initdeferred; /* INITIALLY DEFERRED? */
bool skip_validation; /* skip validation of existing rows? */
bool initially_valid; /* mark the new constraint as valid? */
bool is_enforced; /* enforced constraint? */
}
makeNode(Constraint) will default is_enforced to false.
Which makes the default value not what we want.
That means we may need to pay more attention for the trip from
makeNode(Constraint) to finally insert the constraint to the catalog.
if we change it to is_not_enforced, makeNode will default to false.
is_not_enforced is false, means the constraint is enforced.
which is not that intuitive...
------------------------------------------------------------------
do we need to update for "enforced" in
https://www.postgresql.org/docs/current/sql-keywords-appendix.html
?
------------------------------------------------------------------
seems don't have
ALTER TABLE <name> VALIDATE CONSTRAINT
interacts with not forced sql tests.
for example:
drop table if exists t;
create table t(a int);
alter table t add constraint cc check (a <> 1) not enforced NOT VALID;
insert into t values(1); ---success.
alter table t validate constraint cc;
select conname,convalidated, conenforced
from pg_constraint
where conrelid = 't'::regclass;
returns:
conname | convalidated | conenforced
---------+--------------+-------------
cc | t | f
Now we have a value in the table "t" that violates the check
constraint, while convalidated is true.
----------------------------------------------------------------------------
i add more tests where it should fail.
also add a test case for `create table like INCLUDING CONSTRAINTS`
please check attached.
Attachments:
v4-0001-add-regress-tests-for-not-enforced-enforced-fo.no-cfbotapplication/octet-stream; name=v4-0001-add-regress-tests-for-not-enforced-enforced-fo.no-cfbotDownload
From 48475d1240f6b8e8e85d801443148eb1740daa70 Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
Date: Mon, 18 Nov 2024 20:34:16 +0800
Subject: [PATCH v4 1/1] add regress tests for not enforced/enforced for check
constraint
---
src/test/regress/expected/constraints.out | 95 +++++++++++++++++++
.../regress/expected/create_table_like.out | 15 +++
src/test/regress/sql/constraints.sql | 30 ++++++
src/test/regress/sql/create_table_like.sql | 7 ++
4 files changed, 147 insertions(+)
diff --git a/src/test/regress/expected/constraints.out b/src/test/regress/expected/constraints.out
index 1fe2a76fa3..66261f789f 100644
--- a/src/test/regress/expected/constraints.out
+++ b/src/test/regress/expected/constraints.out
@@ -744,6 +744,101 @@ CREATE TABLE UNIQUE_NOTEN_TBL(i int UNIQUE NOT ENFORCED);
ERROR: misplaced NOT ENFORCED clause
LINE 1: CREATE TABLE UNIQUE_NOTEN_TBL(i int UNIQUE NOT ENFORCED);
^
+-- none of the following should be accepted
+create table t(a int not null enforced);
+ERROR: misplaced ENFORCED clause
+LINE 1: create table t(a int not null enforced);
+ ^
+create table t(a int not null enforced enforced);
+ERROR: misplaced ENFORCED clause
+LINE 1: create table t(a int not null enforced enforced);
+ ^
+create table t(a int not null not enforced);
+ERROR: misplaced NOT ENFORCED clause
+LINE 1: create table t(a int not null not enforced);
+ ^
+create table t(a int unique enforced);
+ERROR: misplaced ENFORCED clause
+LINE 1: create table t(a int unique enforced);
+ ^
+create table t(a int unique not enforced);
+ERROR: misplaced NOT ENFORCED clause
+LINE 1: create table t(a int unique not enforced);
+ ^
+create table t(a int primary key enforced);
+ERROR: misplaced ENFORCED clause
+LINE 1: create table t(a int primary key enforced);
+ ^
+create table t(a int primary key not enforced);
+ERROR: misplaced NOT ENFORCED clause
+LINE 1: create table t(a int primary key not enforced);
+ ^
+create table t(a int generated by default as identity enforced);
+ERROR: misplaced ENFORCED clause
+LINE 1: ...te table t(a int generated by default as identity enforced);
+ ^
+create table t(a int generated ALWAYS as (2) stored enforced);
+ERROR: misplaced ENFORCED clause
+LINE 1: ...eate table t(a int generated ALWAYS as (2) stored enforced);
+ ^
+-- none of the above should be accepted
+create table t(a int primary key);
+create table t1(a int);
+-- none of the following should be accepted
+alter table t add constraint constr0 not null a not enforced;
+ERROR: NOT NULL constraints cannot be marked NOT ENFORCED
+LINE 1: alter table t add constraint constr0 not null a not enforce...
+ ^
+alter table t add constraint constr0 not null a enforced;
+ERROR: NOT NULL constraints cannot be marked ENFORCED
+LINE 1: alter table t add constraint constr0 not null a enforced;
+ ^
+alter table t add constraint constr0 unique (a) not enforced;
+ERROR: UNIQUE constraints cannot be marked NOT ENFORCED
+LINE 1: alter table t add constraint constr0 unique (a) not enforced...
+ ^
+alter table t add constraint constr0 unique (a) enforced;
+ERROR: UNIQUE constraints cannot be marked ENFORCED
+LINE 1: alter table t add constraint constr0 unique (a) enforced;
+ ^
+alter table t add constraint constr0 primary key (a) enforced;
+ERROR: PRIMARY KEY constraints cannot be marked ENFORCED
+LINE 1: ...ter table t add constraint constr0 primary key (a) enforced;
+ ^
+alter table t add constraint constr0 primary key (a) not enforced;
+ERROR: PRIMARY KEY constraints cannot be marked NOT ENFORCED
+LINE 1: ...er table t add constraint constr0 primary key (a) not enforc...
+ ^
+alter table t add constraint constr0 primary key (a) not enforced;
+ERROR: PRIMARY KEY constraints cannot be marked NOT ENFORCED
+LINE 1: ...er table t add constraint constr0 primary key (a) not enforc...
+ ^
+alter table t add constraint constr0 exclude using btree (a with = ) not enforced;
+ERROR: EXCLUDE constraints cannot be marked NOT ENFORCED
+LINE 1: ...onstraint constr0 exclude using btree (a with = ) not enforc...
+ ^
+alter table t add constraint constr0 exclude using btree (a with = ) enforced;
+ERROR: EXCLUDE constraints cannot be marked ENFORCED
+LINE 1: ...constraint constr0 exclude using btree (a with = ) enforced;
+ ^
+alter table t add constraint constr0 check (a > 1) not enforced enforced;
+ERROR: conflicting constraint properties
+LINE 1: ... add constraint constr0 check (a > 1) not enforced enforced;
+ ^
+alter table t1 add constraint constr0 foreign key(a) references t(a) not enforced;
+ERROR: FOREIGN KEY constraints cannot be marked NOT ENFORCED
+LINE 1: ...constraint constr0 foreign key(a) references t(a) not enforc...
+ ^
+alter table t1 add constraint constr0 foreign key(a) references t(a) enforced;
+ERROR: FOREIGN KEY constraints cannot be marked ENFORCED
+LINE 1: ... constraint constr0 foreign key(a) references t(a) enforced;
+ ^
+alter table t1 add constraint constr0 foreign key(a) references t(a) not enforced enforced;
+ERROR: conflicting constraint properties
+LINE 1: ...onstr0 foreign key(a) references t(a) not enforced enforced;
+ ^
+-- none of the above should be accepted
+drop table t1, t;
-- XXX: error message is misleading here
ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
ERROR: constraint "unique_tbl_i_key" of relation "unique_tbl" is not a foreign key constraint
diff --git a/src/test/regress/expected/create_table_like.out b/src/test/regress/expected/create_table_like.out
index e061389135..117afae727 100644
--- a/src/test/regress/expected/create_table_like.out
+++ b/src/test/regress/expected/create_table_like.out
@@ -540,6 +540,21 @@ Not-null constraints:
"noinh_con_copy_b_not_null" NOT NULL "b"
"noinh_con_copy_c_not_null" NOT NULL "c" NO INHERIT
+-- LIKE must respect NO INHERIT property of constraints
+CREATE TABLE check_not_enforced_copy(
+ a int constraint n1 CHECK (a > 0) NO INHERIT not enforced,
+ constraint n2 check (a > 0) enforced);
+CREATE TABLE check_not_enforced_copy1 (LIKE check_not_enforced_copy INCLUDING CONSTRAINTS);
+\d+ check_not_enforced_copy1
+ Table "public.check_not_enforced_copy1"
+ Column | Type | Collation | Nullable | Default | Storage | Stats target | Description
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a | integer | | | | plain | |
+Check constraints:
+ "n1" CHECK (a > 0) NO INHERIT NOT ENFORCED
+ "n2" CHECK (a > 0)
+
+drop table check_not_enforced_copy, check_not_enforced_copy1;
-- fail, as partitioned tables don't allow NO INHERIT constraints
CREATE TABLE noinh_con_copy1_parted (LIKE noinh_con_copy INCLUDING ALL)
PARTITION BY LIST (a);
diff --git a/src/test/regress/sql/constraints.sql b/src/test/regress/sql/constraints.sql
index 32c8b433bf..ee3e5eb106 100644
--- a/src/test/regress/sql/constraints.sql
+++ b/src/test/regress/sql/constraints.sql
@@ -534,6 +534,36 @@ SELECT * FROM unique_tbl;
-- enforcibility cannot be specified or set for unique constrain
CREATE TABLE UNIQUE_EN_TBL(i int UNIQUE ENFORCED);
CREATE TABLE UNIQUE_NOTEN_TBL(i int UNIQUE NOT ENFORCED);
+-- none of the following should be accepted
+create table t(a int not null enforced);
+create table t(a int not null enforced enforced);
+create table t(a int not null not enforced);
+create table t(a int unique enforced);
+create table t(a int unique not enforced);
+create table t(a int primary key enforced);
+create table t(a int primary key not enforced);
+create table t(a int generated by default as identity enforced);
+create table t(a int generated ALWAYS as (2) stored enforced);
+-- none of the above should be accepted
+create table t(a int primary key);
+create table t1(a int);
+-- none of the following should be accepted
+alter table t add constraint constr0 not null a not enforced;
+alter table t add constraint constr0 not null a enforced;
+alter table t add constraint constr0 unique (a) not enforced;
+alter table t add constraint constr0 unique (a) enforced;
+alter table t add constraint constr0 primary key (a) enforced;
+alter table t add constraint constr0 primary key (a) not enforced;
+alter table t add constraint constr0 primary key (a) not enforced;
+alter table t add constraint constr0 exclude using btree (a with = ) not enforced;
+alter table t add constraint constr0 exclude using btree (a with = ) enforced;
+alter table t add constraint constr0 check (a > 1) not enforced enforced;
+
+alter table t1 add constraint constr0 foreign key(a) references t(a) not enforced;
+alter table t1 add constraint constr0 foreign key(a) references t(a) enforced;
+alter table t1 add constraint constr0 foreign key(a) references t(a) not enforced enforced;
+-- none of the above should be accepted
+drop table t1, t;
-- XXX: error message is misleading here
ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORCED;
diff --git a/src/test/regress/sql/create_table_like.sql b/src/test/regress/sql/create_table_like.sql
index a41f8b83d7..460ba73574 100644
--- a/src/test/regress/sql/create_table_like.sql
+++ b/src/test/regress/sql/create_table_like.sql
@@ -200,6 +200,13 @@ CREATE TABLE noinh_con_copy (a int CHECK (a > 0) NO INHERIT, b int not null,
CREATE TABLE noinh_con_copy1 (LIKE noinh_con_copy INCLUDING CONSTRAINTS);
\d+ noinh_con_copy1
+-- LIKE must respect NO INHERIT property of constraints
+CREATE TABLE check_not_enforced_copy(
+ a int constraint n1 CHECK (a > 0) NO INHERIT not enforced,
+ constraint n2 check (a > 0) enforced);
+CREATE TABLE check_not_enforced_copy1 (LIKE check_not_enforced_copy INCLUDING CONSTRAINTS);
+\d+ check_not_enforced_copy1
+drop table check_not_enforced_copy, check_not_enforced_copy1;
-- fail, as partitioned tables don't allow NO INHERIT constraints
CREATE TABLE noinh_con_copy1_parted (LIKE noinh_con_copy INCLUDING ALL)
PARTITION BY LIST (a);
--
2.34.1
On 18.11.24 13:42, jian he wrote:
i only played around with
v4-0001-Add-support-for-NOT-ENFORCED-in-CHECK-constraints.patch.create table t(a int);
alter table t ADD CONSTRAINT the_constraint CHECK (a > 0) NOT ENFORCED;
insert into t select -1;
select conname, contype,condeferrable,condeferred, convalidated,
conenforced,conkey,connoinherit
from pg_constraint
where conrelid = 't'::regclass;pg_constraint->convalidated should be set to false for NOT ENFORCED constraint?
Am I missing something?
The "validated" status is irrelevant when the constraint is set to not
enforced. But it's probably still a good idea to make sure the field is
set consistently. I'm also leaning toward setting it to false. One
advantage of that would be that if you set the constraint to enforced
later, then it's automatically in the correct "not validated" state.
<varlistentry id="sql-createtable-parms-enforce">
<term><literal>ENFORCED</literal></term>
<term><literal>NOT ENFORCED</literal></term>
<listitem>
<para>
This is currently only allowed for <literal>CHECK</literal> constraints.
If the constraint is <literal>NOT ENFORCED</literal>, this clause
specifies that the constraint check will be skipped. When the constraint
is <literal>ENFORCED</literal>, check is performed after each statement.
This is the default.
</para>
</listitem>
</varlistentry>
"This is the default." kind of ambiguous?
I think you mean: by default, all constraints are implicit ENFORCED.
Maybe "the latter is the default" would be clearer.
+ ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("misplaced ENFORCED clause"), + parser_errposition(cxt->pstate, con->location)));+ ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("misplaced NOT ENFORCED clause"), + parser_errposition(cxt->pstate, con->location)));https://www.merriam-webster.com/dictionary/misplace
says:
"to put in a wrong or inappropriate place"I found the "misplaced" error message is not helpful.
for example:
CREATE TABLE UNIQUE_EN_TBL(i int UNIQUE ENFORCED);
ERROR: misplaced ENFORCED clause
the error message only tells us thatspecify ENFORCED is wrong.
but didn't say why it's wrong.we can saying that
"ENFORCED clauses can only be used for CHECK constraints"
This handling is similar to other error messages in
transformConstraintAttrs(). It could be slightly improved, but it's not
essential for this patch.
------------------------------------------------------------------
the following queries is a bug?drop table t;
create table t(a int);
alter table t ADD CONSTRAINT cc CHECK (a > 0) NOT ENFORCED;
insert into t select -1;
alter table t add constraint cc1 check (a > 1) not ENFORCED not ENFORCED;
ERROR: check constraint "cc1" of relation "t" is violated by some row
alter table t add constraint cc1 check (a > 1) not ENFORCED;
ERROR: check constraint "cc1" of relation "t" is violated by some row------------------------------------------------------------------
drop table t;
create table t(a int);
alter table t ADD CONSTRAINT cc CHECK (a > 0) NOT ENFORCED not enforced;seems not easy to make it fail with alter table multiple "not enforced".
I guess it should be fine.
since we disallow a mix of "not enforced" and "enforced".alter table t ADD CONSTRAINT cc CHECK (a > 0) NOT ENFORCED enforced;
------------------------------------------------------------------
Hmm, these duplicate clauses should have been caught by
transformConstraintAttrs().
typedef struct Constraint
{
NodeTag type;
ConstrType contype; /* see above */
char *conname; /* Constraint name, or NULL if unnamed */
bool deferrable; /* DEFERRABLE? */
bool initdeferred; /* INITIALLY DEFERRED? */
bool skip_validation; /* skip validation of existing rows? */
bool initially_valid; /* mark the new constraint as valid? */
bool is_enforced; /* enforced constraint? */
}
makeNode(Constraint) will default is_enforced to false.
Which makes the default value not what we want.
That means we may need to pay more attention for the trip from
makeNode(Constraint) to finally insert the constraint to the catalog.if we change it to is_not_enforced, makeNode will default to false.
is_not_enforced is false, means the constraint is enforced.
which is not that intuitive...
Yes, it could be safer to make the field so that the default is false.
I guess the skip_validation field is like that for a similar reason, but
I'm not sure.
------------------------------------------------------------------
do we need to update for "enforced" in
https://www.postgresql.org/docs/current/sql-keywords-appendix.html
?
------------------------------------------------------------------
That is generated automatically.
seems don't have
ALTER TABLE <name> VALIDATE CONSTRAINT
interacts with not forced sql tests.
for example:drop table if exists t;
create table t(a int);
alter table t add constraint cc check (a <> 1) not enforced NOT VALID;
insert into t values(1); ---success.
alter table t validate constraint cc;select conname,convalidated, conenforced
from pg_constraint
where conrelid = 't'::regclass;returns:
conname | convalidated | conenforced
---------+--------------+-------------
cc | t | fNow we have a value in the table "t" that violates the check
constraint, while convalidated is true.
----------------------------------------------------------------------------
I think we should prevent running VALIDATE for not enforced constraints.
I don't know what that would otherwise mean.
It's also questionable whether NOT VALID makes sense to specify.
On Mon, Nov 18, 2024 at 7:53 PM Peter Eisentraut <peter@eisentraut.org> wrote:
On 18.11.24 13:42, jian he wrote:
i only played around with
v4-0001-Add-support-for-NOT-ENFORCED-in-CHECK-constraints.patch.
Thanks for the review, and sorry for the delay — I was on vacation.
create table t(a int);
alter table t ADD CONSTRAINT the_constraint CHECK (a > 0) NOT ENFORCED;
insert into t select -1;
select conname, contype,condeferrable,condeferred, convalidated,
conenforced,conkey,connoinherit
from pg_constraint
where conrelid = 't'::regclass;pg_constraint->convalidated should be set to false for NOT ENFORCED constraint?
Am I missing something?The "validated" status is irrelevant when the constraint is set to not
enforced. But it's probably still a good idea to make sure the field is
set consistently. I'm also leaning toward setting it to false. One
advantage of that would be that if you set the constraint to enforced
later, then it's automatically in the correct "not validated" state.
I have implemented this approach in the attached version and added the
corresponding code comments and documentation. As a result, I moved
the conenforced and similar flags in another structure, placing them
before the respective validation flags.
<varlistentry id="sql-createtable-parms-enforce">
<term><literal>ENFORCED</literal></term>
<term><literal>NOT ENFORCED</literal></term>
<listitem>
<para>
This is currently only allowed for <literal>CHECK</literal> constraints.
If the constraint is <literal>NOT ENFORCED</literal>, this clause
specifies that the constraint check will be skipped. When the constraint
is <literal>ENFORCED</literal>, check is performed after each statement.
This is the default.
</para>
</listitem>
</varlistentry>
"This is the default." kind of ambiguous?
I think you mean: by default, all constraints are implicit ENFORCED.Maybe "the latter is the default" would be clearer.
Done.
+ ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("misplaced ENFORCED clause"), + parser_errposition(cxt->pstate, con->location)));+ ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("misplaced NOT ENFORCED clause"), + parser_errposition(cxt->pstate, con->location)));https://www.merriam-webster.com/dictionary/misplace
says:
"to put in a wrong or inappropriate place"I found the "misplaced" error message is not helpful.
for example:
CREATE TABLE UNIQUE_EN_TBL(i int UNIQUE ENFORCED);
ERROR: misplaced ENFORCED clause
the error message only tells us thatspecify ENFORCED is wrong.
but didn't say why it's wrong.we can saying that
"ENFORCED clauses can only be used for CHECK constraints"This handling is similar to other error messages in
transformConstraintAttrs(). It could be slightly improved, but it's not
essential for this patch.------------------------------------------------------------------
the following queries is a bug?drop table t;
create table t(a int);
NOT ENFORCED;
insert into t select -1;
alter table t add constraint cc1 check (a > 1) not ENFORCED not ENFORCED;
ERROR: check constraint "cc1" of relation "t" is violated by some row
alter table t add constraint cc1 check (a > 1) not ENFORCED;
ERROR: check constraint "cc1" of relation "t" is violated by some row------------------------------------------------------------------
drop table t;
create table t(a int);
alter table t ADD CONSTRAINT cc CHECK (a > 0) NOT ENFORCED not enforced;seems not easy to make it fail with alter table multiple "not enforced".
I guess it should be fine.
since we disallow a mix of "not enforced" and "enforced".alter table t ADD CONSTRAINT cc CHECK (a > 0) NOT ENFORCED enforced;
------------------------------------------------------------------Hmm, these duplicate clauses should have been caught by
transformConstraintAttrs().
transformConstraintAttrs() is used when adding constraints in CREATE
TABLE statements. I can see similar behavior with other flags as well.
Eg: alter table t ADD CONSTRAINT cc CHECK (a > 0) NOT DEFERRABLE NOT DEFERRABLE;
typedef struct Constraint
{
NodeTag type;
ConstrType contype; /* see above */
char *conname; /* Constraint name, or NULL if unnamed */
bool deferrable; /* DEFERRABLE? */
bool initdeferred; /* INITIALLY DEFERRED? */
bool skip_validation; /* skip validation of existing rows? */
bool initially_valid; /* mark the new constraint as valid? */
bool is_enforced; /* enforced constraint? */
}
makeNode(Constraint) will default is_enforced to false.
Which makes the default value not what we want.
That means we may need to pay more attention for the trip from
makeNode(Constraint) to finally insert the constraint to the catalog.if we change it to is_not_enforced, makeNode will default to false.
is_not_enforced is false, means the constraint is enforced.
which is not that intuitive...Yes, it could be safer to make the field so that the default is false.
I guess the skip_validation field is like that for a similar reason, but
I'm not sure.
Ok. Initially, I was doing it the same way, but to maintain consistency
with the pg_constraint column and avoid negation in multiple places, I
chose that approach. However, I agree that having the default to false
would be safer. I’ve renamed the flag to is_not_enforced. Other names
I considered were not_enforced or is_unenforced, but since we already
have existing flags with two underscores, is_not_enforced shouldn't be
a problem.
------------------------------------------------------------------
do we need to update for "enforced" in
https://www.postgresql.org/docs/current/sql-keywords-appendix.html
?
------------------------------------------------------------------That is generated automatically.
seems don't have
ALTER TABLE <name> VALIDATE CONSTRAINT
interacts with not forced sql tests.
for example:drop table if exists t;
create table t(a int);
alter table t add constraint cc check (a <> 1) not enforced NOT VALID;
insert into t values(1); ---success.
alter table t validate constraint cc;select conname,convalidated, conenforced
from pg_constraint
where conrelid = 't'::regclass;returns:
conname | convalidated | conenforced
---------+--------------+-------------
cc | t | fNow we have a value in the table "t" that violates the check
constraint, while convalidated is true.
----------------------------------------------------------------------------I think we should prevent running VALIDATE for not enforced constraints.
I don't know what that would otherwise mean.It's also questionable whether NOT VALID makes sense to specify.
In the attached version, now, throws an error on validation of a
non-enforced constraint, and the documentation has been updated to
describe this behavior.
I encountered an undesirable behavior with the existing code, where a
NOT VALID foreign key is not allowed on a partitioned table. I don’t
think this should be the case, so I tried removing that restriction
and found that the behavior is quite similar to a regular table with a
NOT VALID FK constraint. However, I still need to confirm the
necessity of this restriction. For now, I’ve bypassed the error for
not-enforced FK constraints and added a TODO. This is why patch 0005
is marked as WIP.
Regards,
Amul
Attachments:
v5-0001-Add-support-for-NOT-ENFORCED-in-CHECK-constraints.patchapplication/x-patch; name=v5-0001-Add-support-for-NOT-ENFORCED-in-CHECK-constraints.patchDownload
From 4fc0b1bd87035249313d271530df669762954812 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Tue, 3 Dec 2024 14:48:36 +0530
Subject: [PATCH v5 1/5] Add support for NOT ENFORCED in CHECK constraints.
This added basic infrastructure for the NOT ENFORCED/ENFORCED
constraint supports where incluing grammar and catalog flags.
Note that CHECK constraints do not currently support ALTER operations,
so changing the enforceability of an existing constraint isn't
possible without dropping and recreating it. We could consider
adding support for altering CHECK constraints either in this
patch series or as a separatly.
---
doc/src/sgml/catalogs.sgml | 10 ++
doc/src/sgml/ddl.sgml | 8 +-
doc/src/sgml/ref/alter_table.sgml | 14 +--
doc/src/sgml/ref/create_table.sgml | 25 ++++-
src/backend/access/common/tupdesc.c | 2 +
src/backend/catalog/heap.c | 26 +++--
src/backend/catalog/index.c | 1 +
src/backend/catalog/pg_constraint.c | 7 ++
src/backend/commands/tablecmds.c | 18 +++-
src/backend/commands/trigger.c | 1 +
src/backend/commands/typecmds.c | 16 +++
src/backend/executor/execMain.c | 8 +-
src/backend/parser/gram.y | 102 ++++++++++++++----
src/backend/parser/parse_utilcmd.c | 53 ++++++++-
src/backend/utils/adt/ruleutils.c | 2 +
src/backend/utils/cache/relcache.c | 1 +
src/include/access/tupdesc.h | 1 +
src/include/catalog/heap.h | 1 +
src/include/catalog/pg_constraint.h | 2 +
src/include/nodes/parsenodes.h | 3 +
src/include/parser/kwlist.h | 1 +
src/test/regress/expected/alter_table.out | 3 +
src/test/regress/expected/constraints.out | 52 ++++++++-
.../regress/expected/create_table_like.out | 10 +-
src/test/regress/sql/alter_table.sql | 2 +
src/test/regress/sql/constraints.sql | 27 ++++-
src/test/regress/sql/create_table_like.sql | 3 +-
27 files changed, 345 insertions(+), 54 deletions(-)
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index bf3cee08a93..a56ef4df562 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2602,6 +2602,16 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l
</para></entry>
</row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>conenforced</structfield> <type>bool</type>
+ </para>
+ <para>
+ Is the constraint enforceable?
+ Currently, can be false only for CHECK constraints
+ </para></entry>
+ </row>
+
<row>
<entry role="catalog_table_entry"><para role="column_definition">
<structfield>convalidated</structfield> <type>bool</type>
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index dea04d64db6..9a20396dae4 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -584,7 +584,10 @@ CREATE TABLE products (
consists of the key word <literal>CHECK</literal> followed by an
expression in parentheses. The check constraint expression should
involve the column thus constrained, otherwise the constraint
- would not make too much sense.
+ would not make too much sense. The check constraint expression can be
+ followed by the <literal>ENFORCED</literal>, which is the default, or
+ <literal>NOT ENFORCED</literal> keyword, determining whether the
+ constraint check is performed or skipped after each statement.
</para>
<indexterm>
@@ -720,7 +723,8 @@ CREATE TABLE products (
to implement that. (This approach avoids the dump/restore problem because
<application>pg_dump</application> does not reinstall triggers until after
restoring data, so that the check will not be enforced during a
- dump/restore.)
+ dump/restore.) You can add that CHECK constraint with <literal>NOT ENFORCED</literal>,
+ allowing the underlying constraint rule to be represented without being enforced.
</para>
</note>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index c8f7ab7d956..ca38937cf1e 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -58,7 +58,7 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable>
ALTER [ COLUMN ] <replaceable class="parameter">column_name</replaceable> SET COMPRESSION <replaceable class="parameter">compression_method</replaceable>
ADD <replaceable class="parameter">table_constraint</replaceable> [ NOT VALID ]
ADD <replaceable class="parameter">table_constraint_using_index</replaceable>
- ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+ ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]
VALIDATE CONSTRAINT <replaceable class="parameter">constraint_name</replaceable>
DROP CONSTRAINT [ IF EXISTS ] <replaceable class="parameter">constraint_name</replaceable> [ RESTRICT | CASCADE ]
DISABLE TRIGGER [ <replaceable class="parameter">trigger_name</replaceable> | ALL | USER ]
@@ -108,7 +108,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
PRIMARY KEY <replaceable class="parameter">index_parameters</replaceable> |
REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> ) ] [ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ]
[ ON DELETE <replaceable class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ] }
-[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]
<phrase>and <replaceable class="parameter">table_constraint</replaceable> is:</phrase>
@@ -120,7 +120,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
EXCLUDE [ USING <replaceable class="parameter">index_method</replaceable> ] ( <replaceable class="parameter">exclude_element</replaceable> WITH <replaceable class="parameter">operator</replaceable> [, ... ] ) <replaceable class="parameter">index_parameters</replaceable> [ WHERE ( <replaceable class="parameter">predicate</replaceable> ) ] |
FOREIGN KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">column_name</replaceable> ] ) REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">refcolumn</replaceable> ] ) ]
[ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ] [ ON DELETE <replaceable class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ] }
-[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]
<phrase>and <replaceable class="parameter">table_constraint_using_index</replaceable> is:</phrase>
@@ -1423,9 +1423,11 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</para>
<para>
- Adding a <literal>CHECK</literal> or <literal>NOT NULL</literal> constraint requires
- scanning the table to verify that existing rows meet the constraint,
- but does not require a table rewrite.
+ Adding an enforced <literal>CHECK</literal> or <literal>NOT NULL</literal>
+ constraint requires scanning the table to verify that existing rows meet the
+ constraint, but does not require a table rewrite. If a <literal>CHECK</literal>
+ constraint is added as <literal>NOT ENFORCED</literal>, the validation will
+ not be performed.
</para>
<para>
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index 70fa929caa4..cfd01c4ef2b 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -71,7 +71,7 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI
PRIMARY KEY <replaceable class="parameter">index_parameters</replaceable> |
REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> ) ] [ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ]
[ ON DELETE <replaceable class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ] }
-[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]
<phrase>and <replaceable class="parameter">table_constraint</replaceable> is:</phrase>
@@ -84,7 +84,7 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI
FOREIGN KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">column_name</replaceable> ] ) REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">refcolumn</replaceable> ] ) ]
[ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ] [ ON DELETE <replaceable
class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ] }
-[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]
<phrase>and <replaceable class="parameter">like_option</replaceable> is:</phrase>
@@ -1377,6 +1377,27 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</listitem>
</varlistentry>
+ <varlistentry id="sql-createtable-parms-enforce">
+ <term><literal>ENFORCED</literal></term>
+ <term><literal>NOT ENFORCED</literal></term>
+ <listitem>
+ <para>
+ This is currently only allowed for <literal>CHECK</literal> constraints.
+ If the constraint is <literal>NOT ENFORCED</literal>, this clause
+ specifies that the constraint check will be skipped. When the constraint
+ is <literal>ENFORCED</literal>, check is performed after each statement;
+ the latter is the default.
+ </para>
+ <para>
+ Note that if the constraint is <literal>NOT ENFORCED</literal>, it is also
+ marked as <literal>NOT VALID</literal>. While this might seem unnecessary,
+ it is done for consistency and should be set accordingly. This ensures
+ that if the constraint is later changed to <literal>ENFORCED</literal>, it
+ will automatically be in the correct <literal>NOT VALID</literal> state.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="sql-createtable-method">
<term><literal>USING <replaceable class="parameter">method</replaceable></literal></term>
<listitem>
diff --git a/src/backend/access/common/tupdesc.c b/src/backend/access/common/tupdesc.c
index 47379fef220..6c64e5b9e71 100644
--- a/src/backend/access/common/tupdesc.c
+++ b/src/backend/access/common/tupdesc.c
@@ -224,6 +224,7 @@ CreateTupleDescCopyConstr(TupleDesc tupdesc)
{
cpy->check[i].ccname = pstrdup(constr->check[i].ccname);
cpy->check[i].ccbin = pstrdup(constr->check[i].ccbin);
+ cpy->check[i].ccenforced = constr->check[i].ccenforced;
cpy->check[i].ccvalid = constr->check[i].ccvalid;
cpy->check[i].ccnoinherit = constr->check[i].ccnoinherit;
}
@@ -547,6 +548,7 @@ equalTupleDescs(TupleDesc tupdesc1, TupleDesc tupdesc2)
if (!(strcmp(check1->ccname, check2->ccname) == 0 &&
strcmp(check1->ccbin, check2->ccbin) == 0 &&
+ check1->ccenforced == check2->ccenforced &&
check1->ccvalid == check2->ccvalid &&
check1->ccnoinherit == check2->ccnoinherit))
return false;
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index d7b88b61dcc..fdf0818348f 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -101,9 +101,9 @@ static ObjectAddress AddNewRelationType(const char *typeName,
Oid new_row_type,
Oid new_array_type);
static void RelationRemoveInheritance(Oid relid);
-static Oid StoreRelCheck(Relation rel, const char *ccname, Node *expr,
- bool is_validated, bool is_local, int16 inhcount,
- bool is_no_inherit, bool is_internal);
+static Oid StoreRelCheck(Relation rel, const char *ccname, Node *expr,
+ bool is_not_enforced, bool is_validated, bool is_local,
+ int16 inhcount, bool is_no_inherit, bool is_internal);
static void StoreConstraints(Relation rel, List *cooked_constraints,
bool is_internal);
static bool MergeWithExistingConstraint(Relation rel, const char *ccname, Node *expr,
@@ -2074,8 +2074,8 @@ SetAttrMissing(Oid relid, char *attname, char *value)
*/
static Oid
StoreRelCheck(Relation rel, const char *ccname, Node *expr,
- bool is_validated, bool is_local, int16 inhcount,
- bool is_no_inherit, bool is_internal)
+ bool is_not_enforced, bool is_validated, bool is_local,
+ int16 inhcount, bool is_no_inherit, bool is_internal)
{
char *ccbin;
List *varList;
@@ -2140,6 +2140,7 @@ StoreRelCheck(Relation rel, const char *ccname, Node *expr,
CONSTRAINT_CHECK, /* Constraint Type */
false, /* Is Deferrable */
false, /* Is Deferred */
+ !is_not_enforced, /* Is Enforced */
is_validated,
InvalidOid, /* no parent constraint */
RelationGetRelid(rel), /* relation */
@@ -2193,6 +2194,7 @@ StoreRelNotNull(Relation rel, const char *nnname, AttrNumber attnum,
CONSTRAINT_NOTNULL,
false,
false,
+ true, /* Is Enforced */
is_validated,
InvalidOid,
RelationGetRelid(rel),
@@ -2262,9 +2264,9 @@ StoreConstraints(Relation rel, List *cooked_constraints, bool is_internal)
case CONSTR_CHECK:
con->conoid =
StoreRelCheck(rel, con->name, con->expr,
- !con->skip_validation, con->is_local,
- con->inhcount, con->is_no_inherit,
- is_internal);
+ con->is_not_enforced, !con->skip_validation,
+ con->is_local, con->inhcount,
+ con->is_no_inherit, is_internal);
numchecks++;
break;
@@ -2405,6 +2407,7 @@ AddRelationNewConstraints(Relation rel,
cooked->name = NULL;
cooked->attnum = colDef->attnum;
cooked->expr = expr;
+ cooked->is_not_enforced = false;
cooked->skip_validation = false;
cooked->is_local = is_local;
cooked->inhcount = is_local ? 0 : 1;
@@ -2524,8 +2527,10 @@ AddRelationNewConstraints(Relation rel,
* OK, store it.
*/
constrOid =
- StoreRelCheck(rel, ccname, expr, cdef->initially_valid, is_local,
- is_local ? 0 : 1, cdef->is_no_inherit, is_internal);
+ StoreRelCheck(rel, ccname, expr, cdef->is_not_enforced,
+ cdef->initially_valid, is_local,
+ is_local ? 0 : 1, cdef->is_no_inherit,
+ is_internal);
numchecks++;
@@ -2535,6 +2540,7 @@ AddRelationNewConstraints(Relation rel,
cooked->name = ccname;
cooked->attnum = 0;
cooked->expr = expr;
+ cooked->is_not_enforced = cdef->is_not_enforced;
cooked->skip_validation = cdef->skip_validation;
cooked->is_local = is_local;
cooked->inhcount = is_local ? 0 : 1;
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 1c3a9e06d37..f4cbaf8953a 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -1957,6 +1957,7 @@ index_constraint_create(Relation heapRelation,
constraintType,
deferrable,
initdeferred,
+ true, /* Is Enforced */
true,
parentConstraintId,
RelationGetRelid(heapRelation),
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index 9c05a98d28c..f9e4c3d3cfa 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -53,6 +53,7 @@ CreateConstraintEntry(const char *constraintName,
char constraintType,
bool isDeferrable,
bool isDeferred,
+ bool isEnforced,
bool isValidated,
Oid parentConstrId,
Oid relId,
@@ -99,6 +100,11 @@ CreateConstraintEntry(const char *constraintName,
ObjectAddresses *addrs_auto;
ObjectAddresses *addrs_normal;
+ /* Only CHECK constraint can be not enforced */
+ Assert(isEnforced || constraintType == CONSTRAINT_CHECK);
+ /* NOT ENFORCED constraint must be NOT VALID */
+ Assert(isEnforced || !isValidated);
+
conDesc = table_open(ConstraintRelationId, RowExclusiveLock);
Assert(constraintName);
@@ -182,6 +188,7 @@ CreateConstraintEntry(const char *constraintName,
values[Anum_pg_constraint_contype - 1] = CharGetDatum(constraintType);
values[Anum_pg_constraint_condeferrable - 1] = BoolGetDatum(isDeferrable);
values[Anum_pg_constraint_condeferred - 1] = BoolGetDatum(isDeferred);
+ values[Anum_pg_constraint_conenforced - 1] = BoolGetDatum(isEnforced);
values[Anum_pg_constraint_convalidated - 1] = BoolGetDatum(isValidated);
values[Anum_pg_constraint_conrelid - 1] = ObjectIdGetDatum(relId);
values[Anum_pg_constraint_contypid - 1] = ObjectIdGetDatum(domainId);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 6ccae4cb4a8..f33d4f8db56 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -373,7 +373,7 @@ static void RangeVarCallbackForTruncate(const RangeVar *relation,
static List *MergeAttributes(List *columns, const List *supers, char relpersistence,
bool is_partition, List **supconstr,
List **supnotnulls);
-static List *MergeCheckConstraint(List *constraints, const char *name, Node *expr);
+static List *MergeCheckConstraint(List *constraints, const char *name, Node *expr, bool is_not_enforced);
static void MergeChildAttribute(List *inh_columns, int exist_attno, int newcol_attno, const ColumnDef *newdef);
static ColumnDef *MergeInheritedAttribute(List *inh_columns, int exist_attno, const ColumnDef *newdef);
static void MergeAttributesIntoExisting(Relation child_rel, Relation parent_rel, bool ispartition);
@@ -2885,7 +2885,8 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
name,
RelationGetRelationName(relation))));
- constraints = MergeCheckConstraint(constraints, name, expr);
+ constraints = MergeCheckConstraint(constraints, name, expr,
+ !check[i].ccenforced);
}
}
@@ -3099,7 +3100,8 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
* the list.
*/
static List *
-MergeCheckConstraint(List *constraints, const char *name, Node *expr)
+MergeCheckConstraint(List *constraints, const char *name, Node *expr,
+ bool is_not_enforced)
{
ListCell *lc;
CookedConstraint *newcon;
@@ -3140,6 +3142,8 @@ MergeCheckConstraint(List *constraints, const char *name, Node *expr)
newcon->name = pstrdup(name);
newcon->expr = expr;
newcon->inhcount = 1;
+ newcon->is_not_enforced = is_not_enforced;
+ newcon->skip_validation = is_not_enforced;
return lappend(constraints, newcon);
}
@@ -10418,6 +10422,7 @@ addFkConstraint(addFkConstraintSides fkside,
CONSTRAINT_FOREIGN,
fkconstraint->deferrable,
fkconstraint->initdeferred,
+ true, /* Is Enforced */
fkconstraint->initially_valid,
parentConstr,
RelationGetRelid(rel),
@@ -12004,6 +12009,11 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
errmsg("constraint \"%s\" of relation \"%s\" is not a foreign key or check constraint",
constrName, RelationGetRelationName(rel))));
+ if (!con->conenforced)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot validated NOT ENFORCED constraint")));
+
if (!con->convalidated)
{
AlteredTableInfo *tab;
@@ -16258,6 +16268,7 @@ constraints_equivalent(HeapTuple a, HeapTuple b, TupleDesc tupleDesc)
if (acon->condeferrable != bcon->condeferrable ||
acon->condeferred != bcon->condeferred ||
+ acon->conenforced != bcon->conenforced ||
strcmp(decompile_conbin(a, tupleDesc),
decompile_conbin(b, tupleDesc)) != 0)
return false;
@@ -20185,6 +20196,7 @@ DetachAddConstraintIfNeeded(List **wqueue, Relation partRel)
n->is_no_inherit = false;
n->raw_expr = NULL;
n->cooked_expr = nodeToString(make_ands_explicit(constraintExpr));
+ n->is_not_enforced = false;
n->initially_valid = true;
n->skip_validation = true;
/* It's a re-add, since it nominally already exists */
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 09356e46d16..49d3fc22983 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -809,6 +809,7 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
CONSTRAINT_TRIGGER,
stmt->deferrable,
stmt->initdeferred,
+ true, /* Is Enforced */
true,
InvalidOid, /* no parent */
RelationGetRelid(rel),
diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c
index 971a8a1ebc5..27ca970fe40 100644
--- a/src/backend/commands/typecmds.c
+++ b/src/backend/commands/typecmds.c
@@ -1011,6 +1011,13 @@ DefineDomain(CreateDomainStmt *stmt)
errmsg("specifying constraint deferrability not supported for domains")));
break;
+ case CONSTR_ATTR_ENFORCED:
+ case CONSTR_ATTR_NOT_ENFORCED:
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("specifying constraint enforceability not supported for domains")));
+ break;
+
default:
elog(ERROR, "unrecognized constraint subtype: %d",
(int) constr->contype);
@@ -2971,6 +2978,13 @@ AlterDomainAddConstraint(List *names, Node *newConstraint,
errmsg("specifying constraint deferrability not supported for domains")));
break;
+ case CONSTR_ATTR_ENFORCED:
+ case CONSTR_ATTR_NOT_ENFORCED:
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("specifying constraint enforceability not supported for domains")));
+ break;
+
default:
elog(ERROR, "unrecognized constraint subtype: %d",
(int) constr->contype);
@@ -3600,6 +3614,7 @@ domainAddCheckConstraint(Oid domainOid, Oid domainNamespace, Oid baseTypeOid,
CONSTRAINT_CHECK, /* Constraint Type */
false, /* Is Deferrable */
false, /* Is Deferred */
+ true, /* Is Enforced */
!constr->skip_validation, /* Is Validated */
InvalidOid, /* no parent constraint */
InvalidOid, /* not a relation constraint */
@@ -3707,6 +3722,7 @@ domainAddNotNullConstraint(Oid domainOid, Oid domainNamespace, Oid baseTypeOid,
CONSTRAINT_NOTNULL, /* Constraint Type */
false, /* Is Deferrable */
false, /* Is Deferred */
+ true, /* Is Enforced */
!constr->skip_validation, /* Is Validated */
InvalidOid, /* no parent constraint */
InvalidOid, /* not a relation constraint */
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 5ca856fd279..1878f4b8d94 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1751,11 +1751,15 @@ ExecRelCheck(ResultRelInfo *resultRelInfo,
{
oldContext = MemoryContextSwitchTo(estate->es_query_cxt);
resultRelInfo->ri_ConstraintExprs =
- (ExprState **) palloc(ncheck * sizeof(ExprState *));
+ (ExprState **) palloc0(ncheck * sizeof(ExprState *));
for (i = 0; i < ncheck; i++)
{
Expr *checkconstr;
+ /* Skip not enforced constraint */
+ if (!check[i].ccenforced)
+ continue;
+
checkconstr = stringToNode(check[i].ccbin);
resultRelInfo->ri_ConstraintExprs[i] =
ExecPrepareExpr(checkconstr, estate);
@@ -1782,7 +1786,7 @@ ExecRelCheck(ResultRelInfo *resultRelInfo,
* is not to be treated as a failure. Therefore, use ExecCheck not
* ExecQual.
*/
- if (!ExecCheck(checkconstr, econtext))
+ if (checkconstr && !ExecCheck(checkconstr, econtext))
return check[i].ccname;
}
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 67eb96396af..a0d5e181ed9 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -143,6 +143,8 @@ typedef struct KeyActions
#define CAS_INITIALLY_DEFERRED 0x08
#define CAS_NOT_VALID 0x10
#define CAS_NO_INHERIT 0x20
+#define CAS_NOT_ENFORCED 0x40
+#define CAS_ENFORCED 0x80
#define parser_yyerror(msg) scanner_yyerror(msg, yyscanner)
@@ -196,8 +198,8 @@ static void SplitColQualList(List *qualList,
List **constraintList, CollateClause **collClause,
core_yyscan_t yyscanner);
static void processCASbits(int cas_bits, int location, const char *constrType,
- bool *deferrable, bool *initdeferred, bool *not_valid,
- bool *no_inherit, core_yyscan_t yyscanner);
+ bool *deferrable, bool *initdeferred, bool *is_not_enforced,
+ bool *not_valid, bool *no_inherit, core_yyscan_t yyscanner);
static PartitionStrategy parsePartitionStrategy(char *strategy, int location,
core_yyscan_t yyscanner);
static void preprocess_pubobj_list(List *pubobjspec_list,
@@ -711,9 +713,9 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
DETACH DICTIONARY DISABLE_P DISCARD DISTINCT DO DOCUMENT_P DOMAIN_P
DOUBLE_P DROP
- EACH ELSE EMPTY_P ENABLE_P ENCODING ENCRYPTED END_P ENUM_P ERROR_P ESCAPE
- EVENT EXCEPT EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION
- EXTENSION EXTERNAL EXTRACT
+ EACH ELSE EMPTY_P ENABLE_P ENCODING ENCRYPTED END_P ENFORCED ENUM_P ERROR_P
+ ESCAPE EVENT EXCEPT EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN
+ EXPRESSION EXTENSION EXTERNAL EXTRACT
FALSE_P FAMILY FETCH FILTER FINALIZE FIRST_P FLOAT_P FOLLOWING FOR
FORCE FOREIGN FORMAT FORWARD FREEZE FROM FULL FUNCTION FUNCTIONS
@@ -2658,7 +2660,7 @@ alter_table_cmd:
processCASbits($4, @4, "ALTER CONSTRAINT statement",
&c->deferrable,
&c->initdeferred,
- NULL, NULL, yyscanner);
+ NULL, NULL, NULL, yyscanner);
$$ = (Node *) n;
}
/* ALTER TABLE <name> VALIDATE CONSTRAINT ... */
@@ -4087,6 +4089,22 @@ ConstraintAttr:
n->location = @1;
$$ = (Node *) n;
}
+ | ENFORCED
+ {
+ Constraint *n = makeNode(Constraint);
+
+ n->contype = CONSTR_ATTR_ENFORCED;
+ n->location = @1;
+ $$ = (Node *) n;
+ }
+ | NOT ENFORCED
+ {
+ Constraint *n = makeNode(Constraint);
+
+ n->contype = CONSTR_ATTR_NOT_ENFORCED;
+ n->location = @1;
+ $$ = (Node *) n;
+ }
;
@@ -4148,7 +4166,8 @@ ConstraintElem:
n->raw_expr = $3;
n->cooked_expr = NULL;
processCASbits($5, @5, "CHECK",
- NULL, NULL, &n->skip_validation,
+ NULL, NULL, &n->is_not_enforced,
+ &n->skip_validation,
&n->is_no_inherit, yyscanner);
n->initially_valid = !n->skip_validation;
$$ = (Node *) n;
@@ -4162,7 +4181,7 @@ ConstraintElem:
n->keys = list_make1(makeString($3));
/* no NOT VALID support yet */
processCASbits($4, @4, "NOT NULL",
- NULL, NULL, NULL,
+ NULL, NULL, NULL, NULL,
&n->is_no_inherit, yyscanner);
n->initially_valid = true;
$$ = (Node *) n;
@@ -4183,7 +4202,7 @@ ConstraintElem:
n->indexspace = $9;
processCASbits($10, @10, "UNIQUE",
&n->deferrable, &n->initdeferred, NULL,
- NULL, yyscanner);
+ NULL, NULL, yyscanner);
$$ = (Node *) n;
}
| UNIQUE ExistingIndex ConstraintAttributeSpec
@@ -4199,7 +4218,7 @@ ConstraintElem:
n->indexspace = NULL;
processCASbits($3, @3, "UNIQUE",
&n->deferrable, &n->initdeferred, NULL,
- NULL, yyscanner);
+ NULL, NULL, yyscanner);
$$ = (Node *) n;
}
| PRIMARY KEY '(' columnList opt_without_overlaps ')' opt_c_include opt_definition OptConsTableSpace
@@ -4217,7 +4236,7 @@ ConstraintElem:
n->indexspace = $9;
processCASbits($10, @10, "PRIMARY KEY",
&n->deferrable, &n->initdeferred, NULL,
- NULL, yyscanner);
+ NULL, NULL, yyscanner);
$$ = (Node *) n;
}
| PRIMARY KEY ExistingIndex ConstraintAttributeSpec
@@ -4233,7 +4252,7 @@ ConstraintElem:
n->indexspace = NULL;
processCASbits($4, @4, "PRIMARY KEY",
&n->deferrable, &n->initdeferred, NULL,
- NULL, yyscanner);
+ NULL, NULL, yyscanner);
$$ = (Node *) n;
}
| EXCLUDE access_method_clause '(' ExclusionConstraintList ')'
@@ -4253,7 +4272,7 @@ ConstraintElem:
n->where_clause = $9;
processCASbits($10, @10, "EXCLUDE",
&n->deferrable, &n->initdeferred, NULL,
- NULL, yyscanner);
+ NULL, NULL, yyscanner);
$$ = (Node *) n;
}
| FOREIGN KEY '(' columnList optionalPeriodName ')' REFERENCES qualified_name
@@ -4282,7 +4301,8 @@ ConstraintElem:
n->fk_del_set_cols = ($11)->deleteAction->cols;
processCASbits($12, @12, "FOREIGN KEY",
&n->deferrable, &n->initdeferred,
- &n->skip_validation, NULL,
+ NULL, &n->skip_validation,
+ NULL,
yyscanner);
n->initially_valid = !n->skip_validation;
$$ = (Node *) n;
@@ -4322,7 +4342,7 @@ DomainConstraintElem:
n->raw_expr = $3;
n->cooked_expr = NULL;
processCASbits($5, @5, "CHECK",
- NULL, NULL, &n->skip_validation,
+ NULL, NULL, NULL, &n->skip_validation,
&n->is_no_inherit, yyscanner);
n->initially_valid = !n->skip_validation;
$$ = (Node *) n;
@@ -4337,7 +4357,7 @@ DomainConstraintElem:
/* no NOT VALID, NO INHERIT support */
processCASbits($3, @3, "NOT NULL",
NULL, NULL, NULL,
- NULL, yyscanner);
+ NULL, NULL, yyscanner);
n->initially_valid = true;
$$ = (Node *) n;
}
@@ -6000,7 +6020,7 @@ CreateTrigStmt:
n->transitionRels = NIL;
processCASbits($11, @11, "TRIGGER",
&n->deferrable, &n->initdeferred, NULL,
- NULL, yyscanner);
+ NULL, NULL, yyscanner);
n->constrrel = $10;
$$ = (Node *) n;
}
@@ -6169,7 +6189,8 @@ ConstraintAttributeSpec:
parser_errposition(@2)));
/* generic message for other conflicts */
if ((newspec & (CAS_NOT_DEFERRABLE | CAS_DEFERRABLE)) == (CAS_NOT_DEFERRABLE | CAS_DEFERRABLE) ||
- (newspec & (CAS_INITIALLY_IMMEDIATE | CAS_INITIALLY_DEFERRED)) == (CAS_INITIALLY_IMMEDIATE | CAS_INITIALLY_DEFERRED))
+ (newspec & (CAS_INITIALLY_IMMEDIATE | CAS_INITIALLY_DEFERRED)) == (CAS_INITIALLY_IMMEDIATE | CAS_INITIALLY_DEFERRED) ||
+ (newspec & (CAS_NOT_ENFORCED | CAS_ENFORCED)) == (CAS_NOT_ENFORCED | CAS_ENFORCED))
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("conflicting constraint properties"),
@@ -6185,6 +6206,8 @@ ConstraintAttributeElem:
| INITIALLY DEFERRED { $$ = CAS_INITIALLY_DEFERRED; }
| NOT VALID { $$ = CAS_NOT_VALID; }
| NO INHERIT { $$ = CAS_NO_INHERIT; }
+ | NOT ENFORCED { $$ = CAS_NOT_ENFORCED; }
+ | ENFORCED { $$ = CAS_ENFORCED; }
;
@@ -17688,6 +17711,7 @@ unreserved_keyword:
| ENABLE_P
| ENCODING
| ENCRYPTED
+ | ENFORCED
| ENUM_P
| ERROR_P
| ESCAPE
@@ -18265,6 +18289,7 @@ bare_label_keyword:
| ENCODING
| ENCRYPTED
| END_P
+ | ENFORCED
| ENUM_P
| ERROR_P
| ESCAPE
@@ -19403,8 +19428,8 @@ SplitColQualList(List *qualList,
*/
static void
processCASbits(int cas_bits, int location, const char *constrType,
- bool *deferrable, bool *initdeferred, bool *not_valid,
- bool *no_inherit, core_yyscan_t yyscanner)
+ bool *deferrable, bool *initdeferred, bool *is_not_enforced,
+ bool *not_valid, bool *no_inherit, core_yyscan_t yyscanner)
{
/* defaults */
if (deferrable)
@@ -19413,6 +19438,8 @@ processCASbits(int cas_bits, int location, const char *constrType,
*initdeferred = false;
if (not_valid)
*not_valid = false;
+ if (is_not_enforced)
+ *is_not_enforced = false;
if (cas_bits & (CAS_DEFERRABLE | CAS_INITIALLY_DEFERRED))
{
@@ -19465,6 +19492,41 @@ processCASbits(int cas_bits, int location, const char *constrType,
constrType),
parser_errposition(location)));
}
+
+ if (cas_bits & CAS_NOT_ENFORCED)
+ {
+ if (is_not_enforced)
+ *is_not_enforced = true;
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ /* translator: %s is CHECK, UNIQUE, or similar */
+ errmsg("%s constraints cannot be marked NOT ENFORCED",
+ constrType),
+ parser_errposition(location)));
+ }
+
+ if (cas_bits & CAS_ENFORCED)
+ {
+ if (is_not_enforced)
+ *is_not_enforced = false;
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ /* translator: %s is CHECK, UNIQUE, or similar */
+ errmsg("%s constraints cannot be marked ENFORCED",
+ constrType),
+ parser_errposition(location)));
+ }
+
+ /*
+ * NB: The not_valid status is irrelevant when the constraint is set to
+ * NOT ENFORCED, but for consistency, it should be set accordingly.
+ * This ensures that if the constraint is later changed to ENFORCED, it
+ * will automatically be in the correct NOT VALID state.
+ */
+ if (is_not_enforced && *is_not_enforced && not_valid)
+ *not_valid = true;
}
/*
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 0f324ee4e31..e629c3186ef 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -954,6 +954,8 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column)
case CONSTR_ATTR_NOT_DEFERRABLE:
case CONSTR_ATTR_DEFERRED:
case CONSTR_ATTR_IMMEDIATE:
+ case CONSTR_ATTR_ENFORCED:
+ case CONSTR_ATTR_NOT_ENFORCED:
/* transformConstraintAttrs took care of these */
break;
@@ -1093,6 +1095,8 @@ transformTableConstraint(CreateStmtContext *cxt, Constraint *constraint)
case CONSTR_ATTR_NOT_DEFERRABLE:
case CONSTR_ATTR_DEFERRED:
case CONSTR_ATTR_IMMEDIATE:
+ case CONSTR_ATTR_ENFORCED:
+ case CONSTR_ATTR_NOT_ENFORCED:
elog(ERROR, "invalid context for constraint type %d",
constraint->contype);
break;
@@ -1433,6 +1437,8 @@ expandTableLikeClause(RangeVar *heapRel, TableLikeClause *table_like_clause)
{
char *ccname = constr->check[ccnum].ccname;
char *ccbin = constr->check[ccnum].ccbin;
+ bool ccenforced = constr->check[ccnum].ccenforced;
+ bool ccvalid = constr->check[ccnum].ccvalid;
bool ccnoinherit = constr->check[ccnum].ccnoinherit;
Node *ccbin_node;
bool found_whole_row;
@@ -1462,13 +1468,14 @@ expandTableLikeClause(RangeVar *heapRel, TableLikeClause *table_like_clause)
n->contype = CONSTR_CHECK;
n->conname = pstrdup(ccname);
n->location = -1;
+ n->is_not_enforced = !ccenforced;
+ n->initially_valid = ccvalid;
n->is_no_inherit = ccnoinherit;
n->raw_expr = NULL;
n->cooked_expr = nodeToString(ccbin_node);
/* We can skip validation, since the new table should be empty. */
n->skip_validation = true;
- n->initially_valid = true;
atsubcmd = makeNode(AlterTableCmd);
atsubcmd->subtype = AT_AddConstraint;
@@ -2921,9 +2928,11 @@ transformCheckConstraints(CreateStmtContext *cxt, bool skipValidation)
return;
/*
- * If creating a new table (but not a foreign table), we can safely skip
- * validation of check constraints, and nonetheless mark them valid. (This
- * will override any user-supplied NOT VALID flag.)
+ * When creating a new table (but not a foreign table), we can safely skip
+ * the validation of check constraints and mark them as valid based on the
+ * constraint enforcement flag, since NOT ENFORCED constraints must always
+ * be marked as NOT VALID. (This will override any user-supplied
+ * NOT VALID flag.)
*/
if (skipValidation)
{
@@ -2932,7 +2941,7 @@ transformCheckConstraints(CreateStmtContext *cxt, bool skipValidation)
Constraint *constraint = (Constraint *) lfirst(ckclist);
constraint->skip_validation = true;
- constraint->initially_valid = true;
+ constraint->initially_valid = !constraint->is_not_enforced;
}
}
}
@@ -3859,6 +3868,7 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
Constraint *lastprimarycon = NULL;
bool saw_deferrability = false;
bool saw_initially = false;
+ bool saw_enforced = false;
ListCell *clist;
#define SUPPORTS_ATTRS(node) \
@@ -3954,12 +3964,45 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
lastprimarycon->initdeferred = false;
break;
+ case CONSTR_ATTR_ENFORCED:
+ if (lastprimarycon &&
+ lastprimarycon->contype != CONSTR_CHECK)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("misplaced ENFORCED clause"),
+ parser_errposition(cxt->pstate, con->location)));
+ if (saw_enforced)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("multiple ENFORCED/NOT ENFORCED clauses not allowed"),
+ parser_errposition(cxt->pstate, con->location)));
+ saw_enforced = true;
+ lastprimarycon->is_not_enforced = false;
+ break;
+
+ case CONSTR_ATTR_NOT_ENFORCED:
+ if (lastprimarycon &&
+ lastprimarycon->contype != CONSTR_CHECK)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("misplaced NOT ENFORCED clause"),
+ parser_errposition(cxt->pstate, con->location)));
+ if (saw_enforced)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("multiple ENFORCED/NOT ENFORCED clauses not allowed"),
+ parser_errposition(cxt->pstate, con->location)));
+ saw_enforced = true;
+ lastprimarycon->is_not_enforced = true;
+ break;
+
default:
/* Otherwise it's not an attribute */
lastprimarycon = con;
/* reset flags for new primary node */
saw_deferrability = false;
saw_initially = false;
+ saw_enforced = false;
break;
}
}
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 2194ab3dfa5..457b355ac4c 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -2591,6 +2591,8 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
appendStringInfoString(&buf, " DEFERRABLE");
if (conForm->condeferred)
appendStringInfoString(&buf, " INITIALLY DEFERRED");
+ if (!conForm->conenforced)
+ appendStringInfoString(&buf, " NOT ENFORCED");
if (!conForm->convalidated)
appendStringInfoString(&buf, " NOT VALID");
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index d0892cee24d..f4721b07f90 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4585,6 +4585,7 @@ CheckConstraintFetch(Relation relation)
break;
}
+ check[found].ccenforced = conform->conenforced;
check[found].ccvalid = conform->convalidated;
check[found].ccnoinherit = conform->connoinherit;
check[found].ccname = MemoryContextStrdup(CacheMemoryContext,
diff --git a/src/include/access/tupdesc.h b/src/include/access/tupdesc.h
index 8930a28d660..908f8e7a57c 100644
--- a/src/include/access/tupdesc.h
+++ b/src/include/access/tupdesc.h
@@ -29,6 +29,7 @@ typedef struct ConstrCheck
{
char *ccname;
char *ccbin; /* nodeToString representation of expr */
+ bool ccenforced;
bool ccvalid;
bool ccnoinherit; /* this is a non-inheritable constraint */
} ConstrCheck;
diff --git a/src/include/catalog/heap.h b/src/include/catalog/heap.h
index 8c278f202b4..ff670137b14 100644
--- a/src/include/catalog/heap.h
+++ b/src/include/catalog/heap.h
@@ -40,6 +40,7 @@ typedef struct CookedConstraint
char *name; /* name, or NULL if none */
AttrNumber attnum; /* which attr (only for NOTNULL, DEFAULT) */
Node *expr; /* transformed default or check expr */
+ bool is_not_enforced; /* is not enforced? (only for CHECK) */
bool skip_validation; /* skip validation? (only for CHECK) */
bool is_local; /* constraint has local (non-inherited) def */
int16 inhcount; /* number of times constraint is inherited */
diff --git a/src/include/catalog/pg_constraint.h b/src/include/catalog/pg_constraint.h
index 4b4476738a2..8e13f5ed710 100644
--- a/src/include/catalog/pg_constraint.h
+++ b/src/include/catalog/pg_constraint.h
@@ -51,6 +51,7 @@ CATALOG(pg_constraint,2606,ConstraintRelationId)
char contype; /* constraint type; see codes below */
bool condeferrable; /* deferrable constraint? */
bool condeferred; /* deferred by default? */
+ bool conenforced; /* enforced constraint? */
bool convalidated; /* constraint has been validated? */
/*
@@ -222,6 +223,7 @@ extern Oid CreateConstraintEntry(const char *constraintName,
char constraintType,
bool isDeferrable,
bool isDeferred,
+ bool isEnforced,
bool isValidated,
Oid parentConstrId,
Oid relId,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 0f9462493e3..e451aa9d912 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2736,6 +2736,8 @@ typedef enum ConstrType /* types of constraints */
CONSTR_ATTR_NOT_DEFERRABLE,
CONSTR_ATTR_DEFERRED,
CONSTR_ATTR_IMMEDIATE,
+ CONSTR_ATTR_ENFORCED,
+ CONSTR_ATTR_NOT_ENFORCED,
} ConstrType;
/* Foreign key action codes */
@@ -2757,6 +2759,7 @@ typedef struct Constraint
char *conname; /* Constraint name, or NULL if unnamed */
bool deferrable; /* DEFERRABLE? */
bool initdeferred; /* INITIALLY DEFERRED? */
+ bool is_not_enforced; /* is not enforced constraint? */
bool skip_validation; /* skip validation of existing rows? */
bool initially_valid; /* mark the new constraint as valid? */
bool is_no_inherit; /* is constraint non-inheritable? */
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 899d64ad55f..f2bc378394e 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -153,6 +153,7 @@ PG_KEYWORD("enable", ENABLE_P, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("encoding", ENCODING, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("encrypted", ENCRYPTED, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("end", END_P, RESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("enforced", ENFORCED, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("enum", ENUM_P, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("error", ERROR_P, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("escape", ESCAPE, UNRESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out
index 2212c8dbb59..b68ac47a3cd 100644
--- a/src/test/regress/expected/alter_table.out
+++ b/src/test/regress/expected/alter_table.out
@@ -507,11 +507,14 @@ ALTER TABLE attmp3 validate constraint attmpconstr;
ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten CHECK (b > 10); -- fail
ERROR: check constraint "b_greater_than_ten" of relation "attmp3" is violated by some row
ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten CHECK (b > 10) NOT VALID; -- succeeds
+ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten_not_enforced CHECK (b > 10) NOT ENFORCED; -- succeeds
ALTER TABLE attmp3 VALIDATE CONSTRAINT b_greater_than_ten; -- fails
ERROR: check constraint "b_greater_than_ten" of relation "attmp3" is violated by some row
DELETE FROM attmp3 WHERE NOT b > 10;
ALTER TABLE attmp3 VALIDATE CONSTRAINT b_greater_than_ten; -- succeeds
ALTER TABLE attmp3 VALIDATE CONSTRAINT b_greater_than_ten; -- succeeds
+ALTER TABLE attmp3 VALIDATE CONSTRAINT b_greater_than_ten_not_enforced; -- fail
+ERROR: cannot validated NOT ENFORCED constraint
-- Test inherited NOT VALID CHECK constraints
select * from attmp3;
a | b
diff --git a/src/test/regress/expected/constraints.out b/src/test/regress/expected/constraints.out
index 71200c90ed3..d2c8e6ad172 100644
--- a/src/test/regress/expected/constraints.out
+++ b/src/test/regress/expected/constraints.out
@@ -87,6 +87,25 @@ SELECT * FROM CHECK_TBL;
6
(3 rows)
+CREATE TABLE NE_CHECK_TBL (x int,
+ CONSTRAINT CHECK_CON CHECK (x > 3) NOT ENFORCED);
+INSERT INTO NE_CHECK_TBL VALUES (5);
+INSERT INTO NE_CHECK_TBL VALUES (4);
+INSERT INTO NE_CHECK_TBL VALUES (3);
+INSERT INTO NE_CHECK_TBL VALUES (2);
+INSERT INTO NE_CHECK_TBL VALUES (6);
+INSERT INTO NE_CHECK_TBL VALUES (1);
+SELECT * FROM NE_CHECK_TBL;
+ x
+---
+ 5
+ 4
+ 3
+ 2
+ 6
+ 1
+(6 rows)
+
CREATE SEQUENCE CHECK_SEQ;
CREATE TABLE CHECK2_TBL (x int, y text, z int,
CONSTRAINT SEQUENCE_CON
@@ -120,7 +139,8 @@ CREATE TABLE INSERT_TBL (x INT DEFAULT nextval('insert_seq'),
y TEXT DEFAULT '-NULL-',
z INT DEFAULT -1 * currval('insert_seq'),
CONSTRAINT INSERT_TBL_CON CHECK (x >= 3 AND y <> 'check failed' AND x < 8),
- CHECK (x + z = 0));
+ CHECK (x + z = 0) ENFORCED, /* no change it is a default */
+ CONSTRAINT NE_INSERT_TBL_CON CHECK (x + z = 1) NOT ENFORCED);
INSERT INTO INSERT_TBL(x,z) VALUES (2, -2);
ERROR: new row for relation "insert_tbl" violates check constraint "insert_tbl_con"
DETAIL: Failing row contains (2, -NULL-, -2).
@@ -715,6 +735,23 @@ SELECT * FROM unique_tbl;
3 | threex
(5 rows)
+-- enforcibility cannot be specified or set for unique constrain
+CREATE TABLE UNIQUE_EN_TBL(i int UNIQUE ENFORCED);
+ERROR: misplaced ENFORCED clause
+LINE 1: CREATE TABLE UNIQUE_EN_TBL(i int UNIQUE ENFORCED);
+ ^
+CREATE TABLE UNIQUE_NOTEN_TBL(i int UNIQUE NOT ENFORCED);
+ERROR: misplaced NOT ENFORCED clause
+LINE 1: CREATE TABLE UNIQUE_NOTEN_TBL(i int UNIQUE NOT ENFORCED);
+ ^
+ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
+ERROR: ALTER CONSTRAINT statement constraints cannot be marked ENFORCED
+LINE 1: ...TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
+ ^
+ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORCED;
+ERROR: ALTER CONSTRAINT statement constraints cannot be marked NOT ENFORCED
+LINE 1: ...ABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORC...
+ ^
DROP TABLE unique_tbl;
--
-- EXCLUDE constraints
@@ -1351,6 +1388,19 @@ ERROR: must be owner of relation constraint_comments_tbl
COMMENT ON CONSTRAINT the_constraint ON DOMAIN constraint_comments_dom IS 'no, another comment';
ERROR: must be owner of type constraint_comments_dom
RESET SESSION AUTHORIZATION;
+-- enforceability not allowed
+CREATE DOMAIN constraint_enforced_dom AS int CONSTRAINT the_constraint CHECK (value > 0) ENFORCED;
+ERROR: specifying constraint enforceability not supported for domains
+CREATE DOMAIN constraint_not_enforced_dom AS int CONSTRAINT the_constraint CHECK (value > 0) NOT ENFORCED;
+ERROR: specifying constraint enforceability not supported for domains
+ALTER DOMAIN constraint_comments_dom ADD CONSTRAINT the_constraint CHECK (value > 0) ENFORCED;
+ERROR: CHECK constraints cannot be marked ENFORCED
+LINE 1: ...om ADD CONSTRAINT the_constraint CHECK (value > 0) ENFORCED;
+ ^
+ALTER DOMAIN constraint_comments_dom ADD CONSTRAINT the_constraint CHECK (value > 0) NOT ENFORCED;
+ERROR: CHECK constraints cannot be marked NOT ENFORCED
+LINE 1: ...m ADD CONSTRAINT the_constraint CHECK (value > 0) NOT ENFORC...
+ ^
DROP TABLE constraint_comments_tbl;
DROP DOMAIN constraint_comments_dom;
DROP ROLE regress_constraint_comments;
diff --git a/src/test/regress/expected/create_table_like.out b/src/test/regress/expected/create_table_like.out
index d091da5a1ef..c444c255c76 100644
--- a/src/test/regress/expected/create_table_like.out
+++ b/src/test/regress/expected/create_table_like.out
@@ -315,7 +315,8 @@ Referenced by:
DROP TABLE inhz;
-- including storage and comments
-CREATE TABLE ctlt1 (a text CHECK (length(a) > 2) PRIMARY KEY, b text);
+CREATE TABLE ctlt1 (a text CHECK (length(a) > 2) ENFORCED PRIMARY KEY,
+ b text CHECK (length(b) > 100) NOT ENFORCED);
CREATE INDEX ctlt1_b_key ON ctlt1 (b);
CREATE INDEX ctlt1_fnidx ON ctlt1 ((a || b));
CREATE STATISTICS ctlt1_a_b_stat ON a,b FROM ctlt1;
@@ -366,6 +367,7 @@ CREATE TABLE ctlt1_inh (LIKE ctlt1 INCLUDING CONSTRAINTS INCLUDING COMMENTS) INH
NOTICE: merging column "a" with inherited definition
NOTICE: merging column "b" with inherited definition
NOTICE: merging constraint "ctlt1_a_check" with inherited definition
+NOTICE: merging constraint "ctlt1_b_check" with inherited definition
\d+ ctlt1_inh
Table "public.ctlt1_inh"
Column | Type | Collation | Nullable | Default | Storage | Stats target | Description
@@ -374,6 +376,7 @@ NOTICE: merging constraint "ctlt1_a_check" with inherited definition
b | text | | | | extended | | B
Check constraints:
"ctlt1_a_check" CHECK (length(a) > 2)
+ "ctlt1_b_check" CHECK (length(b) > 100) NOT ENFORCED NOT VALID
Not-null constraints:
"ctlt1_a_not_null" NOT NULL "a" (local, inherited)
Inherits: ctlt1
@@ -395,6 +398,7 @@ NOTICE: merging multiple inherited definitions of column "a"
c | text | | | | external | |
Check constraints:
"ctlt1_a_check" CHECK (length(a) > 2)
+ "ctlt1_b_check" CHECK (length(b) > 100) NOT ENFORCED NOT VALID
"ctlt3_a_check" CHECK (length(a) < 5)
"ctlt3_c_check" CHECK (length(c) < 7)
Not-null constraints:
@@ -415,6 +419,7 @@ Indexes:
"ctlt13_like_expr_idx" btree ((a || c))
Check constraints:
"ctlt1_a_check" CHECK (length(a) > 2)
+ "ctlt1_b_check" CHECK (length(b) > 100) NOT ENFORCED NOT VALID
"ctlt3_a_check" CHECK (length(a) < 5)
"ctlt3_c_check" CHECK (length(c) < 7)
Not-null constraints:
@@ -440,6 +445,7 @@ Indexes:
"ctlt_all_expr_idx" btree ((a || b))
Check constraints:
"ctlt1_a_check" CHECK (length(a) > 2)
+ "ctlt1_b_check" CHECK (length(b) > 100) NOT ENFORCED NOT VALID
Statistics objects:
"public.ctlt_all_a_b_stat" ON a, b FROM ctlt_all
"public.ctlt_all_expr_stat" ON (a || b) FROM ctlt_all
@@ -482,6 +488,7 @@ Indexes:
"pg_attrdef_expr_idx" btree ((a || b))
Check constraints:
"ctlt1_a_check" CHECK (length(a) > 2)
+ "ctlt1_b_check" CHECK (length(b) > 100) NOT ENFORCED NOT VALID
Statistics objects:
"public.pg_attrdef_a_b_stat" ON a, b FROM public.pg_attrdef
"public.pg_attrdef_expr_stat" ON (a || b) FROM public.pg_attrdef
@@ -506,6 +513,7 @@ Indexes:
"ctlt1_expr_idx" btree ((a || b))
Check constraints:
"ctlt1_a_check" CHECK (length(a) > 2)
+ "ctlt1_b_check" CHECK (length(b) > 100) NOT ENFORCED NOT VALID
Statistics objects:
"ctl_schema.ctlt1_a_b_stat" ON a, b FROM ctlt1
"ctl_schema.ctlt1_expr_stat" ON (a || b) FROM ctlt1
diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql
index 637e3dac389..d8bdb91b367 100644
--- a/src/test/regress/sql/alter_table.sql
+++ b/src/test/regress/sql/alter_table.sql
@@ -387,10 +387,12 @@ ALTER TABLE attmp3 validate constraint attmpconstr;
-- Try a non-verified CHECK constraint
ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten CHECK (b > 10); -- fail
ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten CHECK (b > 10) NOT VALID; -- succeeds
+ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten_not_enforced CHECK (b > 10) NOT ENFORCED; -- succeeds
ALTER TABLE attmp3 VALIDATE CONSTRAINT b_greater_than_ten; -- fails
DELETE FROM attmp3 WHERE NOT b > 10;
ALTER TABLE attmp3 VALIDATE CONSTRAINT b_greater_than_ten; -- succeeds
ALTER TABLE attmp3 VALIDATE CONSTRAINT b_greater_than_ten; -- succeeds
+ALTER TABLE attmp3 VALIDATE CONSTRAINT b_greater_than_ten_not_enforced; -- fail
-- Test inherited NOT VALID CHECK constraints
select * from attmp3;
diff --git a/src/test/regress/sql/constraints.sql b/src/test/regress/sql/constraints.sql
index e607eb1fddb..1f36116d25a 100644
--- a/src/test/regress/sql/constraints.sql
+++ b/src/test/regress/sql/constraints.sql
@@ -67,6 +67,18 @@ INSERT INTO CHECK_TBL VALUES (1);
SELECT * FROM CHECK_TBL;
+CREATE TABLE NE_CHECK_TBL (x int,
+ CONSTRAINT CHECK_CON CHECK (x > 3) NOT ENFORCED);
+
+INSERT INTO NE_CHECK_TBL VALUES (5);
+INSERT INTO NE_CHECK_TBL VALUES (4);
+INSERT INTO NE_CHECK_TBL VALUES (3);
+INSERT INTO NE_CHECK_TBL VALUES (2);
+INSERT INTO NE_CHECK_TBL VALUES (6);
+INSERT INTO NE_CHECK_TBL VALUES (1);
+
+SELECT * FROM NE_CHECK_TBL;
+
CREATE SEQUENCE CHECK_SEQ;
CREATE TABLE CHECK2_TBL (x int, y text, z int,
@@ -92,7 +104,8 @@ CREATE TABLE INSERT_TBL (x INT DEFAULT nextval('insert_seq'),
y TEXT DEFAULT '-NULL-',
z INT DEFAULT -1 * currval('insert_seq'),
CONSTRAINT INSERT_TBL_CON CHECK (x >= 3 AND y <> 'check failed' AND x < 8),
- CHECK (x + z = 0));
+ CHECK (x + z = 0) ENFORCED, /* no change it is a default */
+ CONSTRAINT NE_INSERT_TBL_CON CHECK (x + z = 1) NOT ENFORCED);
INSERT INTO INSERT_TBL(x,z) VALUES (2, -2);
@@ -518,6 +531,12 @@ COMMIT;
SELECT * FROM unique_tbl;
+-- enforcibility cannot be specified or set for unique constrain
+CREATE TABLE UNIQUE_EN_TBL(i int UNIQUE ENFORCED);
+CREATE TABLE UNIQUE_NOTEN_TBL(i int UNIQUE NOT ENFORCED);
+ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
+ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORCED;
+
DROP TABLE unique_tbl;
--
@@ -812,6 +831,12 @@ COMMENT ON CONSTRAINT the_constraint ON constraint_comments_tbl IS 'no, the comm
COMMENT ON CONSTRAINT the_constraint ON DOMAIN constraint_comments_dom IS 'no, another comment';
RESET SESSION AUTHORIZATION;
+-- enforceability not allowed
+CREATE DOMAIN constraint_enforced_dom AS int CONSTRAINT the_constraint CHECK (value > 0) ENFORCED;
+CREATE DOMAIN constraint_not_enforced_dom AS int CONSTRAINT the_constraint CHECK (value > 0) NOT ENFORCED;
+ALTER DOMAIN constraint_comments_dom ADD CONSTRAINT the_constraint CHECK (value > 0) ENFORCED;
+ALTER DOMAIN constraint_comments_dom ADD CONSTRAINT the_constraint CHECK (value > 0) NOT ENFORCED;
+
DROP TABLE constraint_comments_tbl;
DROP DOMAIN constraint_comments_dom;
diff --git a/src/test/regress/sql/create_table_like.sql b/src/test/regress/sql/create_table_like.sql
index dea8942c71f..a41f8b83d77 100644
--- a/src/test/regress/sql/create_table_like.sql
+++ b/src/test/regress/sql/create_table_like.sql
@@ -128,7 +128,8 @@ CREATE TABLE inhz (x text REFERENCES inhz, LIKE inhx INCLUDING INDEXES);
DROP TABLE inhz;
-- including storage and comments
-CREATE TABLE ctlt1 (a text CHECK (length(a) > 2) PRIMARY KEY, b text);
+CREATE TABLE ctlt1 (a text CHECK (length(a) > 2) ENFORCED PRIMARY KEY,
+ b text CHECK (length(b) > 100) NOT ENFORCED);
CREATE INDEX ctlt1_b_key ON ctlt1 (b);
CREATE INDEX ctlt1_fnidx ON ctlt1 ((a || b));
CREATE STATISTICS ctlt1_a_b_stat ON a,b FROM ctlt1;
--
2.43.5
v5-0002-refactor-split-ATExecAlterConstrRecurse.patchapplication/x-patch; name=v5-0002-refactor-split-ATExecAlterConstrRecurse.patchDownload
From b7014d1513021b186add66ddc90b97a008ee225f Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 23 Sep 2024 12:31:07 +0530
Subject: [PATCH v5 2/5] refactor: split ATExecAlterConstrRecurse()
When changing a NOT ENFORCED foreign key constraint to ENFORCED, we
need to create the necessary triggers for enforcing the constraint. We
can set the deferrability at the time of creation and skip the code
used for other constraint alteration operations.
Additionally, move the code that iterates over each child constraint
of the constraint being altered into a separate function.
---
src/backend/commands/tablecmds.c | 194 +++++++++++++++++++------------
1 file changed, 118 insertions(+), 76 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index f33d4f8db56..8575d208e6b 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -394,6 +394,12 @@ static ObjectAddress ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd,
static bool ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
Relation rel, HeapTuple contuple, List **otherrelids,
LOCKMODE lockmode);
+static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
+ bool deferrable, bool initdeferred,
+ List **otherrelids);
+static void ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
+ Relation rel, HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode);
static ObjectAddress ATExecValidateConstraint(List **wqueue,
Relation rel, char *constrName,
bool recurse, bool recursing, LOCKMODE lockmode);
@@ -11840,9 +11846,6 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
{
HeapTuple copyTuple;
Form_pg_constraint copy_con;
- HeapTuple tgtuple;
- ScanKeyData tgkey;
- SysScanDesc tgscan;
copyTuple = heap_copytuple(contuple);
copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
@@ -11863,53 +11866,8 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
* Now we need to update the multiple entries in pg_trigger that
* implement the constraint.
*/
- ScanKeyInit(&tgkey,
- Anum_pg_trigger_tgconstraint,
- BTEqualStrategyNumber, F_OIDEQ,
- ObjectIdGetDatum(conoid));
- tgscan = systable_beginscan(tgrel, TriggerConstraintIndexId, true,
- NULL, 1, &tgkey);
- while (HeapTupleIsValid(tgtuple = systable_getnext(tgscan)))
- {
- Form_pg_trigger tgform = (Form_pg_trigger) GETSTRUCT(tgtuple);
- Form_pg_trigger copy_tg;
- HeapTuple tgCopyTuple;
-
- /*
- * Remember OIDs of other relation(s) involved in FK constraint.
- * (Note: it's likely that we could skip forcing a relcache inval
- * for other rels that don't have a trigger whose properties
- * change, but let's be conservative.)
- */
- if (tgform->tgrelid != RelationGetRelid(rel))
- *otherrelids = list_append_unique_oid(*otherrelids,
- tgform->tgrelid);
-
- /*
- * Update deferrability of RI_FKey_noaction_del,
- * RI_FKey_noaction_upd, RI_FKey_check_ins and RI_FKey_check_upd
- * triggers, but not others; see createForeignKeyActionTriggers
- * and CreateFKCheckTrigger.
- */
- if (tgform->tgfoid != F_RI_FKEY_NOACTION_DEL &&
- tgform->tgfoid != F_RI_FKEY_NOACTION_UPD &&
- tgform->tgfoid != F_RI_FKEY_CHECK_INS &&
- tgform->tgfoid != F_RI_FKEY_CHECK_UPD)
- continue;
-
- tgCopyTuple = heap_copytuple(tgtuple);
- copy_tg = (Form_pg_trigger) GETSTRUCT(tgCopyTuple);
-
- copy_tg->tgdeferrable = cmdcon->deferrable;
- copy_tg->tginitdeferred = cmdcon->initdeferred;
- CatalogTupleUpdate(tgrel, &tgCopyTuple->t_self, tgCopyTuple);
-
- InvokeObjectPostAlterHook(TriggerRelationId, tgform->oid, 0);
-
- heap_freetuple(tgCopyTuple);
- }
-
- systable_endscan(tgscan);
+ AlterConstrTriggerDeferrability(conoid, tgrel, rel, cmdcon->deferrable,
+ cmdcon->initdeferred, otherrelids);
}
/*
@@ -11922,36 +11880,120 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
*/
if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)
- {
- ScanKeyData pkey;
- SysScanDesc pscan;
- HeapTuple childtup;
-
- ScanKeyInit(&pkey,
- Anum_pg_constraint_conparentid,
- BTEqualStrategyNumber, F_OIDEQ,
- ObjectIdGetDatum(conoid));
-
- pscan = systable_beginscan(conrel, ConstraintParentIndexId,
- true, NULL, 1, &pkey);
-
- while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
- {
- Form_pg_constraint childcon = (Form_pg_constraint) GETSTRUCT(childtup);
- Relation childrel;
-
- childrel = table_open(childcon->conrelid, lockmode);
- ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, childrel, childtup,
- otherrelids, lockmode);
- table_close(childrel, NoLock);
- }
-
- systable_endscan(pscan);
- }
+ ATExecAlterChildConstr(cmdcon, conrel, tgrel, rel, contuple,
+ otherrelids, lockmode);
return changed;
}
+/*
+ * A subroutine of ATExecAlterConstrRecurse that updated constraint trigger's
+ * deferrability.
+ *
+ * The arguments to this function have the same meaning as the arguments to
+ * ATExecAlterConstrRecurse.
+ */
+static void
+AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
+ bool deferrable, bool initdeferred,
+ List **otherrelids)
+{
+ HeapTuple tgtuple;
+ ScanKeyData tgkey;
+ SysScanDesc tgscan;
+
+ ScanKeyInit(&tgkey,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+ tgscan = systable_beginscan(tgrel, TriggerConstraintIndexId, true,
+ NULL, 1, &tgkey);
+ while (HeapTupleIsValid(tgtuple = systable_getnext(tgscan)))
+ {
+ Form_pg_trigger tgform = (Form_pg_trigger) GETSTRUCT(tgtuple);
+ Form_pg_trigger copy_tg;
+ HeapTuple tgCopyTuple;
+
+ /*
+ * Remember OIDs of other relation(s) involved in FK constraint.
+ * (Note: it's likely that we could skip forcing a relcache inval for
+ * other rels that don't have a trigger whose properties change, but
+ * let's be conservative.)
+ */
+ if (tgform->tgrelid != RelationGetRelid(rel))
+ *otherrelids = list_append_unique_oid(*otherrelids,
+ tgform->tgrelid);
+
+ /*
+ * Update enable status and deferrability of RI_FKey_noaction_del,
+ * RI_FKey_noaction_upd, RI_FKey_check_ins and RI_FKey_check_upd
+ * triggers, but not others; see createForeignKeyActionTriggers and
+ * CreateFKCheckTrigger.
+ */
+ if (tgform->tgfoid != F_RI_FKEY_NOACTION_DEL &&
+ tgform->tgfoid != F_RI_FKEY_NOACTION_UPD &&
+ tgform->tgfoid != F_RI_FKEY_CHECK_INS &&
+ tgform->tgfoid != F_RI_FKEY_CHECK_UPD)
+ continue;
+
+ tgCopyTuple = heap_copytuple(tgtuple);
+ copy_tg = (Form_pg_trigger) GETSTRUCT(tgCopyTuple);
+
+ copy_tg->tgdeferrable = deferrable;
+ copy_tg->tginitdeferred = initdeferred;
+ CatalogTupleUpdate(tgrel, &tgCopyTuple->t_self, tgCopyTuple);
+
+ InvokeObjectPostAlterHook(TriggerRelationId, tgform->oid, 0);
+
+ heap_freetuple(tgCopyTuple);
+ }
+
+ systable_endscan(tgscan);
+}
+
+/*
+ * Invokes ATExecAlterConstrRecurse for each constraint that is a child of the
+ * specified constraint.
+ *
+ * The arguments to this function have the same meaning as the arguments to
+ * ATExecAlterConstrRecurse.
+ */
+static void
+ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
+ Relation rel, HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode)
+{
+ Form_pg_constraint currcon;
+ Oid conoid;
+ ScanKeyData pkey;
+ SysScanDesc pscan;
+ HeapTuple childtup;
+
+ currcon = (Form_pg_constraint) GETSTRUCT(contuple);
+ conoid = currcon->oid;
+
+ ScanKeyInit(&pkey,
+ Anum_pg_constraint_conparentid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+
+ pscan = systable_beginscan(conrel, ConstraintParentIndexId,
+ true, NULL, 1, &pkey);
+
+ while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
+ {
+ Form_pg_constraint childcon = (Form_pg_constraint) GETSTRUCT(childtup);
+ Relation childrel;
+
+ childrel = table_open(childcon->conrelid, lockmode);
+ ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, childrel, childtup,
+ otherrelids, lockmode);
+ table_close(childrel, NoLock);
+ }
+
+ systable_endscan(pscan);
+}
+
/*
* ALTER TABLE VALIDATE CONSTRAINT
*
--
2.43.5
v5-0003-refactor-Change-ATExecAlterConstrRecurse-argument.patchapplication/x-patch; name=v5-0003-refactor-Change-ATExecAlterConstrRecurse-argument.patchDownload
From e9e02a1e0362e1599996295e3604b02ed0d6b74c Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 23 Sep 2024 12:37:05 +0530
Subject: [PATCH v5 3/5] refactor: Change ATExecAlterConstrRecurse() argument.
Instead of passing the Relation of the referencing relation, we will
pass a relid. Opening the relation using the relid again should not
cause significant issues. However, this approach makes recursion more
efficient, especially in the later patch where we will be recreating
the referencing and referred triggers. In addition to that passing
relid of the referred relation too and Oids of triggers.
----
NOTE: This patch is not meant to be committed separately. It should
be squashed with the main patch that adds ENFORCED/NOT ENFORCED.
----
---
src/backend/commands/tablecmds.c | 66 ++++++++++++++++++++++----------
1 file changed, 45 insertions(+), 21 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 8575d208e6b..45861bc2417 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -392,14 +392,22 @@ static void AlterSeqNamespaces(Relation classRel, Relation rel,
static ObjectAddress ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd,
bool recurse, bool recursing, LOCKMODE lockmode);
static bool ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode);
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger);
static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
bool deferrable, bool initdeferred,
List **otherrelids);
static void ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode);
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger);
static ObjectAddress ATExecValidateConstraint(List **wqueue,
Relation rel, char *constrName,
bool recurse, bool recursing, LOCKMODE lockmode);
@@ -11786,8 +11794,10 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
currcon->condeferred != cmdcon->initdeferred ||
rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
- if (ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, rel, contuple,
- &otherrelids, lockmode))
+ if (ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, currcon->conrelid,
+ currcon->confrelid, contuple, &otherrelids,
+ lockmode, InvalidOid, InvalidOid,
+ InvalidOid, InvalidOid))
ObjectAddressSet(address, ConstraintRelationId, currcon->oid);
}
@@ -11820,13 +11830,18 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
*/
static bool
ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode)
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger)
{
Form_pg_constraint currcon;
Oid conoid;
Oid refrelid;
bool changed = false;
+ Relation rel;
/* since this function recurses, it could be driven to stack overflow */
check_stack_depth();
@@ -11835,6 +11850,8 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
conoid = currcon->oid;
refrelid = currcon->confrelid;
+ rel = table_open(currcon->conrelid, lockmode);
+
/*
* Update pg_constraint with the flags from cmdcon.
*
@@ -11880,8 +11897,14 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
*/
if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)
- ATExecAlterChildConstr(cmdcon, conrel, tgrel, rel, contuple,
- otherrelids, lockmode);
+ ATExecAlterChildConstr(cmdcon, conrel, tgrel, fkrelid, pkrelid,
+ contuple, otherrelids, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
+
+ table_close(rel, NoLock);
return changed;
}
@@ -11960,8 +11983,12 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
*/
static void
ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode)
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger)
{
Form_pg_constraint currcon;
Oid conoid;
@@ -11981,15 +12008,12 @@ ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
true, NULL, 1, &pkey);
while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
- {
- Form_pg_constraint childcon = (Form_pg_constraint) GETSTRUCT(childtup);
- Relation childrel;
-
- childrel = table_open(childcon->conrelid, lockmode);
- ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, childrel, childtup,
- otherrelids, lockmode);
- table_close(childrel, NoLock);
- }
+ ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, fkrelid, pkrelid,
+ childtup, otherrelids, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
systable_endscan(pscan);
}
--
2.43.5
v5-0004-Remove-hastriggers-flag-check-before-fetching-FK-.patchapplication/x-patch; name=v5-0004-Remove-hastriggers-flag-check-before-fetching-FK-.patchDownload
From 79d62a43195bbf9722f14a87370fab21d3f3d131 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 7 Oct 2024 12:40:07 +0530
Subject: [PATCH v5 4/5] Remove hastriggers flag check before fetching FK
constraints.
With NOT ENFORCED, foreign key constraints will be created without
triggers. Therefore, the criteria for fetching foreign keys based on
the presence of triggers no longer apply.
----
NOTE: This patch is intended to reduce the diff noise from the main
patch and is not meant to be committed separately. It should be
squashed with the main patch that adds ENFORCED/NOT ENFORCED.
----
---
src/backend/utils/cache/relcache.c | 5 -
src/bin/pg_dump/pg_dump.c | 8 +-
src/bin/psql/describe.c | 226 ++++++++++++++---------------
3 files changed, 108 insertions(+), 131 deletions(-)
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index f4721b07f90..d5bd72d7ad4 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4669,11 +4669,6 @@ RelationGetFKeyList(Relation relation)
if (relation->rd_fkeyvalid)
return relation->rd_fkeylist;
- /* Fast path: non-partitioned tables without triggers can't have FKs */
- if (!relation->rd_rel->relhastriggers &&
- relation->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
- return NIL;
-
/*
* We build the list we intend to return (in the caller's context) while
* doing the scan. After successfully completing the scan, we copy that
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index ec0cdf4ed74..9ada4cdc182 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -7839,13 +7839,7 @@ getConstraints(Archive *fout, TableInfo tblinfo[], int numTables)
{
TableInfo *tinfo = &tblinfo[i];
- /*
- * For partitioned tables, foreign keys have no triggers so they must
- * be included anyway in case some foreign keys are defined.
- */
- if ((!tinfo->hastriggers &&
- tinfo->relkind != RELKIND_PARTITIONED_TABLE) ||
- !(tinfo->dobj.dump & DUMP_COMPONENT_DEFINITION))
+ if (!(tinfo->dobj.dump & DUMP_COMPONENT_DEFINITION))
continue;
/* OK, we need info for this table */
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 2657abdc72d..054e8a2d821 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2528,136 +2528,124 @@ describeOneTableDetails(const char *schemaname,
PQclear(result);
}
- /*
- * Print foreign-key constraints (there are none if no triggers,
- * except if the table is partitioned, in which case the triggers
- * appear in the partitions)
- */
- if (tableinfo.hastriggers ||
- tableinfo.relkind == RELKIND_PARTITIONED_TABLE)
+ /* Print foreign-key constraints */
+ if (pset.sversion >= 120000 &&
+ (tableinfo.ispartition || tableinfo.relkind == RELKIND_PARTITIONED_TABLE))
{
- if (pset.sversion >= 120000 &&
- (tableinfo.ispartition || tableinfo.relkind == RELKIND_PARTITIONED_TABLE))
+ /*
+ * Put the constraints defined in this table first, followed
+ * by the constraints defined in ancestor partitioned tables.
+ */
+ printfPQExpBuffer(&buf,
+ "SELECT conrelid = '%s'::pg_catalog.regclass AS sametable,\n"
+ " conname,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef,\n"
+ " conrelid::pg_catalog.regclass AS ontable\n"
+ " FROM pg_catalog.pg_constraint,\n"
+ " pg_catalog.pg_partition_ancestors('%s')\n"
+ " WHERE conrelid = relid AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
+ "ORDER BY sametable DESC, conname;",
+ oid, oid);
+ }
+ else
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT true as sametable, conname,\n"
+ " pg_catalog.pg_get_constraintdef(r.oid, true) as condef,\n"
+ " conrelid::pg_catalog.regclass AS ontable\n"
+ "FROM pg_catalog.pg_constraint r\n"
+ "WHERE r.conrelid = '%s' AND r.contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n",
+ oid);
+
+ if (pset.sversion >= 120000)
+ appendPQExpBufferStr(&buf, " AND conparentid = 0\n");
+ appendPQExpBufferStr(&buf, "ORDER BY conname");
+ }
+
+ result = PSQLexec(buf.data);
+ if (!result)
+ goto error_return;
+ else
+ tuples = PQntuples(result);
+
+ if (tuples > 0)
+ {
+ int i_sametable = PQfnumber(result, "sametable"),
+ i_conname = PQfnumber(result, "conname"),
+ i_condef = PQfnumber(result, "condef"),
+ i_ontable = PQfnumber(result, "ontable");
+
+ printTableAddFooter(&cont, _("Foreign-key constraints:"));
+ for (i = 0; i < tuples; i++)
{
/*
- * Put the constraints defined in this table first, followed
- * by the constraints defined in ancestor partitioned tables.
+ * Print untranslated constraint name and definition. Use
+ * a "TABLE tab" prefix when the constraint is defined in
+ * a parent partitioned table.
*/
- printfPQExpBuffer(&buf,
- "SELECT conrelid = '%s'::pg_catalog.regclass AS sametable,\n"
- " conname,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef,\n"
- " conrelid::pg_catalog.regclass AS ontable\n"
- " FROM pg_catalog.pg_constraint,\n"
- " pg_catalog.pg_partition_ancestors('%s')\n"
- " WHERE conrelid = relid AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
- "ORDER BY sametable DESC, conname;",
- oid, oid);
- }
- else
- {
- printfPQExpBuffer(&buf,
- "SELECT true as sametable, conname,\n"
- " pg_catalog.pg_get_constraintdef(r.oid, true) as condef,\n"
- " conrelid::pg_catalog.regclass AS ontable\n"
- "FROM pg_catalog.pg_constraint r\n"
- "WHERE r.conrelid = '%s' AND r.contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n",
- oid);
-
- if (pset.sversion >= 120000)
- appendPQExpBufferStr(&buf, " AND conparentid = 0\n");
- appendPQExpBufferStr(&buf, "ORDER BY conname");
- }
-
- result = PSQLexec(buf.data);
- if (!result)
- goto error_return;
- else
- tuples = PQntuples(result);
-
- if (tuples > 0)
- {
- int i_sametable = PQfnumber(result, "sametable"),
- i_conname = PQfnumber(result, "conname"),
- i_condef = PQfnumber(result, "condef"),
- i_ontable = PQfnumber(result, "ontable");
-
- printTableAddFooter(&cont, _("Foreign-key constraints:"));
- for (i = 0; i < tuples; i++)
- {
- /*
- * Print untranslated constraint name and definition. Use
- * a "TABLE tab" prefix when the constraint is defined in
- * a parent partitioned table.
- */
- if (strcmp(PQgetvalue(result, i, i_sametable), "f") == 0)
- printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
- PQgetvalue(result, i, i_ontable),
- PQgetvalue(result, i, i_conname),
- PQgetvalue(result, i, i_condef));
- else
- printfPQExpBuffer(&buf, " \"%s\" %s",
- PQgetvalue(result, i, i_conname),
- PQgetvalue(result, i, i_condef));
-
- printTableAddFooter(&cont, buf.data);
- }
- }
- PQclear(result);
- }
-
- /* print incoming foreign-key references */
- if (tableinfo.hastriggers ||
- tableinfo.relkind == RELKIND_PARTITIONED_TABLE)
- {
- if (pset.sversion >= 120000)
- {
- printfPQExpBuffer(&buf,
- "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
- " FROM pg_catalog.pg_constraint c\n"
- " WHERE confrelid IN (SELECT pg_catalog.pg_partition_ancestors('%s')\n"
- " UNION ALL VALUES ('%s'::pg_catalog.regclass))\n"
- " AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
- "ORDER BY conname;",
- oid, oid);
- }
- else
- {
- printfPQExpBuffer(&buf,
- "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
- " FROM pg_catalog.pg_constraint\n"
- " WHERE confrelid = %s AND contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n"
- "ORDER BY conname;",
- oid);
- }
-
- result = PSQLexec(buf.data);
- if (!result)
- goto error_return;
- else
- tuples = PQntuples(result);
-
- if (tuples > 0)
- {
- int i_conname = PQfnumber(result, "conname"),
- i_ontable = PQfnumber(result, "ontable"),
- i_condef = PQfnumber(result, "condef");
-
- printTableAddFooter(&cont, _("Referenced by:"));
- for (i = 0; i < tuples; i++)
- {
+ if (strcmp(PQgetvalue(result, i, i_sametable), "f") == 0)
printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
PQgetvalue(result, i, i_ontable),
PQgetvalue(result, i, i_conname),
PQgetvalue(result, i, i_condef));
+ else
+ printfPQExpBuffer(&buf, " \"%s\" %s",
+ PQgetvalue(result, i, i_conname),
+ PQgetvalue(result, i, i_condef));
- printTableAddFooter(&cont, buf.data);
- }
+ printTableAddFooter(&cont, buf.data);
}
- PQclear(result);
}
+ PQclear(result);
+
+ /* print incoming foreign-key references */
+ if (pset.sversion >= 120000)
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
+ " FROM pg_catalog.pg_constraint c\n"
+ " WHERE confrelid IN (SELECT pg_catalog.pg_partition_ancestors('%s')\n"
+ " UNION ALL VALUES ('%s'::pg_catalog.regclass))\n"
+ " AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
+ "ORDER BY conname;",
+ oid, oid);
+ }
+ else
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
+ " FROM pg_catalog.pg_constraint\n"
+ " WHERE confrelid = %s AND contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n"
+ "ORDER BY conname;",
+ oid);
+ }
+
+ result = PSQLexec(buf.data);
+ if (!result)
+ goto error_return;
+ else
+ tuples = PQntuples(result);
+
+ if (tuples > 0)
+ {
+ int i_conname = PQfnumber(result, "conname"),
+ i_ontable = PQfnumber(result, "ontable"),
+ i_condef = PQfnumber(result, "condef");
+
+ printTableAddFooter(&cont, _("Referenced by:"));
+ for (i = 0; i < tuples; i++)
+ {
+ printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
+ PQgetvalue(result, i, i_ontable),
+ PQgetvalue(result, i, i_conname),
+ PQgetvalue(result, i, i_condef));
+
+ printTableAddFooter(&cont, buf.data);
+ }
+ }
+ PQclear(result);
/* print any row-level policies */
if (pset.sversion >= 90500)
--
2.43.5
v5-0005-WIP-Add-support-for-NOT-ENFORCED-in-foreign-key-c.patchapplication/x-patch; name=v5-0005-WIP-Add-support-for-NOT-ENFORCED-in-foreign-key-c.patchDownload
From b873596140c7238b4f79fc17adf70848dc764ffe Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Tue, 3 Dec 2024 17:21:46 +0530
Subject: [PATCH v5 5/5] WIP-Add support for NOT ENFORCED in foreign key
constraints.
Typically, when a foreign key (FK) constraint is created on a table,
action and check triggers are added to maintain data integrity. With
this patch, if a constraint is marked as NOT ENFORCED, integrity
checks are no longer required, making these triggers unnecessary.
Consequently, when creating a NOT ENFORCED FK constraint, triggers
will not be created, and the constraint will be marked as NOT VALID.
Similarly, if an existing FK constraint is changed to NOT ENFORCED,
the associated triggers will be dropped, and the constraint will also
be marked as NOT VALID. Conversely, if a NOT ENFORCED FK constraint is
changed to ENFORCED, the necessary triggers will be created, and the
constraint will remain in the NOT VALID state.
== WIP ==
TODO: The restriction that not allowing NOT VALID foreign key
constrain on partitioned table is baypass now for the NOT ENFORCED
constratin but could be removed or simply go ahead by bypassing it
which yet to confirm and that the reason added this TODO and mark this
patch WIP.
---
doc/src/sgml/advanced.sgml | 13 +
doc/src/sgml/catalogs.sgml | 2 +-
doc/src/sgml/ddl.sgml | 12 +-
doc/src/sgml/ref/alter_table.sgml | 10 +-
doc/src/sgml/ref/create_table.sgml | 10 +-
src/backend/catalog/pg_constraint.c | 5 +-
src/backend/commands/tablecmds.c | 474 ++++++++++++++++------
src/backend/parser/gram.y | 6 +-
src/backend/parser/parse_utilcmd.c | 14 +-
src/backend/utils/cache/relcache.c | 1 +
src/include/utils/rel.h | 3 +
src/test/regress/expected/constraints.out | 9 +-
src/test/regress/expected/foreign_key.out | 126 +++++-
src/test/regress/sql/constraints.sql | 1 +
src/test/regress/sql/foreign_key.sql | 89 +++-
15 files changed, 608 insertions(+), 167 deletions(-)
diff --git a/doc/src/sgml/advanced.sgml b/doc/src/sgml/advanced.sgml
index 755c9f14850..b7d90de69d7 100644
--- a/doc/src/sgml/advanced.sgml
+++ b/doc/src/sgml/advanced.sgml
@@ -125,6 +125,19 @@ ERROR: insert or update on table "weather" violates foreign key constraint "wea
DETAIL: Key (city)=(Berkeley) is not present in table "cities".
</screen>
</para>
+ <para>
+ If, for some reason, you do not want to enforce this constraint and wish to
+ avoid the error, you can modify the foreign key constraint to <literal>NOT ENFORCED</literal>,
+ or specify it in the <link linkend="sql-createtable"><command>CREATE TABLE</command></link>.
+ </para>
+ <para>
+ Let's change the constraint to <literal>NOT ENFORCED</literal>, and when you
+ attempt to reinsert the same record, it will not result in any error.
+<programlisting>
+ALTER TABLE weather ALTER CONSTRAINT weather_city_fkey NOT ENFORCED;
+INSERT INTO weather VALUES ('Berkeley', 45, 53, 0.0, '1994-11-28');
+</programlisting>
+ </para>
<para>
The behavior of foreign keys can be finely tuned to your
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index a56ef4df562..3bb04574481 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2608,7 +2608,7 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l
</para>
<para>
Is the constraint enforceable?
- Currently, can be false only for CHECK constraints
+ Currently, can be false only for foreign keys and CHECK constraints
</para></entry>
</row>
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index 9a20396dae4..cda36576670 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -1058,11 +1058,13 @@ CREATE TABLE example (
</indexterm>
<para>
- A foreign key constraint specifies that the values in a column (or
- a group of columns) must match the values appearing in some row
- of another table.
- We say this maintains the <firstterm>referential
- integrity</firstterm> between two related tables.
+ A foreign key constraint with <literal>ENFORCED</literal>, the default
+ setting, specifies that the values in a column (or a group of columns)
+ must match the values appearing in some row of another table. We say this
+ maintains the <firstterm>referential integrity</firstterm> between two
+ related tables. If a foreign key constraint is created with <literal>NOT ENFORCED</literal>,
+ the check for aforementioned <firstterm>referential integrity</firstterm>
+ will not be performed.
</para>
<para>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index ca38937cf1e..fa807cb9f3a 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -555,6 +555,13 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
This form alters the attributes of a constraint that was previously
created. Currently only foreign key constraints may be altered.
</para>
+ <para>
+ Note that when a constraint is changed to <literal>NOT ENFORCED</literal>,
+ it is also marked as <literal>NOT VALID</literal>. While this might seem
+ unnecessary, it is done for consistency and should be set accordingly.
+ This ensures that if the constraint is later changed to <literal>ENFORCED</literal>,
+ it will automatically be in the correct <literal>NOT VALID</literal> state.
+ </para>
</listitem>
</varlistentry>
@@ -565,7 +572,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
This form validates a foreign key or check constraint that was
previously created as <literal>NOT VALID</literal>, by scanning the
table to ensure there are no rows for which the constraint is not
- satisfied. Nothing happens if the constraint is already marked valid.
+ satisfied. If the constraint is not enforced, an error is thrown.
+ Nothing happens if the constraint is already marked valid.
(See <xref linkend="sql-altertable-notes"/> below for an explanation
of the usefulness of this command.)
</para>
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index cfd01c4ef2b..89fc7354c00 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -1382,11 +1382,11 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
<term><literal>NOT ENFORCED</literal></term>
<listitem>
<para>
- This is currently only allowed for <literal>CHECK</literal> constraints.
- If the constraint is <literal>NOT ENFORCED</literal>, this clause
- specifies that the constraint check will be skipped. When the constraint
- is <literal>ENFORCED</literal>, check is performed after each statement;
- the latter is the default.
+ This is currently only allowed for foreign key and <literal>CHECK</literal>
+ constraints. If the constraint is <literal>NOT ENFORCED</literal>, this
+ clause specifies that the constraint check will be skipped. When the
+ constraint is <literal>ENFORCED</literal>, check is performed after each
+ statement; the latter is the default.
</para>
<para>
Note that if the constraint is <literal>NOT ENFORCED</literal>, it is also
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index f9e4c3d3cfa..3e8a6eb94f3 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -100,8 +100,9 @@ CreateConstraintEntry(const char *constraintName,
ObjectAddresses *addrs_auto;
ObjectAddresses *addrs_normal;
- /* Only CHECK constraint can be not enforced */
- Assert(isEnforced || constraintType == CONSTRAINT_CHECK);
+ /* Only CHECK or FOREIGN KEY constraint can be not enforced */
+ Assert(isEnforced || constraintType == CONSTRAINT_CHECK ||
+ constraintType == CONSTRAINT_FOREIGN);
/* NOT ENFORCED constraint must be NOT VALID */
Assert(isEnforced || !isValidated);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 45861bc2417..c4f17aa2018 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -389,19 +389,29 @@ static void AlterIndexNamespaces(Relation classRel, Relation rel,
static void AlterSeqNamespaces(Relation classRel, Relation rel,
Oid oldNspOid, Oid newNspOid, ObjectAddresses *objsMoved,
LOCKMODE lockmode);
-static ObjectAddress ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd,
+static ObjectAddress ATExecAlterConstraint(List **wqueue, Relation rel, AlterTableCmd *cmd,
bool recurse, bool recursing, LOCKMODE lockmode);
-static bool ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
+static bool ATExecAlterConstrRecurse(List **wqueue, Constraint *cmdcon,
+ Relation conrel, Relation tgrel,
const Oid fkrelid, const Oid pkrelid,
HeapTuple contuple, List **otherrelids,
LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
Oid ReferencedParentUpdTrigger,
Oid ReferencingParentInsTrigger,
Oid ReferencingParentUpdTrigger);
-static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
+static void ATExecAlterConstrEnforceability(List **wqueue, Constraint *cmdcon,
+ Relation conrel, Relation tgrel,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger);
+static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Oid fkrel,
bool deferrable, bool initdeferred,
List **otherrelids);
-static void ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
+static void ATExecAlterChildConstr(List **wqueue, Constraint *cmdcon,
+ Relation conrel, Relation tgrel,
const Oid fkrelid, const Oid pkrelid,
HeapTuple contuple, List **otherrelids,
LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
@@ -5418,7 +5428,7 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
lockmode);
break;
case AT_AlterConstraint: /* ALTER CONSTRAINT */
- address = ATExecAlterConstraint(rel, cmd, false, false, lockmode);
+ address = ATExecAlterConstraint(wqueue, rel, cmd, false, false, lockmode);
break;
case AT_ValidateConstraint: /* VALIDATE CONSTRAINT */
address = ATExecValidateConstraint(wqueue, rel, cmd->name, cmd->recurse,
@@ -9762,7 +9772,16 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
errmsg("cannot use ONLY for foreign key on partitioned table \"%s\" referencing relation \"%s\"",
RelationGetRelationName(rel),
RelationGetRelationName(pkrel))));
- if (fkconstraint->skip_validation && !fkconstraint->initially_valid)
+
+ /*
+ * TODO: I'm not sure why this restriction exists; NOT VALID foreign
+ * keys seem to work fine on partitioned tables, with behavior matching
+ * that of NOT VALID foreign keys on regular tables. I still need to
+ * confirm the necessity of this restriction, and it might be something
+ * that can be removed in the future. For now, I have bypassed this
+ * error for NOT ENFORCED foreign key constraints.
+ */
+ if (!fkconstraint->is_not_enforced && fkconstraint->skip_validation && !fkconstraint->initially_valid)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot add NOT VALID foreign key on partitioned table \"%s\" referencing relation \"%s\"",
@@ -10436,7 +10455,7 @@ addFkConstraint(addFkConstraintSides fkside,
CONSTRAINT_FOREIGN,
fkconstraint->deferrable,
fkconstraint->initdeferred,
- true, /* Is Enforced */
+ !fkconstraint->is_not_enforced, /* Is Enforced */
fkconstraint->initially_valid,
parentConstr,
RelationGetRelid(rel),
@@ -10554,20 +10573,22 @@ addFkRecurseReferenced(Constraint *fkconstraint, Relation rel,
Oid parentDelTrigger, Oid parentUpdTrigger,
bool with_period)
{
- Oid deleteTriggerOid,
- updateTriggerOid;
+ Oid deleteTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
Assert(CheckRelationLockedByMe(pkrel, ShareRowExclusiveLock, true));
Assert(CheckRelationLockedByMe(rel, ShareRowExclusiveLock, true));
/*
- * Create the action triggers that enforce the constraint.
+ * Create action triggers to enforce the constraint, or skip them if the
+ * constraint is not enforced.
*/
- createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
- fkconstraint,
- parentConstr, indexOid,
- parentDelTrigger, parentUpdTrigger,
- &deleteTriggerOid, &updateTriggerOid);
+ if (!fkconstraint->is_not_enforced)
+ createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
+ fkconstraint,
+ parentConstr, indexOid,
+ parentDelTrigger, parentUpdTrigger,
+ &deleteTriggerOid, &updateTriggerOid);
/*
* If the referenced table is partitioned, recurse on ourselves to handle
@@ -10688,8 +10709,8 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
Oid parentInsTrigger, Oid parentUpdTrigger,
bool with_period)
{
- Oid insertTriggerOid,
- updateTriggerOid;
+ Oid insertTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
Assert(OidIsValid(parentConstr));
Assert(CheckRelationLockedByMe(rel, ShareRowExclusiveLock, true));
@@ -10701,29 +10722,32 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
errmsg("foreign key constraints are not supported on foreign tables")));
/*
- * Add the check triggers to it and, if necessary, schedule it to be
- * checked in Phase 3.
+ * Add check triggers if the constraint is enforced, and if needed,
+ * schedule them to be checked in Phase 3.
*
* If the relation is partitioned, drill down to do it to its partitions.
*/
- createForeignKeyCheckTriggers(RelationGetRelid(rel),
- RelationGetRelid(pkrel),
- fkconstraint,
- parentConstr,
- indexOid,
- parentInsTrigger, parentUpdTrigger,
- &insertTriggerOid, &updateTriggerOid);
+ if (!fkconstraint->is_not_enforced)
+ createForeignKeyCheckTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
+ fkconstraint,
+ parentConstr,
+ indexOid,
+ parentInsTrigger, parentUpdTrigger,
+ &insertTriggerOid, &updateTriggerOid);
if (rel->rd_rel->relkind == RELKIND_RELATION)
{
/*
* Tell Phase 3 to check that the constraint is satisfied by existing
- * rows. We can skip this during table creation, when requested
- * explicitly by specifying NOT VALID in an ADD FOREIGN KEY command,
- * and when we're recreating a constraint following a SET DATA TYPE
- * operation that did not impugn its validity.
+ * rows. We can skip this during table creation, when constraint is
+ * specified as NOT ENFORCED, when requested explicitly by specifying
+ * NOT VALID in an ADD FOREIGN KEY command, and when we're recreating
+ * a constraint following a SET DATA TYPE operation that did not
+ * impugn its validity.
*/
- if (wqueue && !old_check_ok && !fkconstraint->skip_validation)
+ if (wqueue && !old_check_ok && !fkconstraint->is_not_enforced &&
+ !fkconstraint->skip_validation)
{
NewConstraint *newcon;
AlteredTableInfo *tab;
@@ -10958,8 +10982,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
AttrNumber confdelsetcols[INDEX_MAX_KEYS];
Constraint *fkconstraint;
ObjectAddress address;
- Oid deleteTriggerOid,
- updateTriggerOid;
+ Oid deleteTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(constrOid));
if (!HeapTupleIsValid(tuple))
@@ -11019,8 +11043,9 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
fkconstraint->fk_del_set_cols = NIL;
fkconstraint->old_conpfeqop = NIL;
fkconstraint->old_pktable_oid = InvalidOid;
+ fkconstraint->is_not_enforced = !constrForm->conenforced;
+ fkconstraint->initially_valid = constrForm->convalidated;
fkconstraint->skip_validation = false;
- fkconstraint->initially_valid = true;
/* set up colnames that are used to generate the constraint name */
for (int i = 0; i < numfks; i++)
@@ -11048,9 +11073,10 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
* parent OIDs for similar triggers that will be created on the
* partition in addFkRecurseReferenced().
*/
- GetForeignKeyActionTriggers(trigrel, constrOid,
- constrForm->confrelid, constrForm->conrelid,
- &deleteTriggerOid, &updateTriggerOid);
+ if (constrForm->conenforced)
+ GetForeignKeyActionTriggers(trigrel, constrOid,
+ constrForm->confrelid, constrForm->conrelid,
+ &deleteTriggerOid, &updateTriggerOid);
/* Add this constraint ... */
address = addFkConstraint(addFkReferencedSide,
@@ -11216,17 +11242,18 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
mapped_conkey[i] = attmap->attnums[conkey[i] - 1];
/*
- * Get the "check" triggers belonging to the constraint to pass as
- * parent OIDs for similar triggers that will be created on the
- * partition in addFkRecurseReferencing(). They are also passed to
- * tryAttachPartitionForeignKey() below to simply assign as parents to
- * the partition's existing "check" triggers, that is, if the
- * corresponding constraints is deemed attachable to the parent
- * constraint.
+ * Get the "check" triggers belonging to the constraint, if it is
+ * enforced, to pass as parent OIDs for similar triggers that will be
+ * created on the partition in addFkRecurseReferencing(). They are
+ * also passed to tryAttachPartitionForeignKey() below to simply
+ * assign as parents to the partition's existing "check" triggers,
+ * that is, if the corresponding constraints is deemed attachable to
+ * the parent constraint.
*/
- GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
- constrForm->confrelid, constrForm->conrelid,
- &insertTriggerOid, &updateTriggerOid);
+ if (constrForm->conenforced)
+ GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
+ constrForm->confrelid, constrForm->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
/*
* Before creating a new constraint, see whether any existing FKs are
@@ -11278,8 +11305,9 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
fkconstraint->fk_del_set_cols = NIL;
fkconstraint->old_conpfeqop = NIL;
fkconstraint->old_pktable_oid = InvalidOid;
- fkconstraint->skip_validation = false;
+ fkconstraint->is_not_enforced = !constrForm->conenforced;
fkconstraint->initially_valid = constrForm->convalidated;
+ fkconstraint->skip_validation = false;
for (int i = 0; i < numfks; i++)
{
Form_pg_attribute att;
@@ -11362,8 +11390,6 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
ScanKeyData key;
SysScanDesc scan;
HeapTuple trigtup;
- Oid insertTriggerOid,
- updateTriggerOid;
parentConstrTup = SearchSysCache1(CONSTROID,
ObjectIdGetDatum(parentConstrOid));
@@ -11405,6 +11431,7 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
!partConstr->convalidated ||
partConstr->condeferrable != parentConstr->condeferrable ||
partConstr->condeferred != parentConstr->condeferred ||
+ partConstr->conenforced != parentConstr->conenforced ||
partConstr->confupdtype != parentConstr->confupdtype ||
partConstr->confdeltype != parentConstr->confdeltype ||
partConstr->confmatchtype != parentConstr->confmatchtype)
@@ -11464,18 +11491,25 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
ConstraintSetParentConstraint(fk->conoid, parentConstrOid, partRelid);
/*
- * Like the constraint, attach partition's "check" triggers to the
- * corresponding parent triggers.
+ * Similar to the constraint, attach the partition's "check" triggers to
+ * the corresponding parent triggers if the constraint is enforced.
+ * Non-enforced constraints do not have these triggers.
*/
- GetForeignKeyCheckTriggers(trigrel,
- fk->conoid, fk->confrelid, fk->conrelid,
- &insertTriggerOid, &updateTriggerOid);
- Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
- TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
- partRelid);
- Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
- TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
- partRelid);
+ if (fk->conenforced)
+ {
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
+ partRelid);
+ Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
+ partRelid);
+ }
/*
* If the referenced table is partitioned, then the partition we're
@@ -11689,8 +11723,8 @@ GetForeignKeyCheckTriggers(Relation trigrel,
* InvalidObjectAddress.
*/
static ObjectAddress
-ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
- bool recursing, LOCKMODE lockmode)
+ATExecAlterConstraint(List **wqueue, Relation rel, AlterTableCmd *cmd,
+ bool recurse, bool recursing, LOCKMODE lockmode)
{
Constraint *cmdcon;
Relation conrel;
@@ -11792,12 +11826,14 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
address = InvalidObjectAddress;
if (currcon->condeferrable != cmdcon->deferrable ||
currcon->condeferred != cmdcon->initdeferred ||
+ currcon->conenforced != !cmdcon->is_not_enforced ||
rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
- if (ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, currcon->conrelid,
- currcon->confrelid, contuple, &otherrelids,
- lockmode, InvalidOid, InvalidOid,
- InvalidOid, InvalidOid))
+ if (ATExecAlterConstrRecurse(wqueue, cmdcon, conrel, tgrel,
+ currcon->conrelid, currcon->confrelid,
+ contuple, &otherrelids, lockmode,
+ InvalidOid, InvalidOid, InvalidOid,
+ InvalidOid))
ObjectAddressSet(address, ConstraintRelationId, currcon->oid);
}
@@ -11829,8 +11865,8 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
* but existing releases don't do that.)
*/
static bool
-ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
- const Oid fkrelid, const Oid pkrelid,
+ATExecAlterConstrRecurse(List **wqueue, Constraint *cmdcon, Relation conrel,
+ Relation tgrel, const Oid fkrelid, const Oid pkrelid,
HeapTuple contuple, List **otherrelids,
LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
Oid ReferencedParentUpdTrigger,
@@ -11839,19 +11875,18 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
{
Form_pg_constraint currcon;
Oid conoid;
+ Oid conrelid;
Oid refrelid;
bool changed = false;
- Relation rel;
/* since this function recurses, it could be driven to stack overflow */
check_stack_depth();
currcon = (Form_pg_constraint) GETSTRUCT(contuple);
conoid = currcon->oid;
+ conrelid = currcon->conrelid;
refrelid = currcon->confrelid;
- rel = table_open(currcon->conrelid, lockmode);
-
/*
* Update pg_constraint with the flags from cmdcon.
*
@@ -11859,7 +11894,8 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
* silently do nothing.
*/
if (currcon->condeferrable != cmdcon->deferrable ||
- currcon->condeferred != cmdcon->initdeferred)
+ currcon->condeferred != cmdcon->initdeferred ||
+ currcon->conenforced != !cmdcon->is_not_enforced)
{
HeapTuple copyTuple;
Form_pg_constraint copy_con;
@@ -11868,6 +11904,17 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
copy_con->condeferrable = cmdcon->deferrable;
copy_con->condeferred = cmdcon->initdeferred;
+ copy_con->conenforced = !cmdcon->is_not_enforced;
+
+ /*
+ * NB: The convalidated status is irrelevant when the constraint is set to
+ * NOT ENFORCED, but for consistency, it should be set accordingly.
+ * This ensures that if the constraint is later changed to ENFORCED, it
+ * will automatically be in the correct NOT VALID state.
+ */
+ if (cmdcon->is_not_enforced)
+ copy_con->convalidated = false;
+
CatalogTupleUpdate(conrel, ©Tuple->t_self, copyTuple);
InvokeObjectPostAlterHook(ConstraintRelationId,
@@ -11877,38 +11924,188 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
changed = true;
/* Make new constraint flags visible to others */
- CacheInvalidateRelcache(rel);
+ CacheInvalidateRelcacheByRelid(conrelid);
+ }
+ if (currcon->conenforced != !cmdcon->is_not_enforced)
+ {
+ ATExecAlterConstrEnforceability(wqueue, cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, contuple, otherrelids, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
+ }
+ else
+ {
/*
* Now we need to update the multiple entries in pg_trigger that
* implement the constraint.
*/
- AlterConstrTriggerDeferrability(conoid, tgrel, rel, cmdcon->deferrable,
- cmdcon->initdeferred, otherrelids);
+ AlterConstrTriggerDeferrability(conoid, tgrel, conrelid,
+ cmdcon->deferrable,
+ cmdcon->initdeferred,
+ otherrelids);
+
+ /*
+ * If the table at either end of the constraint is partitioned, we
+ * need to recurse and handle every constraint that is a child of this
+ * one.
+ *
+ * (This assumes that the recurse flag is forcibly set for partitioned
+ * tables, and not set for legacy inheritance, though we don't check
+ * for that here.)
+ */
+ if (get_rel_relkind(conrelid) == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)
+ ATExecAlterChildConstr(wqueue, cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, contuple, otherrelids, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
}
- /*
- * If the table at either end of the constraint is partitioned, we need to
- * recurse and handle every constraint that is a child of this one.
- *
- * (This assumes that the recurse flag is forcibly set for partitioned
- * tables, and not set for legacy inheritance, though we don't check for
- * that here.)
- */
- if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
- get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)
- ATExecAlterChildConstr(cmdcon, conrel, tgrel, fkrelid, pkrelid,
- contuple, otherrelids, lockmode,
- ReferencedParentDelTrigger,
- ReferencedParentUpdTrigger,
- ReferencingParentInsTrigger,
- ReferencingParentUpdTrigger);
-
- table_close(rel, NoLock);
-
return changed;
}
+/*
+ * A subroutine of ATExecAlterConstrRecurse that updates the enforceability of
+ * a foreign key constraint. Depending on whether the constraint is being set
+ * to enforced or not enforced, it creates or drops the trigger accordingly.
+ *
+ * The arguments to this function have the same meaning as the arguments to
+ * ATExecAlterConstrRecurse.
+ */
+static void
+ATExecAlterConstrEnforceability(List **wqueue, Constraint *cmdcon,
+ Relation conrel, Relation tgrel,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger)
+{
+ Form_pg_constraint currcon;
+ Oid conoid;
+
+ currcon = (Form_pg_constraint) GETSTRUCT(contuple);
+ conoid = currcon->oid;
+
+ /* Drop triggers */
+ if (cmdcon->is_not_enforced)
+ {
+ HeapTuple tgtuple;
+ ScanKeyData tgkey;
+ SysScanDesc tgscan;
+
+ /*
+ * When setting a constraint to NOT ENFORCED, the constraint triggers
+ * need to be dropped. Therefore, we must process the child relations
+ * first, followed by the parent, to account for dependencies.
+ */
+ if (get_rel_relkind(currcon->conrelid) == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE)
+ ATExecAlterChildConstr(wqueue, cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, contuple, otherrelids, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
+
+ ScanKeyInit(&tgkey,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+ tgscan = systable_beginscan(tgrel, TriggerConstraintIndexId, true,
+ NULL, 1, &tgkey);
+ while (HeapTupleIsValid(tgtuple = systable_getnext(tgscan)))
+ {
+ Form_pg_trigger tgform = (Form_pg_trigger) GETSTRUCT(tgtuple);
+ ObjectAddress trigger;
+
+ /*
+ * Delete only RI_FKey_noaction_del, RI_FKey_noaction_upd,
+ * RI_FKey_check_ins and RI_FKey_check_upd triggers, but not
+ * others; see createForeignKeyActionTriggers and
+ * CreateFKCheckTrigger.
+ */
+ if (tgform->tgfoid != F_RI_FKEY_NOACTION_DEL &&
+ tgform->tgfoid != F_RI_FKEY_NOACTION_UPD &&
+ tgform->tgfoid != F_RI_FKEY_CHECK_INS &&
+ tgform->tgfoid != F_RI_FKEY_CHECK_UPD)
+ continue;
+
+ deleteDependencyRecordsFor(TriggerRelationId, tgform->oid,
+ false);
+ /* make dependency deletion visible to performDeletion */
+ CommandCounterIncrement();
+ ObjectAddressSet(trigger, TriggerRelationId, tgform->oid);
+ performDeletion(&trigger, DROP_RESTRICT, 0);
+ /* make trigger drop visible, in case the loop iterates */
+ CommandCounterIncrement();
+ }
+
+ systable_endscan(tgscan);
+ }
+ else /* Create triggers */
+ {
+ Relation rel;
+ Oid ReferencedDelTriggerOid = InvalidOid,
+ ReferencedUpdTriggerOid = InvalidOid,
+ ReferencingInsTriggerOid = InvalidOid,
+ ReferencingUpdTriggerOid = InvalidOid;
+
+ /* Update the foreign key action required for trigger creation. */
+ cmdcon->fk_matchtype = currcon->confmatchtype;
+ cmdcon->fk_upd_action = currcon->confupdtype;
+ cmdcon->fk_del_action = currcon->confdeltype;
+
+ rel = table_open(currcon->conrelid, lockmode);
+
+ /* Create referenced triggers */
+ if (currcon->conrelid == fkrelid)
+ createForeignKeyActionTriggers(rel,
+ currcon->confrelid,
+ cmdcon,
+ conoid,
+ currcon->conindid,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ &ReferencedDelTriggerOid,
+ &ReferencedUpdTriggerOid);
+
+ /* Create referencing triggers */
+ if (currcon->confrelid == pkrelid)
+ createForeignKeyCheckTriggers(currcon->conrelid,
+ pkrelid,
+ cmdcon,
+ conoid,
+ currcon->conindid,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger,
+ &ReferencingInsTriggerOid,
+ &ReferencingUpdTriggerOid);
+
+
+ /*
+ * If the table at either end of the constraint is partitioned, we
+ * need to recurse and create triggers for each constraint that is a
+ * child of this one.
+ */
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE)
+ ATExecAlterChildConstr(wqueue, cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, contuple, otherrelids, lockmode,
+ ReferencedDelTriggerOid,
+ ReferencedUpdTriggerOid,
+ ReferencingInsTriggerOid,
+ ReferencingUpdTriggerOid);
+ table_close(rel, NoLock);
+ }
+}
+
/*
* A subroutine of ATExecAlterConstrRecurse that updated constraint trigger's
* deferrability.
@@ -11917,7 +12114,7 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
* ATExecAlterConstrRecurse.
*/
static void
-AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
+AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Oid fkrel,
bool deferrable, bool initdeferred,
List **otherrelids)
{
@@ -11943,7 +12140,7 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
* other rels that don't have a trigger whose properties change, but
* let's be conservative.)
*/
- if (tgform->tgrelid != RelationGetRelid(rel))
+ if (tgform->tgrelid != fkrel)
*otherrelids = list_append_unique_oid(*otherrelids,
tgform->tgrelid);
@@ -11982,8 +12179,8 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
* ATExecAlterConstrRecurse.
*/
static void
-ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
- const Oid fkrelid, const Oid pkrelid,
+ATExecAlterChildConstr(List **wqueue, Constraint *cmdcon, Relation conrel,
+ Relation tgrel, const Oid fkrelid, const Oid pkrelid,
HeapTuple contuple, List **otherrelids,
LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
Oid ReferencedParentUpdTrigger,
@@ -12008,9 +12205,9 @@ ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
true, NULL, 1, &pkey);
while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
- ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, fkrelid, pkrelid,
- childtup, otherrelids, lockmode,
- ReferencedParentDelTrigger,
+ ATExecAlterConstrRecurse(wqueue, cmdcon, conrel, tgrel,
+ fkrelid, pkrelid, childtup, otherrelids,
+ lockmode, ReferencedParentDelTrigger,
ReferencedParentUpdTrigger,
ReferencingParentInsTrigger,
ReferencingParentUpdTrigger);
@@ -12088,25 +12285,33 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
if (con->contype == CONSTRAINT_FOREIGN)
{
- NewConstraint *newcon;
- Constraint *fkconstraint;
+ /*
+ * Queue validation for phase 3 only if constraint is enforced;
+ * otherwise, adding it to the validation queue won't be very
+ * effective, as the verification will be skipped.
+ */
+ if (con->conenforced)
+ {
+ NewConstraint *newcon;
+ Constraint *fkconstraint;
- /* Queue validation for phase 3 */
- fkconstraint = makeNode(Constraint);
- /* for now this is all we need */
- fkconstraint->conname = constrName;
+ /* Queue validation for phase 3 */
+ fkconstraint = makeNode(Constraint);
+ /* for now this is all we need */
+ fkconstraint->conname = constrName;
- newcon = (NewConstraint *) palloc0(sizeof(NewConstraint));
- newcon->name = constrName;
- newcon->contype = CONSTR_FOREIGN;
- newcon->refrelid = con->confrelid;
- newcon->refindid = con->conindid;
- newcon->conid = con->oid;
- newcon->qual = (Node *) fkconstraint;
+ newcon = (NewConstraint *) palloc0(sizeof(NewConstraint));
+ newcon->name = constrName;
+ newcon->contype = CONSTR_FOREIGN;
+ newcon->refrelid = con->confrelid;
+ newcon->refindid = con->conindid;
+ newcon->conid = con->oid;
+ newcon->qual = (Node *) fkconstraint;
- /* Find or create work queue entry for this table */
- tab = ATGetQueueEntry(wqueue, rel);
- tab->constraints = lappend(tab->constraints, newcon);
+ /* Find or create work queue entry for this table */
+ tab = ATGetQueueEntry(wqueue, rel);
+ tab->constraints = lappend(tab->constraints, newcon);
+ }
/*
* We disallow creating invalid foreign keys to or from
@@ -19934,8 +20139,6 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
Form_pg_constraint conform;
- Oid insertTriggerOid,
- updateTriggerOid;
contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
if (!HeapTupleIsValid(contup))
@@ -19958,17 +20161,25 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
/*
* Also, look up the partition's "check" triggers corresponding to the
- * constraint being detached and detach them from the parent triggers.
+ * enforced constraint being detached and detach them from the parent
+ * triggers. Non-enforced constraints do not have these triggers;
+ * therefore, this step is not needed.
*/
- GetForeignKeyCheckTriggers(trigrel,
- fk->conoid, fk->confrelid, fk->conrelid,
- &insertTriggerOid, &updateTriggerOid);
- Assert(OidIsValid(insertTriggerOid));
- TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
- RelationGetRelid(partRel));
- Assert(OidIsValid(updateTriggerOid));
- TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
- RelationGetRelid(partRel));
+ if (fk->conenforced)
+ {
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ Assert(OidIsValid(updateTriggerOid));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ }
/*
* Lastly, create the action triggers on the referenced table, using
@@ -20008,8 +20219,9 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
fkconstraint->conname = pstrdup(NameStr(conform->conname));
fkconstraint->deferrable = conform->condeferrable;
fkconstraint->initdeferred = conform->condeferred;
+ fkconstraint->is_not_enforced = !conform->conenforced;
+ fkconstraint->initially_valid = conform->convalidated;
fkconstraint->skip_validation = true;
- fkconstraint->initially_valid = true;
/* a few irrelevant fields omitted here */
fkconstraint->pktable = NULL;
fkconstraint->fk_attrs = NIL;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index a0d5e181ed9..711dab3151c 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -2660,7 +2660,8 @@ alter_table_cmd:
processCASbits($4, @4, "ALTER CONSTRAINT statement",
&c->deferrable,
&c->initdeferred,
- NULL, NULL, NULL, yyscanner);
+ &c->is_not_enforced,
+ NULL, NULL, yyscanner);
$$ = (Node *) n;
}
/* ALTER TABLE <name> VALIDATE CONSTRAINT ... */
@@ -4026,6 +4027,7 @@ ColConstraintElem:
n->fk_del_set_cols = ($5)->deleteAction->cols;
n->skip_validation = false;
n->initially_valid = true;
+ n->is_not_enforced = false;
$$ = (Node *) n;
}
;
@@ -4301,7 +4303,7 @@ ConstraintElem:
n->fk_del_set_cols = ($11)->deleteAction->cols;
processCASbits($12, @12, "FOREIGN KEY",
&n->deferrable, &n->initdeferred,
- NULL, &n->skip_validation,
+ &n->is_not_enforced, &n->skip_validation,
NULL,
yyscanner);
n->initially_valid = !n->skip_validation;
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index e629c3186ef..58916b4d2b6 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -2961,8 +2961,10 @@ transformFKConstraints(CreateStmtContext *cxt,
/*
* If CREATE TABLE or adding a column with NULL default, we can safely
- * skip validation of FK constraints, and nonetheless mark them valid.
- * (This will override any user-supplied NOT VALID flag.)
+ * skip validation of FK constraints, and mark them as valid based on the
+ * constraint enforcement flag, since NOT ENFORCED constraints must always
+ * be marked as NOT VALID. (This will override any user-supplied NOT VALID
+ * flag.)
*/
if (skipValidation)
{
@@ -2971,7 +2973,7 @@ transformFKConstraints(CreateStmtContext *cxt,
Constraint *constraint = (Constraint *) lfirst(fkclist);
constraint->skip_validation = true;
- constraint->initially_valid = true;
+ constraint->initially_valid = !constraint->is_not_enforced;
}
}
@@ -3966,7 +3968,8 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
case CONSTR_ATTR_ENFORCED:
if (lastprimarycon &&
- lastprimarycon->contype != CONSTR_CHECK)
+ lastprimarycon->contype != CONSTR_CHECK &&
+ lastprimarycon->contype != CONSTR_FOREIGN)
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("misplaced ENFORCED clause"),
@@ -3982,7 +3985,8 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
case CONSTR_ATTR_NOT_ENFORCED:
if (lastprimarycon &&
- lastprimarycon->contype != CONSTR_CHECK)
+ lastprimarycon->contype != CONSTR_CHECK &&
+ lastprimarycon->contype != CONSTR_FOREIGN)
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("misplaced NOT ENFORCED clause"),
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index d5bd72d7ad4..478fa51f936 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4700,6 +4700,7 @@ RelationGetFKeyList(Relation relation)
info->conoid = constraint->oid;
info->conrelid = constraint->conrelid;
info->confrelid = constraint->confrelid;
+ info->conenforced = constraint->conenforced;
DeconstructFkConstraintRow(htup, &info->nkeys,
info->conkey,
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 87002049538..2db011ddfb7 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -284,6 +284,9 @@ typedef struct ForeignKeyCacheInfo
/* number of columns in the foreign key */
int nkeys;
+ /* Is enforced ? */
+ bool conenforced;
+
/*
* these arrays each have nkeys valid entries:
*/
diff --git a/src/test/regress/expected/constraints.out b/src/test/regress/expected/constraints.out
index d2c8e6ad172..1fe2a76fa34 100644
--- a/src/test/regress/expected/constraints.out
+++ b/src/test/regress/expected/constraints.out
@@ -744,14 +744,11 @@ CREATE TABLE UNIQUE_NOTEN_TBL(i int UNIQUE NOT ENFORCED);
ERROR: misplaced NOT ENFORCED clause
LINE 1: CREATE TABLE UNIQUE_NOTEN_TBL(i int UNIQUE NOT ENFORCED);
^
+-- XXX: error message is misleading here
ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
-ERROR: ALTER CONSTRAINT statement constraints cannot be marked ENFORCED
-LINE 1: ...TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
- ^
+ERROR: constraint "unique_tbl_i_key" of relation "unique_tbl" is not a foreign key constraint
ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORCED;
-ERROR: ALTER CONSTRAINT statement constraints cannot be marked NOT ENFORCED
-LINE 1: ...ABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORC...
- ^
+ERROR: constraint "unique_tbl_i_key" of relation "unique_tbl" is not a foreign key constraint
DROP TABLE unique_tbl;
--
-- EXCLUDE constraints
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 3f459f70ac1..6524065bfd2 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -1,12 +1,44 @@
--
-- FOREIGN KEY
--
--- MATCH FULL
+-- NOT ENFORCED
--
-- First test, check and cascade
--
CREATE TABLE PKTABLE ( ptest1 int PRIMARY KEY, ptest2 text );
-CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE, ftest2 int );
+CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE NOT ENFORCED,
+ ftest2 int );
+-- Inserting into the foreign key table will not result in an error, even if
+-- there is no matching key in the referenced table.
+INSERT INTO FKTABLE VALUES (1, 2);
+INSERT INTO FKTABLE VALUES (2, 3);
+-- Check FKTABLE
+SELECT * FROM FKTABLE;
+ ftest1 | ftest2
+--------+--------
+ 1 | 2
+ 2 | 3
+(2 rows)
+
+-- Changing it back to ENFORCED will leave the constraint in the NOT VALID state
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+-- Which needs to be explicitly validated.
+ALTER TABLE FKTABLE VALIDATE CONSTRAINT fktable_ftest1_fkey;
+ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_fkey"
+DETAIL: Key (ftest1)=(1) is not present in table "pktable".
+-- Remove any existing rows that violate the constraint, then attempt to change
+-- it.
+TRUNCATE FKTABLE;
+ALTER TABLE FKTABLE VALIDATE CONSTRAINT fktable_ftest1_fkey;
+-- Any further inserts will fail due to the enforcement.
+INSERT INTO FKTABLE VALUES (3, 4);
+ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_fkey"
+DETAIL: Key (ftest1)=(3) is not present in table "pktable".
+--
+-- MATCH FULL
+--
+-- First test, check and cascade
+--
-- Insert test data into PKTABLE
INSERT INTO PKTABLE VALUES (1, 'Test1');
INSERT INTO PKTABLE VALUES (2, 'Test2');
@@ -351,6 +383,8 @@ INSERT INTO FKTABLE VALUES (1, NULL);
ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL;
ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_ftest2_fkey"
DETAIL: MATCH FULL does not allow mixing of null and nonnull key values.
+-- NOT ENFORCED will bypass the initial check.
+ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL NOT ENFORCED;
DROP TABLE FKTABLE;
DROP TABLE PKTABLE;
-- MATCH SIMPLE
@@ -1276,13 +1310,28 @@ INSERT INTO fktable VALUES (0, 20);
ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_fk_fkey"
DETAIL: Key (fk)=(20) is not present in table "pktable".
COMMIT;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT ENFORCED;
+BEGIN;
+-- doesn't match FK, no error.
+UPDATE pktable SET id = 10 WHERE id = 5;
+-- doesn't match PK, no error.
+INSERT INTO fktable VALUES (0, 20);
+ROLLBACK;
-- try additional syntax
-ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE ENFORCED;
-- illegal option
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE INITIALLY DEFERRED;
ERROR: constraint declared INITIALLY DEFERRED must be DEFERRABLE
LINE 1: ...e ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE INITIALLY ...
^
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORCED;
+ERROR: conflicting constraint properties
+LINE 1: ...fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORC...
+ ^
+CREATE TEMP TABLE fktable2 (fk int references pktable ENFORCED NOT ENFORCED);
+ERROR: multiple ENFORCED/NOT ENFORCED clauses not allowed
+LINE 1: ...ABLE fktable2 (fk int references pktable ENFORCED NOT ENFORC...
+ ^
-- test order of firing of FK triggers when several RI-induced changes need to
-- be made to the same row. This was broken by subtransaction-related
-- changes in 8.0.
@@ -1580,7 +1629,8 @@ ALTER TABLE fk_partitioned_fk DROP COLUMN fdrop1;
CREATE TABLE fk_partitioned_fk_1 (fdrop1 int, fdrop2 int, a int, fdrop3 int, b int);
ALTER TABLE fk_partitioned_fk_1 DROP COLUMN fdrop1, DROP COLUMN fdrop2, DROP COLUMN fdrop3;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FROM (0,0) TO (1000,1000);
-ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk;
+ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
@@ -1663,7 +1713,38 @@ DETAIL: Key (a, b)=(2500, 2502) is still referenced from table "fk_partitioned_
Indexes:
"fk_notpartitioned_pk_pkey" PRIMARY KEY, btree (a, b)
Referenced by:
- TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b)
+ TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) NOT VALID
+
+-- Check the exsting FK trigger
+CREATE TEMP TABLE tmp_trg_info AS SELECT tgtype, tgrelid::regclass, tgconstraint
+FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+SELECT count(1) FROM tmp_trg_info;
+ count
+-------
+ 14
+(1 row)
+
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+-- No triggers
+SELECT count(1) FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+ count
+-------
+ 0
+(1 row)
+
+-- Changing it back to ENFORCED will recreate the necessary triggers.
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+-- Should be exactly the same number of triggers found as before
+SELECT COUNT(1) FROM pg_trigger pgt JOIN tmp_trg_info tt
+ON (pgt.tgtype = tt.tgtype AND pgt.tgrelid = tt.tgrelid AND pgt.tgconstraint = tt.tgconstraint);
+ count
+-------
+ 14
+(1 row)
ALTER TABLE fk_partitioned_fk DROP CONSTRAINT fk_partitioned_fk_a_b_fkey;
-- done.
@@ -1868,6 +1949,41 @@ Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
Foreign-key constraints:
TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
+DROP TABLE fk_partitioned_fk_2;
+CREATE TABLE fk_partitioned_fk_2 (b int, a int,
+ FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
+BEGIN;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have two constraints
+\d fk_partitioned_fk_2
+ Table "public.fk_partitioned_fk_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ b | integer | | |
+ a | integer | | |
+Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
+Foreign-key constraints:
+ "fk_partitioned_fk_2_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED NOT VALID
+ TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
+
+DROP TABLE fk_partitioned_fk_2;
+ROLLBACK;
+BEGIN;
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have only one constraint
+\d fk_partitioned_fk_2
+ Table "public.fk_partitioned_fk_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ b | integer | | |
+ a | integer | | |
+Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
+Foreign-key constraints:
+ "fk_partitioned_fk_2_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED NOT VALID
+ TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED NOT VALID
+
+ROLLBACK;
DROP TABLE fk_partitioned_fk_2;
CREATE TABLE fk_partitioned_fk_4 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE) PARTITION BY RANGE (b, a);
CREATE TABLE fk_partitioned_fk_4_1 PARTITION OF fk_partitioned_fk_4 FOR VALUES FROM (1,1) TO (100,100);
diff --git a/src/test/regress/sql/constraints.sql b/src/test/regress/sql/constraints.sql
index 1f36116d25a..32c8b433bf0 100644
--- a/src/test/regress/sql/constraints.sql
+++ b/src/test/regress/sql/constraints.sql
@@ -534,6 +534,7 @@ SELECT * FROM unique_tbl;
-- enforcibility cannot be specified or set for unique constrain
CREATE TABLE UNIQUE_EN_TBL(i int UNIQUE ENFORCED);
CREATE TABLE UNIQUE_NOTEN_TBL(i int UNIQUE NOT ENFORCED);
+-- XXX: error message is misleading here
ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORCED;
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index 2e710e419c2..9b23fe8cf8f 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -2,13 +2,40 @@
-- FOREIGN KEY
--
--- MATCH FULL
+-- NOT ENFORCED
--
-- First test, check and cascade
--
CREATE TABLE PKTABLE ( ptest1 int PRIMARY KEY, ptest2 text );
-CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE, ftest2 int );
+CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE NOT ENFORCED,
+ ftest2 int );
+-- Inserting into the foreign key table will not result in an error, even if
+-- there is no matching key in the referenced table.
+INSERT INTO FKTABLE VALUES (1, 2);
+INSERT INTO FKTABLE VALUES (2, 3);
+
+-- Check FKTABLE
+SELECT * FROM FKTABLE;
+
+-- Changing it back to ENFORCED will leave the constraint in the NOT VALID state
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+-- Which needs to be explicitly validated.
+ALTER TABLE FKTABLE VALIDATE CONSTRAINT fktable_ftest1_fkey;
+
+-- Remove any existing rows that violate the constraint, then attempt to change
+-- it.
+TRUNCATE FKTABLE;
+ALTER TABLE FKTABLE VALIDATE CONSTRAINT fktable_ftest1_fkey;
+
+-- Any further inserts will fail due to the enforcement.
+INSERT INTO FKTABLE VALUES (3, 4);
+
+--
+-- MATCH FULL
+--
+-- First test, check and cascade
+--
-- Insert test data into PKTABLE
INSERT INTO PKTABLE VALUES (1, 'Test1');
INSERT INTO PKTABLE VALUES (2, 'Test2');
@@ -230,6 +257,9 @@ INSERT INTO FKTABLE VALUES (1, NULL);
ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL;
+-- NOT ENFORCED will bypass the initial check.
+ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL NOT ENFORCED;
+
DROP TABLE FKTABLE;
DROP TABLE PKTABLE;
@@ -968,10 +998,23 @@ INSERT INTO fktable VALUES (0, 20);
COMMIT;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT ENFORCED;
+
+BEGIN;
+
+-- doesn't match FK, no error.
+UPDATE pktable SET id = 10 WHERE id = 5;
+-- doesn't match PK, no error.
+INSERT INTO fktable VALUES (0, 20);
+
+ROLLBACK;
+
-- try additional syntax
-ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE ENFORCED;
-- illegal option
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORCED;
+CREATE TEMP TABLE fktable2 (fk int references pktable ENFORCED NOT ENFORCED);
-- test order of firing of FK triggers when several RI-induced changes need to
-- be made to the same row. This was broken by subtransaction-related
@@ -1182,7 +1225,8 @@ ALTER TABLE fk_partitioned_fk DROP COLUMN fdrop1;
CREATE TABLE fk_partitioned_fk_1 (fdrop1 int, fdrop2 int, a int, fdrop3 int, b int);
ALTER TABLE fk_partitioned_fk_1 DROP COLUMN fdrop1, DROP COLUMN fdrop2, DROP COLUMN fdrop3;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FROM (0,0) TO (1000,1000);
-ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk;
+ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
@@ -1236,6 +1280,27 @@ UPDATE fk_notpartitioned_pk SET b = 1502 WHERE a = 1500;
UPDATE fk_notpartitioned_pk SET b = 2504 WHERE a = 2500;
-- check psql behavior
\d fk_notpartitioned_pk
+
+-- Check the exsting FK trigger
+CREATE TEMP TABLE tmp_trg_info AS SELECT tgtype, tgrelid::regclass, tgconstraint
+FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+SELECT count(1) FROM tmp_trg_info;
+
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+-- No triggers
+SELECT count(1) FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+
+-- Changing it back to ENFORCED will recreate the necessary triggers.
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+
+-- Should be exactly the same number of triggers found as before
+SELECT COUNT(1) FROM pg_trigger pgt JOIN tmp_trg_info tt
+ON (pgt.tgtype = tt.tgtype AND pgt.tgrelid = tt.tgrelid AND pgt.tgconstraint = tt.tgconstraint);
+
ALTER TABLE fk_partitioned_fk DROP CONSTRAINT fk_partitioned_fk_a_b_fkey;
-- done.
DROP TABLE fk_notpartitioned_pk, fk_partitioned_fk;
@@ -1372,6 +1437,22 @@ ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN
\d fk_partitioned_fk_2
DROP TABLE fk_partitioned_fk_2;
+CREATE TABLE fk_partitioned_fk_2 (b int, a int,
+ FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
+BEGIN;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have two constraints
+\d fk_partitioned_fk_2
+DROP TABLE fk_partitioned_fk_2;
+ROLLBACK;
+BEGIN;
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have only one constraint
+\d fk_partitioned_fk_2
+ROLLBACK;
+DROP TABLE fk_partitioned_fk_2;
+
CREATE TABLE fk_partitioned_fk_4 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE) PARTITION BY RANGE (b, a);
CREATE TABLE fk_partitioned_fk_4_1 PARTITION OF fk_partitioned_fk_4 FOR VALUES FROM (1,1) TO (100,100);
CREATE TABLE fk_partitioned_fk_4_2 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE SET NULL);
--
2.43.5
On 03.12.24 13:00, Amul Sul wrote:
create table t(a int);
alter table t ADD CONSTRAINT the_constraint CHECK (a > 0) NOT ENFORCED;
insert into t select -1;
select conname, contype,condeferrable,condeferred, convalidated,
conenforced,conkey,connoinherit
from pg_constraint
where conrelid = 't'::regclass;pg_constraint->convalidated should be set to false for NOT ENFORCED constraint?
Am I missing something?The "validated" status is irrelevant when the constraint is set to not
enforced. But it's probably still a good idea to make sure the field is
set consistently. I'm also leaning toward setting it to false. One
advantage of that would be that if you set the constraint to enforced
later, then it's automatically in the correct "not validated" state.
Let's make it so that ruleutils.c doesn't print the NOT VALID when it's
already printing NOT ENFORCED. Otherwise, it gets unnecessarily verbose
and confusing.
typedef struct Constraint
{
NodeTag type;
ConstrType contype; /* see above */
char *conname; /* Constraint name, or NULL if unnamed */
bool deferrable; /* DEFERRABLE? */
bool initdeferred; /* INITIALLY DEFERRED? */
bool skip_validation; /* skip validation of existing rows? */
bool initially_valid; /* mark the new constraint as valid? */
bool is_enforced; /* enforced constraint? */
}
makeNode(Constraint) will default is_enforced to false.
Which makes the default value not what we want.
That means we may need to pay more attention for the trip from
makeNode(Constraint) to finally insert the constraint to the catalog.if we change it to is_not_enforced, makeNode will default to false.
is_not_enforced is false, means the constraint is enforced.
which is not that intuitive...Yes, it could be safer to make the field so that the default is false.
I guess the skip_validation field is like that for a similar reason, but
I'm not sure.Ok. Initially, I was doing it the same way, but to maintain consistency
with the pg_constraint column and avoid negation in multiple places, I
chose that approach. However, I agree that having the default to false
would be safer. I’ve renamed the flag to is_not_enforced. Other names
I considered were not_enforced or is_unenforced, but since we already
have existing flags with two underscores, is_not_enforced shouldn't be
a problem.
I was initially thinking about this as well, but after seeing it now, I
don't think this is a good change. Because now we have both "enforced"
and "not_enforced" sprinkled around the code. If we were to do this
consistently everywhere, then it might make sense, but this way it's
just confusing. The Constraint struct is only initialized in a few
places, so I think we can be careful there. Also note that the field
initially_valid is equally usually true.
I could of other notes on patch 0001:
Update information_schema table_constraint.enforced (see
src/backend/catalog/information_schema.sql and
doc/src/sgml/information_schema.sgml).
The handling of merging check constraints seems incomplete. What should
be the behavior of this:
=> create table p1 (a int check (a > 0) not enforced);
CREATE TABLE
=> create table c1 (a int check (a > 0) enforced) inherits (p1);
CREATE TABLE
Or this?
=> create table p2 (a int check (a > 0) enforced);
CREATE TABLE
=> create table c2 () inherits (p1, p2);
CREATE TABLE
Should we catch these and error?
i just only apply v5-0001 for now.
create table t(a int);
alter table t ADD CONSTRAINT cc CHECK (a > 0);
alter table t alter CONSTRAINT cc NOT ENFORCED;
alter table t alter CONSTRAINT cc ENFORCED;
the last two queries will fail, which means
ALTER CONSTRAINT constraint_name [ DEFERRABLE | NOT DEFERRABLE ] [
INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]
in doc/src/sgml/ref/alter_table.sgml is not correct?
also no code change in ATExecAlterConstraint.
errmsg("cannot validated NOT ENFORCED constraint")));
should be
errmsg("cannot validate NOT ENFORCED constraint")));
?
typedef struct ConstrCheck
{
char *ccname;
char *ccbin; /* nodeToString representation of expr */
bool ccenforced;
bool ccvalid;
bool ccnoinherit; /* this is a non-inheritable constraint */
} ConstrCheck
ConstraintImpliedByRelConstraint,
get_relation_constraints
need skip notenforced check constraint?
put domain related tests from constraints.sql to domain.sql would be better.
errmsg("cannot validated NOT ENFORCED constraint")));
should be
errmsg("cannot validate NOT ENFORCED constraint")));
?
looking at it again.
if (!con->conenforced)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot validated NOT ENFORCED constraint")));
ERRCODE_WRONG_OBJECT_TYPE is not that ok? maybe
ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE
or
ERRCODE_INVALID_TABLE_DEFINITION
if (!con->conenforced)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot validated NOT ENFORCED constraint")));
if (!con->convalidated)
{
....
if (con->contype == CONSTRAINT_FOREIGN)
{
/*
* Queue validation for phase 3 only if constraint is enforced;
* otherwise, adding it to the validation queue won't be very
* effective, as the verification will be skipped.
*/
if (con->conenforced)
......
}
in ATExecValidateConstraint "" if (con->conenforced)""" will always be true?
On Wed, Dec 4, 2024 at 1:40 PM jian he <jian.universality@gmail.com> wrote:
i just only apply v5-0001 for now.
create table t(a int);
alter table t ADD CONSTRAINT cc CHECK (a > 0);
alter table t alter CONSTRAINT cc NOT ENFORCED;
alter table t alter CONSTRAINT cc ENFORCED;the last two queries will fail, which means
ALTER CONSTRAINT constraint_name [ DEFERRABLE | NOT DEFERRABLE ] [
INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]
in doc/src/sgml/ref/alter_table.sgml is not correct?
also no code change in ATExecAlterConstraint.
Your are correct, will move this to 0005 patch.
errmsg("cannot validated NOT ENFORCED constraint")));
should be
errmsg("cannot validate NOT ENFORCED constraint")));
?
Yes, I realized that while working on Peter's last review comments.
typedef struct ConstrCheck
{
char *ccname;
char *ccbin; /* nodeToString representation of expr */
bool ccenforced;
bool ccvalid;
bool ccnoinherit; /* this is a non-inheritable constraint */
} ConstrCheckConstraintImpliedByRelConstraint,
get_relation_constraints
need skip notenforced check constraint?
That gets skipped since ccvalid is false for NOT ENFORCED constraints.
However, for better readability, I've added an assertion with a
comment in my local changes.
put domain related tests from constraints.sql to domain.sql would be better.
Ok.
looking at it again.
if (!con->conenforced)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot validated NOT ENFORCED constraint")));ERRCODE_WRONG_OBJECT_TYPE is not that ok? maybe
ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE
or
ERRCODE_INVALID_TABLE_DEFINITION
I think ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE would be much suitable.
if (!con->conenforced)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot validated NOT ENFORCED constraint")));
if (!con->convalidated)
{
....
if (con->contype == CONSTRAINT_FOREIGN)
{
/*
* Queue validation for phase 3 only if constraint is enforced;
* otherwise, adding it to the validation queue won't be very
* effective, as the verification will be skipped.
*/
if (con->conenforced)
......
}in ATExecValidateConstraint "" if (con->conenforced)""" will always be true?
Yes, the changes from that patch have been reverted in my local code, which
I will post soon.
Thanks again for your review comments; they were very helpful.
Regards,
Amul
On Tue, Dec 3, 2024 at 6:29 PM Peter Eisentraut <peter@eisentraut.org> wrote:
On 03.12.24 13:00, Amul Sul wrote:
[....]Ok. Initially, I was doing it the same way, but to maintain consistency
with the pg_constraint column and avoid negation in multiple places, I
chose that approach. However, I agree that having the default to false
would be safer. I’ve renamed the flag to is_not_enforced. Other names
I considered were not_enforced or is_unenforced, but since we already
have existing flags with two underscores, is_not_enforced shouldn't be
a problem.I was initially thinking about this as well, but after seeing it now, I
don't think this is a good change. Because now we have both "enforced"
and "not_enforced" sprinkled around the code. If we were to do this
consistently everywhere, then it might make sense, but this way it's
just confusing. The Constraint struct is only initialized in a few
places, so I think we can be careful there. Also note that the field
initially_valid is equally usually true.
Ok, reverted and returned to using the is_enforced flag as before.
I could of other notes on patch 0001:
Update information_schema table_constraint.enforced (see
src/backend/catalog/information_schema.sql and
doc/src/sgml/information_schema.sgml).
Done.
The handling of merging check constraints seems incomplete. What should
be the behavior of this:=> create table p1 (a int check (a > 0) not enforced);
CREATE TABLE
=> create table c1 (a int check (a > 0) enforced) inherits (p1);
CREATE TABLEOr this?
=> create table p2 (a int check (a > 0) enforced);
CREATE TABLE
=> create table c2 () inherits (p1, p2);
CREATE TABLEShould we catch these and error?
I don't see any issue with treating ENFORCED and NOT ENFORCED as
distinct constraints if the names are different. However, if the names
are the same but the enforceability differs, I think we should throw
an error for now.
A better implementation I can think of would be to have behavior
similar to the NULL constraint: if the same column in one parent is
NOT NULL and another is not, the inherited child should always have
the NOT NULL column constraint, (in our case it should be ENFORCED),
eg: in the following case, c2 will have the column set as NOT NULL.
create table p1 (a int NOT NULL);
create table p2 (a int);
create table c2 () inherits (p1, p2);
But, I am a bit skeptical about following where it gets NOT NULL as
well but I expected it shouldn't be, is that right behaviour?
create table c3 (a int NULL) inherits (p1, p2);
So, if we want the child constraint enforceability to be overridden by
the child specification, we should handle it accordingly, unlike the
above NULL case. Thoughts?
Attached is the updated version that includes fixes based on Jian He's
review comments. 0005 is still WIP for the same reasons mentioned in
the v5 version. Alvaro also confirmed to me of-list, that the NOT
VALID FK constraint hasn't been implemented, and simply dropping the
restriction (the error check) may not be sufficient for its
implementation. I still need to investigate what else is required,
which is why this version remains WIP.
Regards,
Amul
Attachments:
v6-0001-Add-support-for-NOT-ENFORCED-in-CHECK-constraints.patchapplication/x-patch; name=v6-0001-Add-support-for-NOT-ENFORCED-in-CHECK-constraints.patchDownload
From 2f58331dc93d3d2ca6e7f9da2635261faae5dda1 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Wed, 4 Dec 2024 10:24:40 +0530
Subject: [PATCH v6 1/5] Add support for NOT ENFORCED in CHECK constraints.
This added basic infrastructure for the NOT ENFORCED/ENFORCED
constraint supports where incluing grammar and catalog flags.
Note that CHECK constraints do not currently support ALTER operations,
so changing the enforceability of an existing constraint isn't
possible without dropping and recreating it. We could consider
adding support for altering CHECK constraints either in this
patch series or as a separatly.
---
doc/src/sgml/catalogs.sgml | 10 ++
doc/src/sgml/ddl.sgml | 8 +-
doc/src/sgml/information_schema.sgml | 4 +-
doc/src/sgml/ref/alter_table.sgml | 12 +-
doc/src/sgml/ref/create_table.sgml | 25 ++++-
src/backend/access/common/tupdesc.c | 2 +
src/backend/catalog/heap.c | 26 +++--
src/backend/catalog/index.c | 1 +
src/backend/catalog/information_schema.sql | 2 +-
src/backend/catalog/pg_constraint.c | 7 ++
src/backend/commands/tablecmds.c | 30 ++++-
src/backend/commands/trigger.c | 1 +
src/backend/commands/typecmds.c | 16 +++
src/backend/executor/execMain.c | 8 +-
src/backend/nodes/makefuncs.c | 1 +
src/backend/optimizer/util/plancat.c | 12 ++
src/backend/parser/gram.y | 104 ++++++++++++++----
src/backend/parser/parse_utilcmd.c | 53 ++++++++-
src/backend/utils/adt/ruleutils.c | 6 +-
src/backend/utils/cache/relcache.c | 1 +
src/include/access/tupdesc.h | 1 +
src/include/catalog/catversion.h | 2 +-
src/include/catalog/heap.h | 1 +
src/include/catalog/pg_constraint.h | 2 +
src/include/nodes/parsenodes.h | 3 +
src/include/parser/kwlist.h | 1 +
src/test/regress/expected/alter_table.out | 3 +
src/test/regress/expected/constraints.out | 40 ++++++-
.../regress/expected/create_table_like.out | 10 +-
src/test/regress/expected/domain.out | 15 +++
src/test/regress/expected/inherit.out | 7 ++
src/test/regress/sql/alter_table.sql | 2 +
src/test/regress/sql/constraints.sql | 22 +++-
src/test/regress/sql/create_table_like.sql | 3 +-
src/test/regress/sql/domain.sql | 8 ++
src/test/regress/sql/inherit.sql | 6 +
36 files changed, 396 insertions(+), 59 deletions(-)
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index bf3cee08a93..a56ef4df562 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2602,6 +2602,16 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l
</para></entry>
</row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>conenforced</structfield> <type>bool</type>
+ </para>
+ <para>
+ Is the constraint enforceable?
+ Currently, can be false only for CHECK constraints
+ </para></entry>
+ </row>
+
<row>
<entry role="catalog_table_entry"><para role="column_definition">
<structfield>convalidated</structfield> <type>bool</type>
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index dea04d64db6..9a20396dae4 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -584,7 +584,10 @@ CREATE TABLE products (
consists of the key word <literal>CHECK</literal> followed by an
expression in parentheses. The check constraint expression should
involve the column thus constrained, otherwise the constraint
- would not make too much sense.
+ would not make too much sense. The check constraint expression can be
+ followed by the <literal>ENFORCED</literal>, which is the default, or
+ <literal>NOT ENFORCED</literal> keyword, determining whether the
+ constraint check is performed or skipped after each statement.
</para>
<indexterm>
@@ -720,7 +723,8 @@ CREATE TABLE products (
to implement that. (This approach avoids the dump/restore problem because
<application>pg_dump</application> does not reinstall triggers until after
restoring data, so that the check will not be enforced during a
- dump/restore.)
+ dump/restore.) You can add that CHECK constraint with <literal>NOT ENFORCED</literal>,
+ allowing the underlying constraint rule to be represented without being enforced.
</para>
</note>
diff --git a/doc/src/sgml/information_schema.sgml b/doc/src/sgml/information_schema.sgml
index 9442b0718c0..19dffe7be6a 100644
--- a/doc/src/sgml/information_schema.sgml
+++ b/doc/src/sgml/information_schema.sgml
@@ -6896,9 +6896,7 @@ ORDER BY c.ordinal_position;
<structfield>enforced</structfield> <type>yes_or_no</type>
</para>
<para>
- Applies to a feature not available in
- <productname>PostgreSQL</productname> (currently always
- <literal>YES</literal>)
+ <literal>YES</literal> if the constraint is enforced, <literal>NO</literal> if not
</para></entry>
</row>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index c8f7ab7d956..938450fba18 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -108,7 +108,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
PRIMARY KEY <replaceable class="parameter">index_parameters</replaceable> |
REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> ) ] [ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ]
[ ON DELETE <replaceable class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ] }
-[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]
<phrase>and <replaceable class="parameter">table_constraint</replaceable> is:</phrase>
@@ -120,7 +120,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
EXCLUDE [ USING <replaceable class="parameter">index_method</replaceable> ] ( <replaceable class="parameter">exclude_element</replaceable> WITH <replaceable class="parameter">operator</replaceable> [, ... ] ) <replaceable class="parameter">index_parameters</replaceable> [ WHERE ( <replaceable class="parameter">predicate</replaceable> ) ] |
FOREIGN KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">column_name</replaceable> ] ) REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">refcolumn</replaceable> ] ) ]
[ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ] [ ON DELETE <replaceable class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ] }
-[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]
<phrase>and <replaceable class="parameter">table_constraint_using_index</replaceable> is:</phrase>
@@ -1423,9 +1423,11 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</para>
<para>
- Adding a <literal>CHECK</literal> or <literal>NOT NULL</literal> constraint requires
- scanning the table to verify that existing rows meet the constraint,
- but does not require a table rewrite.
+ Adding an enforced <literal>CHECK</literal> or <literal>NOT NULL</literal>
+ constraint requires scanning the table to verify that existing rows meet the
+ constraint, but does not require a table rewrite. If a <literal>CHECK</literal>
+ constraint is added as <literal>NOT ENFORCED</literal>, the validation will
+ not be performed.
</para>
<para>
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index 70fa929caa4..cfd01c4ef2b 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -71,7 +71,7 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI
PRIMARY KEY <replaceable class="parameter">index_parameters</replaceable> |
REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> ) ] [ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ]
[ ON DELETE <replaceable class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ] }
-[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]
<phrase>and <replaceable class="parameter">table_constraint</replaceable> is:</phrase>
@@ -84,7 +84,7 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI
FOREIGN KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">column_name</replaceable> ] ) REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">refcolumn</replaceable> ] ) ]
[ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ] [ ON DELETE <replaceable
class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ] }
-[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]
<phrase>and <replaceable class="parameter">like_option</replaceable> is:</phrase>
@@ -1377,6 +1377,27 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</listitem>
</varlistentry>
+ <varlistentry id="sql-createtable-parms-enforce">
+ <term><literal>ENFORCED</literal></term>
+ <term><literal>NOT ENFORCED</literal></term>
+ <listitem>
+ <para>
+ This is currently only allowed for <literal>CHECK</literal> constraints.
+ If the constraint is <literal>NOT ENFORCED</literal>, this clause
+ specifies that the constraint check will be skipped. When the constraint
+ is <literal>ENFORCED</literal>, check is performed after each statement;
+ the latter is the default.
+ </para>
+ <para>
+ Note that if the constraint is <literal>NOT ENFORCED</literal>, it is also
+ marked as <literal>NOT VALID</literal>. While this might seem unnecessary,
+ it is done for consistency and should be set accordingly. This ensures
+ that if the constraint is later changed to <literal>ENFORCED</literal>, it
+ will automatically be in the correct <literal>NOT VALID</literal> state.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="sql-createtable-method">
<term><literal>USING <replaceable class="parameter">method</replaceable></literal></term>
<listitem>
diff --git a/src/backend/access/common/tupdesc.c b/src/backend/access/common/tupdesc.c
index 47379fef220..6c64e5b9e71 100644
--- a/src/backend/access/common/tupdesc.c
+++ b/src/backend/access/common/tupdesc.c
@@ -224,6 +224,7 @@ CreateTupleDescCopyConstr(TupleDesc tupdesc)
{
cpy->check[i].ccname = pstrdup(constr->check[i].ccname);
cpy->check[i].ccbin = pstrdup(constr->check[i].ccbin);
+ cpy->check[i].ccenforced = constr->check[i].ccenforced;
cpy->check[i].ccvalid = constr->check[i].ccvalid;
cpy->check[i].ccnoinherit = constr->check[i].ccnoinherit;
}
@@ -547,6 +548,7 @@ equalTupleDescs(TupleDesc tupdesc1, TupleDesc tupdesc2)
if (!(strcmp(check1->ccname, check2->ccname) == 0 &&
strcmp(check1->ccbin, check2->ccbin) == 0 &&
+ check1->ccenforced == check2->ccenforced &&
check1->ccvalid == check2->ccvalid &&
check1->ccnoinherit == check2->ccnoinherit))
return false;
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index d7b88b61dcc..ad263507cea 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -101,9 +101,9 @@ static ObjectAddress AddNewRelationType(const char *typeName,
Oid new_row_type,
Oid new_array_type);
static void RelationRemoveInheritance(Oid relid);
-static Oid StoreRelCheck(Relation rel, const char *ccname, Node *expr,
- bool is_validated, bool is_local, int16 inhcount,
- bool is_no_inherit, bool is_internal);
+static Oid StoreRelCheck(Relation rel, const char *ccname, Node *expr,
+ bool is_enforced, bool is_validated, bool is_local,
+ int16 inhcount, bool is_no_inherit, bool is_internal);
static void StoreConstraints(Relation rel, List *cooked_constraints,
bool is_internal);
static bool MergeWithExistingConstraint(Relation rel, const char *ccname, Node *expr,
@@ -2074,8 +2074,8 @@ SetAttrMissing(Oid relid, char *attname, char *value)
*/
static Oid
StoreRelCheck(Relation rel, const char *ccname, Node *expr,
- bool is_validated, bool is_local, int16 inhcount,
- bool is_no_inherit, bool is_internal)
+ bool is_enforced, bool is_validated, bool is_local,
+ int16 inhcount, bool is_no_inherit, bool is_internal)
{
char *ccbin;
List *varList;
@@ -2140,6 +2140,7 @@ StoreRelCheck(Relation rel, const char *ccname, Node *expr,
CONSTRAINT_CHECK, /* Constraint Type */
false, /* Is Deferrable */
false, /* Is Deferred */
+ is_enforced, /* Is Enforced */
is_validated,
InvalidOid, /* no parent constraint */
RelationGetRelid(rel), /* relation */
@@ -2193,6 +2194,7 @@ StoreRelNotNull(Relation rel, const char *nnname, AttrNumber attnum,
CONSTRAINT_NOTNULL,
false,
false,
+ true, /* Is Enforced */
is_validated,
InvalidOid,
RelationGetRelid(rel),
@@ -2262,9 +2264,9 @@ StoreConstraints(Relation rel, List *cooked_constraints, bool is_internal)
case CONSTR_CHECK:
con->conoid =
StoreRelCheck(rel, con->name, con->expr,
- !con->skip_validation, con->is_local,
- con->inhcount, con->is_no_inherit,
- is_internal);
+ con->is_enforced, !con->skip_validation,
+ con->is_local, con->inhcount,
+ con->is_no_inherit, is_internal);
numchecks++;
break;
@@ -2405,6 +2407,7 @@ AddRelationNewConstraints(Relation rel,
cooked->name = NULL;
cooked->attnum = colDef->attnum;
cooked->expr = expr;
+ cooked->is_enforced = true;
cooked->skip_validation = false;
cooked->is_local = is_local;
cooked->inhcount = is_local ? 0 : 1;
@@ -2524,8 +2527,10 @@ AddRelationNewConstraints(Relation rel,
* OK, store it.
*/
constrOid =
- StoreRelCheck(rel, ccname, expr, cdef->initially_valid, is_local,
- is_local ? 0 : 1, cdef->is_no_inherit, is_internal);
+ StoreRelCheck(rel, ccname, expr, cdef->is_enforced,
+ cdef->initially_valid, is_local,
+ is_local ? 0 : 1, cdef->is_no_inherit,
+ is_internal);
numchecks++;
@@ -2535,6 +2540,7 @@ AddRelationNewConstraints(Relation rel,
cooked->name = ccname;
cooked->attnum = 0;
cooked->expr = expr;
+ cooked->is_enforced = cdef->is_enforced;
cooked->skip_validation = cdef->skip_validation;
cooked->is_local = is_local;
cooked->inhcount = is_local ? 0 : 1;
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 1c3a9e06d37..f4cbaf8953a 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -1957,6 +1957,7 @@ index_constraint_create(Relation heapRelation,
constraintType,
deferrable,
initdeferred,
+ true, /* Is Enforced */
true,
parentConstraintId,
RelationGetRelid(heapRelation),
diff --git a/src/backend/catalog/information_schema.sql b/src/backend/catalog/information_schema.sql
index 76c78c0d184..bd241130381 100644
--- a/src/backend/catalog/information_schema.sql
+++ b/src/backend/catalog/information_schema.sql
@@ -1844,7 +1844,7 @@ CREATE VIEW table_constraints AS
AS is_deferrable,
CAST(CASE WHEN c.condeferred THEN 'YES' ELSE 'NO' END AS yes_or_no)
AS initially_deferred,
- CAST('YES' AS yes_or_no) AS enforced,
+ CAST(CASE WHEN c.conenforced THEN 'YES' ELSE 'NO' END AS yes_or_no) AS enforced,
CAST(CASE WHEN c.contype = 'u'
THEN CASE WHEN (SELECT NOT indnullsnotdistinct FROM pg_index WHERE indexrelid = conindid) THEN 'YES' ELSE 'NO' END
END
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index 9c05a98d28c..f9e4c3d3cfa 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -53,6 +53,7 @@ CreateConstraintEntry(const char *constraintName,
char constraintType,
bool isDeferrable,
bool isDeferred,
+ bool isEnforced,
bool isValidated,
Oid parentConstrId,
Oid relId,
@@ -99,6 +100,11 @@ CreateConstraintEntry(const char *constraintName,
ObjectAddresses *addrs_auto;
ObjectAddresses *addrs_normal;
+ /* Only CHECK constraint can be not enforced */
+ Assert(isEnforced || constraintType == CONSTRAINT_CHECK);
+ /* NOT ENFORCED constraint must be NOT VALID */
+ Assert(isEnforced || !isValidated);
+
conDesc = table_open(ConstraintRelationId, RowExclusiveLock);
Assert(constraintName);
@@ -182,6 +188,7 @@ CreateConstraintEntry(const char *constraintName,
values[Anum_pg_constraint_contype - 1] = CharGetDatum(constraintType);
values[Anum_pg_constraint_condeferrable - 1] = BoolGetDatum(isDeferrable);
values[Anum_pg_constraint_condeferred - 1] = BoolGetDatum(isDeferred);
+ values[Anum_pg_constraint_conenforced - 1] = BoolGetDatum(isEnforced);
values[Anum_pg_constraint_convalidated - 1] = BoolGetDatum(isValidated);
values[Anum_pg_constraint_conrelid - 1] = ObjectIdGetDatum(relId);
values[Anum_pg_constraint_contypid - 1] = ObjectIdGetDatum(domainId);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 6ccae4cb4a8..24df1aec6b7 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -373,7 +373,7 @@ static void RangeVarCallbackForTruncate(const RangeVar *relation,
static List *MergeAttributes(List *columns, const List *supers, char relpersistence,
bool is_partition, List **supconstr,
List **supnotnulls);
-static List *MergeCheckConstraint(List *constraints, const char *name, Node *expr);
+static List *MergeCheckConstraint(List *constraints, const char *name, Node *expr, bool is_enforced);
static void MergeChildAttribute(List *inh_columns, int exist_attno, int newcol_attno, const ColumnDef *newdef);
static ColumnDef *MergeInheritedAttribute(List *inh_columns, int exist_attno, const ColumnDef *newdef);
static void MergeAttributesIntoExisting(Relation child_rel, Relation parent_rel, bool ispartition);
@@ -2885,7 +2885,8 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
name,
RelationGetRelationName(relation))));
- constraints = MergeCheckConstraint(constraints, name, expr);
+ constraints = MergeCheckConstraint(constraints, name, expr,
+ check[i].ccenforced);
}
}
@@ -3099,7 +3100,7 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
* the list.
*/
static List *
-MergeCheckConstraint(List *constraints, const char *name, Node *expr)
+MergeCheckConstraint(List *constraints, const char *name, Node *expr, bool is_enforced)
{
ListCell *lc;
CookedConstraint *newcon;
@@ -3122,6 +3123,13 @@ MergeCheckConstraint(List *constraints, const char *name, Node *expr)
ereport(ERROR,
errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
errmsg("too many inheritance parents"));
+
+ if (ccon->is_enforced != is_enforced)
+ ereport(ERROR,
+ (errcode(ERRCODE_DUPLICATE_OBJECT),
+ errmsg("check constraint name \"%s\" appears multiple times but with different enforceability",
+ name)));
+
return constraints;
}
@@ -3140,6 +3148,8 @@ MergeCheckConstraint(List *constraints, const char *name, Node *expr)
newcon->name = pstrdup(name);
newcon->expr = expr;
newcon->inhcount = 1;
+ newcon->is_enforced = is_enforced;
+ newcon->skip_validation = !is_enforced;
return lappend(constraints, newcon);
}
@@ -10418,6 +10428,7 @@ addFkConstraint(addFkConstraintSides fkside,
CONSTRAINT_FOREIGN,
fkconstraint->deferrable,
fkconstraint->initdeferred,
+ true, /* Is Enforced */
fkconstraint->initially_valid,
parentConstr,
RelationGetRelid(rel),
@@ -12004,6 +12015,11 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
errmsg("constraint \"%s\" of relation \"%s\" is not a foreign key or check constraint",
constrName, RelationGetRelationName(rel))));
+ if (!con->conenforced)
+ ereport(ERROR,
+ (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("cannot validate NOT ENFORCED constraint")));
+
if (!con->convalidated)
{
AlteredTableInfo *tab;
@@ -16258,6 +16274,7 @@ constraints_equivalent(HeapTuple a, HeapTuple b, TupleDesc tupleDesc)
if (acon->condeferrable != bcon->condeferrable ||
acon->condeferred != bcon->condeferred ||
+ acon->conenforced != bcon->conenforced ||
strcmp(decompile_conbin(a, tupleDesc),
decompile_conbin(b, tupleDesc)) != 0)
return false;
@@ -18875,6 +18892,12 @@ ConstraintImpliedByRelConstraint(Relation scanrel, List *testConstraint, List *p
if (!constr->check[i].ccvalid)
continue;
+ /*
+ * NOT ENFORCED constraints are always marked as invalid, which
+ * should have been ignored.
+ */
+ Assert(constr->check[i].ccenforced);
+
cexpr = stringToNode(constr->check[i].ccbin);
/*
@@ -20185,6 +20208,7 @@ DetachAddConstraintIfNeeded(List **wqueue, Relation partRel)
n->is_no_inherit = false;
n->raw_expr = NULL;
n->cooked_expr = nodeToString(make_ands_explicit(constraintExpr));
+ n->is_enforced = true;
n->initially_valid = true;
n->skip_validation = true;
/* It's a re-add, since it nominally already exists */
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 09356e46d16..49d3fc22983 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -809,6 +809,7 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
CONSTRAINT_TRIGGER,
stmt->deferrable,
stmt->initdeferred,
+ true, /* Is Enforced */
true,
InvalidOid, /* no parent */
RelationGetRelid(rel),
diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c
index da591c0922b..59f3cd9a5a3 100644
--- a/src/backend/commands/typecmds.c
+++ b/src/backend/commands/typecmds.c
@@ -1018,6 +1018,13 @@ DefineDomain(CreateDomainStmt *stmt)
errmsg("specifying GENERATED not supported for domains")));
break;
+ case CONSTR_ATTR_ENFORCED:
+ case CONSTR_ATTR_NOT_ENFORCED:
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("specifying constraint enforceability not supported for domains")));
+ break;
+
/* no default, to let compiler warn about missing case */
}
}
@@ -2975,6 +2982,13 @@ AlterDomainAddConstraint(List *names, Node *newConstraint,
errmsg("specifying constraint deferrability not supported for domains")));
break;
+ case CONSTR_ATTR_ENFORCED:
+ case CONSTR_ATTR_NOT_ENFORCED:
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("specifying constraint enforceability not supported for domains")));
+ break;
+
default:
elog(ERROR, "unrecognized constraint subtype: %d",
(int) constr->contype);
@@ -3604,6 +3618,7 @@ domainAddCheckConstraint(Oid domainOid, Oid domainNamespace, Oid baseTypeOid,
CONSTRAINT_CHECK, /* Constraint Type */
false, /* Is Deferrable */
false, /* Is Deferred */
+ true, /* Is Enforced */
!constr->skip_validation, /* Is Validated */
InvalidOid, /* no parent constraint */
InvalidOid, /* not a relation constraint */
@@ -3711,6 +3726,7 @@ domainAddNotNullConstraint(Oid domainOid, Oid domainNamespace, Oid baseTypeOid,
CONSTRAINT_NOTNULL, /* Constraint Type */
false, /* Is Deferrable */
false, /* Is Deferred */
+ true, /* Is Enforced */
!constr->skip_validation, /* Is Validated */
InvalidOid, /* no parent constraint */
InvalidOid, /* not a relation constraint */
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 5ca856fd279..1878f4b8d94 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1751,11 +1751,15 @@ ExecRelCheck(ResultRelInfo *resultRelInfo,
{
oldContext = MemoryContextSwitchTo(estate->es_query_cxt);
resultRelInfo->ri_ConstraintExprs =
- (ExprState **) palloc(ncheck * sizeof(ExprState *));
+ (ExprState **) palloc0(ncheck * sizeof(ExprState *));
for (i = 0; i < ncheck; i++)
{
Expr *checkconstr;
+ /* Skip not enforced constraint */
+ if (!check[i].ccenforced)
+ continue;
+
checkconstr = stringToNode(check[i].ccbin);
resultRelInfo->ri_ConstraintExprs[i] =
ExecPrepareExpr(checkconstr, estate);
@@ -1782,7 +1786,7 @@ ExecRelCheck(ResultRelInfo *resultRelInfo,
* is not to be treated as a failure. Therefore, use ExecCheck not
* ExecQual.
*/
- if (!ExecCheck(checkconstr, econtext))
+ if (checkconstr && !ExecCheck(checkconstr, econtext))
return check[i].ccname;
}
diff --git a/src/backend/nodes/makefuncs.c b/src/backend/nodes/makefuncs.c
index 7e5df7bea4d..72e6a0e89be 100644
--- a/src/backend/nodes/makefuncs.c
+++ b/src/backend/nodes/makefuncs.c
@@ -455,6 +455,7 @@ makeNotNullConstraint(String *colname)
notnull->keys = list_make1(colname);
notnull->skip_validation = false;
notnull->initially_valid = true;
+ notnull->is_enforced = true;
return notnull;
}
diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c
index 37b0ca2e439..155453e1ba0 100644
--- a/src/backend/optimizer/util/plancat.c
+++ b/src/backend/optimizer/util/plancat.c
@@ -1303,9 +1303,21 @@ get_relation_constraints(PlannerInfo *root,
*/
if (!constr->check[i].ccvalid)
continue;
+
+ /*
+ * NOT ENFORCED constraints are always marked as invalid, which
+ * should have been ignored.
+ */
+ Assert(constr->check[i].ccenforced);
+
+ /*
+ * Also ignore if NO INHERIT and we weren't told
+ * that that's safe.
+ */
if (constr->check[i].ccnoinherit && !include_noinherit)
continue;
+
cexpr = stringToNode(constr->check[i].ccbin);
/*
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 67eb96396af..833b3be02be 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -143,6 +143,8 @@ typedef struct KeyActions
#define CAS_INITIALLY_DEFERRED 0x08
#define CAS_NOT_VALID 0x10
#define CAS_NO_INHERIT 0x20
+#define CAS_NOT_ENFORCED 0x40
+#define CAS_ENFORCED 0x80
#define parser_yyerror(msg) scanner_yyerror(msg, yyscanner)
@@ -196,8 +198,8 @@ static void SplitColQualList(List *qualList,
List **constraintList, CollateClause **collClause,
core_yyscan_t yyscanner);
static void processCASbits(int cas_bits, int location, const char *constrType,
- bool *deferrable, bool *initdeferred, bool *not_valid,
- bool *no_inherit, core_yyscan_t yyscanner);
+ bool *deferrable, bool *initdeferred, bool *is_enforced,
+ bool *not_valid, bool *no_inherit, core_yyscan_t yyscanner);
static PartitionStrategy parsePartitionStrategy(char *strategy, int location,
core_yyscan_t yyscanner);
static void preprocess_pubobj_list(List *pubobjspec_list,
@@ -711,9 +713,9 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
DETACH DICTIONARY DISABLE_P DISCARD DISTINCT DO DOCUMENT_P DOMAIN_P
DOUBLE_P DROP
- EACH ELSE EMPTY_P ENABLE_P ENCODING ENCRYPTED END_P ENUM_P ERROR_P ESCAPE
- EVENT EXCEPT EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION
- EXTENSION EXTERNAL EXTRACT
+ EACH ELSE EMPTY_P ENABLE_P ENCODING ENCRYPTED END_P ENFORCED ENUM_P ERROR_P
+ ESCAPE EVENT EXCEPT EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN
+ EXPRESSION EXTENSION EXTERNAL EXTRACT
FALSE_P FAMILY FETCH FILTER FINALIZE FIRST_P FLOAT_P FOLLOWING FOR
FORCE FOREIGN FORMAT FORWARD FREEZE FROM FULL FUNCTION FUNCTIONS
@@ -2658,7 +2660,7 @@ alter_table_cmd:
processCASbits($4, @4, "ALTER CONSTRAINT statement",
&c->deferrable,
&c->initdeferred,
- NULL, NULL, yyscanner);
+ NULL, NULL, NULL, yyscanner);
$$ = (Node *) n;
}
/* ALTER TABLE <name> VALIDATE CONSTRAINT ... */
@@ -3961,6 +3963,7 @@ ColConstraintElem:
n->is_no_inherit = $5;
n->raw_expr = $3;
n->cooked_expr = NULL;
+ n->is_enforced = true;
n->skip_validation = false;
n->initially_valid = true;
$$ = (Node *) n;
@@ -4087,6 +4090,22 @@ ConstraintAttr:
n->location = @1;
$$ = (Node *) n;
}
+ | ENFORCED
+ {
+ Constraint *n = makeNode(Constraint);
+
+ n->contype = CONSTR_ATTR_ENFORCED;
+ n->location = @1;
+ $$ = (Node *) n;
+ }
+ | NOT ENFORCED
+ {
+ Constraint *n = makeNode(Constraint);
+
+ n->contype = CONSTR_ATTR_NOT_ENFORCED;
+ n->location = @1;
+ $$ = (Node *) n;
+ }
;
@@ -4148,7 +4167,8 @@ ConstraintElem:
n->raw_expr = $3;
n->cooked_expr = NULL;
processCASbits($5, @5, "CHECK",
- NULL, NULL, &n->skip_validation,
+ NULL, NULL, &n->is_enforced,
+ &n->skip_validation,
&n->is_no_inherit, yyscanner);
n->initially_valid = !n->skip_validation;
$$ = (Node *) n;
@@ -4162,7 +4182,7 @@ ConstraintElem:
n->keys = list_make1(makeString($3));
/* no NOT VALID support yet */
processCASbits($4, @4, "NOT NULL",
- NULL, NULL, NULL,
+ NULL, NULL, NULL, NULL,
&n->is_no_inherit, yyscanner);
n->initially_valid = true;
$$ = (Node *) n;
@@ -4183,7 +4203,7 @@ ConstraintElem:
n->indexspace = $9;
processCASbits($10, @10, "UNIQUE",
&n->deferrable, &n->initdeferred, NULL,
- NULL, yyscanner);
+ NULL, NULL, yyscanner);
$$ = (Node *) n;
}
| UNIQUE ExistingIndex ConstraintAttributeSpec
@@ -4199,7 +4219,7 @@ ConstraintElem:
n->indexspace = NULL;
processCASbits($3, @3, "UNIQUE",
&n->deferrable, &n->initdeferred, NULL,
- NULL, yyscanner);
+ NULL, NULL, yyscanner);
$$ = (Node *) n;
}
| PRIMARY KEY '(' columnList opt_without_overlaps ')' opt_c_include opt_definition OptConsTableSpace
@@ -4217,7 +4237,7 @@ ConstraintElem:
n->indexspace = $9;
processCASbits($10, @10, "PRIMARY KEY",
&n->deferrable, &n->initdeferred, NULL,
- NULL, yyscanner);
+ NULL, NULL, yyscanner);
$$ = (Node *) n;
}
| PRIMARY KEY ExistingIndex ConstraintAttributeSpec
@@ -4233,7 +4253,7 @@ ConstraintElem:
n->indexspace = NULL;
processCASbits($4, @4, "PRIMARY KEY",
&n->deferrable, &n->initdeferred, NULL,
- NULL, yyscanner);
+ NULL, NULL, yyscanner);
$$ = (Node *) n;
}
| EXCLUDE access_method_clause '(' ExclusionConstraintList ')'
@@ -4253,7 +4273,7 @@ ConstraintElem:
n->where_clause = $9;
processCASbits($10, @10, "EXCLUDE",
&n->deferrable, &n->initdeferred, NULL,
- NULL, yyscanner);
+ NULL, NULL, yyscanner);
$$ = (Node *) n;
}
| FOREIGN KEY '(' columnList optionalPeriodName ')' REFERENCES qualified_name
@@ -4282,7 +4302,8 @@ ConstraintElem:
n->fk_del_set_cols = ($11)->deleteAction->cols;
processCASbits($12, @12, "FOREIGN KEY",
&n->deferrable, &n->initdeferred,
- &n->skip_validation, NULL,
+ NULL, &n->skip_validation,
+ NULL,
yyscanner);
n->initially_valid = !n->skip_validation;
$$ = (Node *) n;
@@ -4322,8 +4343,9 @@ DomainConstraintElem:
n->raw_expr = $3;
n->cooked_expr = NULL;
processCASbits($5, @5, "CHECK",
- NULL, NULL, &n->skip_validation,
+ NULL, NULL, NULL, &n->skip_validation,
&n->is_no_inherit, yyscanner);
+ n->is_enforced = true;
n->initially_valid = !n->skip_validation;
$$ = (Node *) n;
}
@@ -4337,7 +4359,7 @@ DomainConstraintElem:
/* no NOT VALID, NO INHERIT support */
processCASbits($3, @3, "NOT NULL",
NULL, NULL, NULL,
- NULL, yyscanner);
+ NULL, NULL, yyscanner);
n->initially_valid = true;
$$ = (Node *) n;
}
@@ -6000,7 +6022,7 @@ CreateTrigStmt:
n->transitionRels = NIL;
processCASbits($11, @11, "TRIGGER",
&n->deferrable, &n->initdeferred, NULL,
- NULL, yyscanner);
+ NULL, NULL, yyscanner);
n->constrrel = $10;
$$ = (Node *) n;
}
@@ -6169,7 +6191,8 @@ ConstraintAttributeSpec:
parser_errposition(@2)));
/* generic message for other conflicts */
if ((newspec & (CAS_NOT_DEFERRABLE | CAS_DEFERRABLE)) == (CAS_NOT_DEFERRABLE | CAS_DEFERRABLE) ||
- (newspec & (CAS_INITIALLY_IMMEDIATE | CAS_INITIALLY_DEFERRED)) == (CAS_INITIALLY_IMMEDIATE | CAS_INITIALLY_DEFERRED))
+ (newspec & (CAS_INITIALLY_IMMEDIATE | CAS_INITIALLY_DEFERRED)) == (CAS_INITIALLY_IMMEDIATE | CAS_INITIALLY_DEFERRED) ||
+ (newspec & (CAS_NOT_ENFORCED | CAS_ENFORCED)) == (CAS_NOT_ENFORCED | CAS_ENFORCED))
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("conflicting constraint properties"),
@@ -6185,6 +6208,8 @@ ConstraintAttributeElem:
| INITIALLY DEFERRED { $$ = CAS_INITIALLY_DEFERRED; }
| NOT VALID { $$ = CAS_NOT_VALID; }
| NO INHERIT { $$ = CAS_NO_INHERIT; }
+ | NOT ENFORCED { $$ = CAS_NOT_ENFORCED; }
+ | ENFORCED { $$ = CAS_ENFORCED; }
;
@@ -17688,6 +17713,7 @@ unreserved_keyword:
| ENABLE_P
| ENCODING
| ENCRYPTED
+ | ENFORCED
| ENUM_P
| ERROR_P
| ESCAPE
@@ -18265,6 +18291,7 @@ bare_label_keyword:
| ENCODING
| ENCRYPTED
| END_P
+ | ENFORCED
| ENUM_P
| ERROR_P
| ESCAPE
@@ -19403,8 +19430,8 @@ SplitColQualList(List *qualList,
*/
static void
processCASbits(int cas_bits, int location, const char *constrType,
- bool *deferrable, bool *initdeferred, bool *not_valid,
- bool *no_inherit, core_yyscan_t yyscanner)
+ bool *deferrable, bool *initdeferred, bool *is_enforced,
+ bool *not_valid, bool *no_inherit, core_yyscan_t yyscanner)
{
/* defaults */
if (deferrable)
@@ -19413,6 +19440,8 @@ processCASbits(int cas_bits, int location, const char *constrType,
*initdeferred = false;
if (not_valid)
*not_valid = false;
+ if (is_enforced)
+ *is_enforced = true;
if (cas_bits & (CAS_DEFERRABLE | CAS_INITIALLY_DEFERRED))
{
@@ -19465,6 +19494,41 @@ processCASbits(int cas_bits, int location, const char *constrType,
constrType),
parser_errposition(location)));
}
+
+ if (cas_bits & CAS_NOT_ENFORCED)
+ {
+ if (is_enforced)
+ *is_enforced = false;
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ /* translator: %s is CHECK, UNIQUE, or similar */
+ errmsg("%s constraints cannot be marked NOT ENFORCED",
+ constrType),
+ parser_errposition(location)));
+
+ /*
+ * NB: The validated status is irrelevant when the constraint is set to
+ * NOT ENFORCED, but for consistency, it should be set accordingly.
+ * This ensures that if the constraint is later changed to ENFORCED, it
+ * will automatically be in the correct NOT VALIDATED state.
+ */
+ if (not_valid)
+ *not_valid = true;
+ }
+
+ if (cas_bits & CAS_ENFORCED)
+ {
+ if (is_enforced)
+ *is_enforced = true;
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ /* translator: %s is CHECK, UNIQUE, or similar */
+ errmsg("%s constraints cannot be marked ENFORCED",
+ constrType),
+ parser_errposition(location)));
+ }
}
/*
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 0f324ee4e31..5ab44149e59 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -954,6 +954,8 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column)
case CONSTR_ATTR_NOT_DEFERRABLE:
case CONSTR_ATTR_DEFERRED:
case CONSTR_ATTR_IMMEDIATE:
+ case CONSTR_ATTR_ENFORCED:
+ case CONSTR_ATTR_NOT_ENFORCED:
/* transformConstraintAttrs took care of these */
break;
@@ -1093,6 +1095,8 @@ transformTableConstraint(CreateStmtContext *cxt, Constraint *constraint)
case CONSTR_ATTR_NOT_DEFERRABLE:
case CONSTR_ATTR_DEFERRED:
case CONSTR_ATTR_IMMEDIATE:
+ case CONSTR_ATTR_ENFORCED:
+ case CONSTR_ATTR_NOT_ENFORCED:
elog(ERROR, "invalid context for constraint type %d",
constraint->contype);
break;
@@ -1433,6 +1437,8 @@ expandTableLikeClause(RangeVar *heapRel, TableLikeClause *table_like_clause)
{
char *ccname = constr->check[ccnum].ccname;
char *ccbin = constr->check[ccnum].ccbin;
+ bool ccenforced = constr->check[ccnum].ccenforced;
+ bool ccvalid = constr->check[ccnum].ccvalid;
bool ccnoinherit = constr->check[ccnum].ccnoinherit;
Node *ccbin_node;
bool found_whole_row;
@@ -1462,13 +1468,14 @@ expandTableLikeClause(RangeVar *heapRel, TableLikeClause *table_like_clause)
n->contype = CONSTR_CHECK;
n->conname = pstrdup(ccname);
n->location = -1;
+ n->is_enforced = ccenforced;
+ n->initially_valid = ccvalid;
n->is_no_inherit = ccnoinherit;
n->raw_expr = NULL;
n->cooked_expr = nodeToString(ccbin_node);
/* We can skip validation, since the new table should be empty. */
n->skip_validation = true;
- n->initially_valid = true;
atsubcmd = makeNode(AlterTableCmd);
atsubcmd->subtype = AT_AddConstraint;
@@ -2921,9 +2928,11 @@ transformCheckConstraints(CreateStmtContext *cxt, bool skipValidation)
return;
/*
- * If creating a new table (but not a foreign table), we can safely skip
- * validation of check constraints, and nonetheless mark them valid. (This
- * will override any user-supplied NOT VALID flag.)
+ * When creating a new table (but not a foreign table), we can safely skip
+ * the validation of check constraints and mark them as valid based on the
+ * constraint enforcement flag, since NOT ENFORCED constraints must always
+ * be marked as NOT VALID. (This will override any user-supplied
+ * NOT VALID flag.)
*/
if (skipValidation)
{
@@ -2932,7 +2941,7 @@ transformCheckConstraints(CreateStmtContext *cxt, bool skipValidation)
Constraint *constraint = (Constraint *) lfirst(ckclist);
constraint->skip_validation = true;
- constraint->initially_valid = true;
+ constraint->initially_valid = constraint->is_enforced;
}
}
}
@@ -3859,6 +3868,7 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
Constraint *lastprimarycon = NULL;
bool saw_deferrability = false;
bool saw_initially = false;
+ bool saw_enforced = false;
ListCell *clist;
#define SUPPORTS_ATTRS(node) \
@@ -3954,12 +3964,45 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
lastprimarycon->initdeferred = false;
break;
+ case CONSTR_ATTR_ENFORCED:
+ if (lastprimarycon &&
+ lastprimarycon->contype != CONSTR_CHECK)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("misplaced ENFORCED clause"),
+ parser_errposition(cxt->pstate, con->location)));
+ if (saw_enforced)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("multiple ENFORCED/NOT ENFORCED clauses not allowed"),
+ parser_errposition(cxt->pstate, con->location)));
+ saw_enforced = true;
+ lastprimarycon->is_enforced = true;
+ break;
+
+ case CONSTR_ATTR_NOT_ENFORCED:
+ if (lastprimarycon &&
+ lastprimarycon->contype != CONSTR_CHECK)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("misplaced NOT ENFORCED clause"),
+ parser_errposition(cxt->pstate, con->location)));
+ if (saw_enforced)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("multiple ENFORCED/NOT ENFORCED clauses not allowed"),
+ parser_errposition(cxt->pstate, con->location)));
+ saw_enforced = true;
+ lastprimarycon->is_enforced = false;
+ break;
+
default:
/* Otherwise it's not an attribute */
lastprimarycon = con;
/* reset flags for new primary node */
saw_deferrability = false;
saw_initially = false;
+ saw_enforced = false;
break;
}
}
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 2194ab3dfa5..7332f37c736 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -2591,7 +2591,11 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
appendStringInfoString(&buf, " DEFERRABLE");
if (conForm->condeferred)
appendStringInfoString(&buf, " INITIALLY DEFERRED");
- if (!conForm->convalidated)
+
+ /* Validated status is irrelevant when the constraint is NOT ENFORCED. */
+ if (!conForm->conenforced)
+ appendStringInfoString(&buf, " NOT ENFORCED");
+ else if (!conForm->convalidated)
appendStringInfoString(&buf, " NOT VALID");
/* Cleanup */
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 422509f18d7..25db64dc440 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4585,6 +4585,7 @@ CheckConstraintFetch(Relation relation)
break;
}
+ check[found].ccenforced = conform->conenforced;
check[found].ccvalid = conform->convalidated;
check[found].ccnoinherit = conform->connoinherit;
check[found].ccname = MemoryContextStrdup(CacheMemoryContext,
diff --git a/src/include/access/tupdesc.h b/src/include/access/tupdesc.h
index 8930a28d660..908f8e7a57c 100644
--- a/src/include/access/tupdesc.h
+++ b/src/include/access/tupdesc.h
@@ -29,6 +29,7 @@ typedef struct ConstrCheck
{
char *ccname;
char *ccbin; /* nodeToString representation of expr */
+ bool ccenforced;
bool ccvalid;
bool ccnoinherit; /* this is a non-inheritable constraint */
} ConstrCheck;
diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h
index 3ca5dbf9e83..17250cc0393 100644
--- a/src/include/catalog/catversion.h
+++ b/src/include/catalog/catversion.h
@@ -57,6 +57,6 @@
*/
/* yyyymmddN */
-#define CATALOG_VERSION_NO 202412021
+#define CATALOG_VERSION_NO 202412041
#endif
diff --git a/src/include/catalog/heap.h b/src/include/catalog/heap.h
index 8c278f202b4..19d25244b89 100644
--- a/src/include/catalog/heap.h
+++ b/src/include/catalog/heap.h
@@ -40,6 +40,7 @@ typedef struct CookedConstraint
char *name; /* name, or NULL if none */
AttrNumber attnum; /* which attr (only for NOTNULL, DEFAULT) */
Node *expr; /* transformed default or check expr */
+ bool is_enforced; /* is enforced? (only for CHECK) */
bool skip_validation; /* skip validation? (only for CHECK) */
bool is_local; /* constraint has local (non-inherited) def */
int16 inhcount; /* number of times constraint is inherited */
diff --git a/src/include/catalog/pg_constraint.h b/src/include/catalog/pg_constraint.h
index 4b4476738a2..8e13f5ed710 100644
--- a/src/include/catalog/pg_constraint.h
+++ b/src/include/catalog/pg_constraint.h
@@ -51,6 +51,7 @@ CATALOG(pg_constraint,2606,ConstraintRelationId)
char contype; /* constraint type; see codes below */
bool condeferrable; /* deferrable constraint? */
bool condeferred; /* deferred by default? */
+ bool conenforced; /* enforced constraint? */
bool convalidated; /* constraint has been validated? */
/*
@@ -222,6 +223,7 @@ extern Oid CreateConstraintEntry(const char *constraintName,
char constraintType,
bool isDeferrable,
bool isDeferred,
+ bool isEnforced,
bool isValidated,
Oid parentConstrId,
Oid relId,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 0f9462493e3..912aabfa4ca 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2736,6 +2736,8 @@ typedef enum ConstrType /* types of constraints */
CONSTR_ATTR_NOT_DEFERRABLE,
CONSTR_ATTR_DEFERRED,
CONSTR_ATTR_IMMEDIATE,
+ CONSTR_ATTR_ENFORCED,
+ CONSTR_ATTR_NOT_ENFORCED,
} ConstrType;
/* Foreign key action codes */
@@ -2757,6 +2759,7 @@ typedef struct Constraint
char *conname; /* Constraint name, or NULL if unnamed */
bool deferrable; /* DEFERRABLE? */
bool initdeferred; /* INITIALLY DEFERRED? */
+ bool is_enforced; /* enforced constraint? */
bool skip_validation; /* skip validation of existing rows? */
bool initially_valid; /* mark the new constraint as valid? */
bool is_no_inherit; /* is constraint non-inheritable? */
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 899d64ad55f..f2bc378394e 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -153,6 +153,7 @@ PG_KEYWORD("enable", ENABLE_P, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("encoding", ENCODING, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("encrypted", ENCRYPTED, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("end", END_P, RESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("enforced", ENFORCED, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("enum", ENUM_P, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("error", ERROR_P, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("escape", ESCAPE, UNRESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out
index 2212c8dbb59..b68ac47a3cd 100644
--- a/src/test/regress/expected/alter_table.out
+++ b/src/test/regress/expected/alter_table.out
@@ -507,11 +507,14 @@ ALTER TABLE attmp3 validate constraint attmpconstr;
ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten CHECK (b > 10); -- fail
ERROR: check constraint "b_greater_than_ten" of relation "attmp3" is violated by some row
ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten CHECK (b > 10) NOT VALID; -- succeeds
+ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten_not_enforced CHECK (b > 10) NOT ENFORCED; -- succeeds
ALTER TABLE attmp3 VALIDATE CONSTRAINT b_greater_than_ten; -- fails
ERROR: check constraint "b_greater_than_ten" of relation "attmp3" is violated by some row
DELETE FROM attmp3 WHERE NOT b > 10;
ALTER TABLE attmp3 VALIDATE CONSTRAINT b_greater_than_ten; -- succeeds
ALTER TABLE attmp3 VALIDATE CONSTRAINT b_greater_than_ten; -- succeeds
+ALTER TABLE attmp3 VALIDATE CONSTRAINT b_greater_than_ten_not_enforced; -- fail
+ERROR: cannot validated NOT ENFORCED constraint
-- Test inherited NOT VALID CHECK constraints
select * from attmp3;
a | b
diff --git a/src/test/regress/expected/constraints.out b/src/test/regress/expected/constraints.out
index 71200c90ed3..692a69fe457 100644
--- a/src/test/regress/expected/constraints.out
+++ b/src/test/regress/expected/constraints.out
@@ -87,6 +87,25 @@ SELECT * FROM CHECK_TBL;
6
(3 rows)
+CREATE TABLE NE_CHECK_TBL (x int,
+ CONSTRAINT CHECK_CON CHECK (x > 3) NOT ENFORCED);
+INSERT INTO NE_CHECK_TBL VALUES (5);
+INSERT INTO NE_CHECK_TBL VALUES (4);
+INSERT INTO NE_CHECK_TBL VALUES (3);
+INSERT INTO NE_CHECK_TBL VALUES (2);
+INSERT INTO NE_CHECK_TBL VALUES (6);
+INSERT INTO NE_CHECK_TBL VALUES (1);
+SELECT * FROM NE_CHECK_TBL;
+ x
+---
+ 5
+ 4
+ 3
+ 2
+ 6
+ 1
+(6 rows)
+
CREATE SEQUENCE CHECK_SEQ;
CREATE TABLE CHECK2_TBL (x int, y text, z int,
CONSTRAINT SEQUENCE_CON
@@ -120,7 +139,8 @@ CREATE TABLE INSERT_TBL (x INT DEFAULT nextval('insert_seq'),
y TEXT DEFAULT '-NULL-',
z INT DEFAULT -1 * currval('insert_seq'),
CONSTRAINT INSERT_TBL_CON CHECK (x >= 3 AND y <> 'check failed' AND x < 8),
- CHECK (x + z = 0));
+ CHECK (x + z = 0) ENFORCED, /* no change it is a default */
+ CONSTRAINT NE_INSERT_TBL_CON CHECK (x + z = 1) NOT ENFORCED);
INSERT INTO INSERT_TBL(x,z) VALUES (2, -2);
ERROR: new row for relation "insert_tbl" violates check constraint "insert_tbl_con"
DETAIL: Failing row contains (2, -NULL-, -2).
@@ -715,6 +735,24 @@ SELECT * FROM unique_tbl;
3 | threex
(5 rows)
+-- enforcibility cannot be specified or set for unique constrain
+CREATE TABLE UNIQUE_EN_TBL(i int UNIQUE ENFORCED);
+ERROR: misplaced ENFORCED clause
+LINE 1: CREATE TABLE UNIQUE_EN_TBL(i int UNIQUE ENFORCED);
+ ^
+CREATE TABLE UNIQUE_NOTEN_TBL(i int UNIQUE NOT ENFORCED);
+ERROR: misplaced NOT ENFORCED clause
+LINE 1: CREATE TABLE UNIQUE_NOTEN_TBL(i int UNIQUE NOT ENFORCED);
+ ^
+-- XXX: error message is misleading here
+ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
+ERROR: ALTER CONSTRAINT statement constraints cannot be marked ENFORCED
+LINE 1: ...TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
+ ^
+ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORCED;
+ERROR: ALTER CONSTRAINT statement constraints cannot be marked NOT ENFORCED
+LINE 1: ...ABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORC...
+ ^
DROP TABLE unique_tbl;
--
-- EXCLUDE constraints
diff --git a/src/test/regress/expected/create_table_like.out b/src/test/regress/expected/create_table_like.out
index d091da5a1ef..e0613891351 100644
--- a/src/test/regress/expected/create_table_like.out
+++ b/src/test/regress/expected/create_table_like.out
@@ -315,7 +315,8 @@ Referenced by:
DROP TABLE inhz;
-- including storage and comments
-CREATE TABLE ctlt1 (a text CHECK (length(a) > 2) PRIMARY KEY, b text);
+CREATE TABLE ctlt1 (a text CHECK (length(a) > 2) ENFORCED PRIMARY KEY,
+ b text CHECK (length(b) > 100) NOT ENFORCED);
CREATE INDEX ctlt1_b_key ON ctlt1 (b);
CREATE INDEX ctlt1_fnidx ON ctlt1 ((a || b));
CREATE STATISTICS ctlt1_a_b_stat ON a,b FROM ctlt1;
@@ -366,6 +367,7 @@ CREATE TABLE ctlt1_inh (LIKE ctlt1 INCLUDING CONSTRAINTS INCLUDING COMMENTS) INH
NOTICE: merging column "a" with inherited definition
NOTICE: merging column "b" with inherited definition
NOTICE: merging constraint "ctlt1_a_check" with inherited definition
+NOTICE: merging constraint "ctlt1_b_check" with inherited definition
\d+ ctlt1_inh
Table "public.ctlt1_inh"
Column | Type | Collation | Nullable | Default | Storage | Stats target | Description
@@ -374,6 +376,7 @@ NOTICE: merging constraint "ctlt1_a_check" with inherited definition
b | text | | | | extended | | B
Check constraints:
"ctlt1_a_check" CHECK (length(a) > 2)
+ "ctlt1_b_check" CHECK (length(b) > 100) NOT ENFORCED
Not-null constraints:
"ctlt1_a_not_null" NOT NULL "a" (local, inherited)
Inherits: ctlt1
@@ -395,6 +398,7 @@ NOTICE: merging multiple inherited definitions of column "a"
c | text | | | | external | |
Check constraints:
"ctlt1_a_check" CHECK (length(a) > 2)
+ "ctlt1_b_check" CHECK (length(b) > 100) NOT ENFORCED
"ctlt3_a_check" CHECK (length(a) < 5)
"ctlt3_c_check" CHECK (length(c) < 7)
Not-null constraints:
@@ -415,6 +419,7 @@ Indexes:
"ctlt13_like_expr_idx" btree ((a || c))
Check constraints:
"ctlt1_a_check" CHECK (length(a) > 2)
+ "ctlt1_b_check" CHECK (length(b) > 100) NOT ENFORCED
"ctlt3_a_check" CHECK (length(a) < 5)
"ctlt3_c_check" CHECK (length(c) < 7)
Not-null constraints:
@@ -440,6 +445,7 @@ Indexes:
"ctlt_all_expr_idx" btree ((a || b))
Check constraints:
"ctlt1_a_check" CHECK (length(a) > 2)
+ "ctlt1_b_check" CHECK (length(b) > 100) NOT ENFORCED
Statistics objects:
"public.ctlt_all_a_b_stat" ON a, b FROM ctlt_all
"public.ctlt_all_expr_stat" ON (a || b) FROM ctlt_all
@@ -482,6 +488,7 @@ Indexes:
"pg_attrdef_expr_idx" btree ((a || b))
Check constraints:
"ctlt1_a_check" CHECK (length(a) > 2)
+ "ctlt1_b_check" CHECK (length(b) > 100) NOT ENFORCED
Statistics objects:
"public.pg_attrdef_a_b_stat" ON a, b FROM public.pg_attrdef
"public.pg_attrdef_expr_stat" ON (a || b) FROM public.pg_attrdef
@@ -506,6 +513,7 @@ Indexes:
"ctlt1_expr_idx" btree ((a || b))
Check constraints:
"ctlt1_a_check" CHECK (length(a) > 2)
+ "ctlt1_b_check" CHECK (length(b) > 100) NOT ENFORCED
Statistics objects:
"ctl_schema.ctlt1_a_b_stat" ON a, b FROM ctlt1
"ctl_schema.ctlt1_expr_stat" ON (a || b) FROM ctlt1
diff --git a/src/test/regress/expected/domain.out b/src/test/regress/expected/domain.out
index 42b6559f9c8..d894122ce61 100644
--- a/src/test/regress/expected/domain.out
+++ b/src/test/regress/expected/domain.out
@@ -1297,6 +1297,21 @@ select pg_basetype(1); -- expect NULL not error
drop domain mytext cascade;
NOTICE: drop cascades to type mytext_child_1
--
+-- Explicit enforceability specification not allowed
+---
+CREATE DOMAIN constraint_enforced_dom AS int CONSTRAINT the_constraint CHECK (value > 0) ENFORCED;
+ERROR: specifying constraint enforceability not supported for domains
+CREATE DOMAIN constraint_not_enforced_dom AS int CONSTRAINT the_constraint CHECK (value > 0) NOT ENFORCED;
+ERROR: specifying constraint enforceability not supported for domains
+ALTER DOMAIN constraint_comments_dom ADD CONSTRAINT the_constraint CHECK (value > 0) ENFORCED;
+ERROR: CHECK constraints cannot be marked ENFORCED
+LINE 1: ...om ADD CONSTRAINT the_constraint CHECK (value > 0) ENFORCED;
+ ^
+ALTER DOMAIN constraint_comments_dom ADD CONSTRAINT the_constraint CHECK (value > 0) NOT ENFORCED;
+ERROR: CHECK constraints cannot be marked NOT ENFORCED
+LINE 1: ...m ADD CONSTRAINT the_constraint CHECK (value > 0) NOT ENFORC...
+ ^
+--
-- Information schema
--
SELECT * FROM information_schema.column_domain_usage
diff --git a/src/test/regress/expected/inherit.out b/src/test/regress/expected/inherit.out
index bb81f6d2b4d..0613ae5d52c 100644
--- a/src/test/regress/expected/inherit.out
+++ b/src/test/regress/expected/inherit.out
@@ -1330,6 +1330,13 @@ order by 1, 2;
p1_c1 | inh_check_constraint2 | t | 1
(4 rows)
+-- check constraints with different enforceability cannot be merged
+alter table p1_c1 add constraint inh_check_constraint3 check (f1 < 10) enforced;
+alter table p1 add constraint inh_check_constraint3 check (f1 < 10) not enforced;
+NOTICE: merging constraint "inh_check_constraint3" with inherited definition
+create table p1_fail() inherits(p1, p1_c1);
+NOTICE: merging multiple inherited definitions of column "f1"
+ERROR: check constraint name "inh_check_constraint3" appears multiple times but with different enforceability
drop table p1 cascade;
NOTICE: drop cascades to table p1_c1
--
diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql
index 637e3dac389..d8bdb91b367 100644
--- a/src/test/regress/sql/alter_table.sql
+++ b/src/test/regress/sql/alter_table.sql
@@ -387,10 +387,12 @@ ALTER TABLE attmp3 validate constraint attmpconstr;
-- Try a non-verified CHECK constraint
ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten CHECK (b > 10); -- fail
ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten CHECK (b > 10) NOT VALID; -- succeeds
+ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten_not_enforced CHECK (b > 10) NOT ENFORCED; -- succeeds
ALTER TABLE attmp3 VALIDATE CONSTRAINT b_greater_than_ten; -- fails
DELETE FROM attmp3 WHERE NOT b > 10;
ALTER TABLE attmp3 VALIDATE CONSTRAINT b_greater_than_ten; -- succeeds
ALTER TABLE attmp3 VALIDATE CONSTRAINT b_greater_than_ten; -- succeeds
+ALTER TABLE attmp3 VALIDATE CONSTRAINT b_greater_than_ten_not_enforced; -- fail
-- Test inherited NOT VALID CHECK constraints
select * from attmp3;
diff --git a/src/test/regress/sql/constraints.sql b/src/test/regress/sql/constraints.sql
index e607eb1fddb..d6742f83fb9 100644
--- a/src/test/regress/sql/constraints.sql
+++ b/src/test/regress/sql/constraints.sql
@@ -67,6 +67,18 @@ INSERT INTO CHECK_TBL VALUES (1);
SELECT * FROM CHECK_TBL;
+CREATE TABLE NE_CHECK_TBL (x int,
+ CONSTRAINT CHECK_CON CHECK (x > 3) NOT ENFORCED);
+
+INSERT INTO NE_CHECK_TBL VALUES (5);
+INSERT INTO NE_CHECK_TBL VALUES (4);
+INSERT INTO NE_CHECK_TBL VALUES (3);
+INSERT INTO NE_CHECK_TBL VALUES (2);
+INSERT INTO NE_CHECK_TBL VALUES (6);
+INSERT INTO NE_CHECK_TBL VALUES (1);
+
+SELECT * FROM NE_CHECK_TBL;
+
CREATE SEQUENCE CHECK_SEQ;
CREATE TABLE CHECK2_TBL (x int, y text, z int,
@@ -92,7 +104,8 @@ CREATE TABLE INSERT_TBL (x INT DEFAULT nextval('insert_seq'),
y TEXT DEFAULT '-NULL-',
z INT DEFAULT -1 * currval('insert_seq'),
CONSTRAINT INSERT_TBL_CON CHECK (x >= 3 AND y <> 'check failed' AND x < 8),
- CHECK (x + z = 0));
+ CHECK (x + z = 0) ENFORCED, /* no change it is a default */
+ CONSTRAINT NE_INSERT_TBL_CON CHECK (x + z = 1) NOT ENFORCED);
INSERT INTO INSERT_TBL(x,z) VALUES (2, -2);
@@ -518,6 +531,13 @@ COMMIT;
SELECT * FROM unique_tbl;
+-- enforcibility cannot be specified or set for unique constrain
+CREATE TABLE UNIQUE_EN_TBL(i int UNIQUE ENFORCED);
+CREATE TABLE UNIQUE_NOTEN_TBL(i int UNIQUE NOT ENFORCED);
+-- XXX: error message is misleading here
+ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
+ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORCED;
+
DROP TABLE unique_tbl;
--
diff --git a/src/test/regress/sql/create_table_like.sql b/src/test/regress/sql/create_table_like.sql
index dea8942c71f..a41f8b83d77 100644
--- a/src/test/regress/sql/create_table_like.sql
+++ b/src/test/regress/sql/create_table_like.sql
@@ -128,7 +128,8 @@ CREATE TABLE inhz (x text REFERENCES inhz, LIKE inhx INCLUDING INDEXES);
DROP TABLE inhz;
-- including storage and comments
-CREATE TABLE ctlt1 (a text CHECK (length(a) > 2) PRIMARY KEY, b text);
+CREATE TABLE ctlt1 (a text CHECK (length(a) > 2) ENFORCED PRIMARY KEY,
+ b text CHECK (length(b) > 100) NOT ENFORCED);
CREATE INDEX ctlt1_b_key ON ctlt1 (b);
CREATE INDEX ctlt1_fnidx ON ctlt1 ((a || b));
CREATE STATISTICS ctlt1_a_b_stat ON a,b FROM ctlt1;
diff --git a/src/test/regress/sql/domain.sql b/src/test/regress/sql/domain.sql
index ee07b03174e..518455c058a 100644
--- a/src/test/regress/sql/domain.sql
+++ b/src/test/regress/sql/domain.sql
@@ -866,6 +866,14 @@ select pg_basetype(1); -- expect NULL not error
drop domain mytext cascade;
+--
+-- Explicit enforceability specification not allowed
+---
+CREATE DOMAIN constraint_enforced_dom AS int CONSTRAINT the_constraint CHECK (value > 0) ENFORCED;
+CREATE DOMAIN constraint_not_enforced_dom AS int CONSTRAINT the_constraint CHECK (value > 0) NOT ENFORCED;
+ALTER DOMAIN constraint_comments_dom ADD CONSTRAINT the_constraint CHECK (value > 0) ENFORCED;
+ALTER DOMAIN constraint_comments_dom ADD CONSTRAINT the_constraint CHECK (value > 0) NOT ENFORCED;
+
--
-- Information schema
diff --git a/src/test/regress/sql/inherit.sql b/src/test/regress/sql/inherit.sql
index f51c70d6b03..d44ebf4e8ad 100644
--- a/src/test/regress/sql/inherit.sql
+++ b/src/test/regress/sql/inherit.sql
@@ -472,6 +472,12 @@ select conrelid::regclass::text as relname, conname, conislocal, coninhcount
from pg_constraint where conname like 'inh\_check\_constraint%'
order by 1, 2;
+-- check constraints with different enforceability cannot be merged
+alter table p1_c1 add constraint inh_check_constraint3 check (f1 < 10) enforced;
+alter table p1 add constraint inh_check_constraint3 check (f1 < 10) not enforced;
+
+create table p1_fail() inherits(p1, p1_c1);
+
drop table p1 cascade;
--
--
2.43.5
v6-0002-refactor-split-ATExecAlterConstrRecurse.patchapplication/x-patch; name=v6-0002-refactor-split-ATExecAlterConstrRecurse.patchDownload
From 1c129470d1baacf8b85f650a997793ae8293a547 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 23 Sep 2024 12:31:07 +0530
Subject: [PATCH v6 2/5] refactor: split ATExecAlterConstrRecurse()
When changing a NOT ENFORCED foreign key constraint to ENFORCED, we
need to create the necessary triggers for enforcing the constraint. We
can set the deferrability at the time of creation and skip the code
used for other constraint alteration operations.
Additionally, move the code that iterates over each child constraint
of the constraint being altered into a separate function.
---
src/backend/commands/tablecmds.c | 194 +++++++++++++++++++------------
1 file changed, 118 insertions(+), 76 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 24df1aec6b7..1e839642e26 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -394,6 +394,12 @@ static ObjectAddress ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd,
static bool ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
Relation rel, HeapTuple contuple, List **otherrelids,
LOCKMODE lockmode);
+static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
+ bool deferrable, bool initdeferred,
+ List **otherrelids);
+static void ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
+ Relation rel, HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode);
static ObjectAddress ATExecValidateConstraint(List **wqueue,
Relation rel, char *constrName,
bool recurse, bool recursing, LOCKMODE lockmode);
@@ -11846,9 +11852,6 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
{
HeapTuple copyTuple;
Form_pg_constraint copy_con;
- HeapTuple tgtuple;
- ScanKeyData tgkey;
- SysScanDesc tgscan;
copyTuple = heap_copytuple(contuple);
copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
@@ -11869,53 +11872,8 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
* Now we need to update the multiple entries in pg_trigger that
* implement the constraint.
*/
- ScanKeyInit(&tgkey,
- Anum_pg_trigger_tgconstraint,
- BTEqualStrategyNumber, F_OIDEQ,
- ObjectIdGetDatum(conoid));
- tgscan = systable_beginscan(tgrel, TriggerConstraintIndexId, true,
- NULL, 1, &tgkey);
- while (HeapTupleIsValid(tgtuple = systable_getnext(tgscan)))
- {
- Form_pg_trigger tgform = (Form_pg_trigger) GETSTRUCT(tgtuple);
- Form_pg_trigger copy_tg;
- HeapTuple tgCopyTuple;
-
- /*
- * Remember OIDs of other relation(s) involved in FK constraint.
- * (Note: it's likely that we could skip forcing a relcache inval
- * for other rels that don't have a trigger whose properties
- * change, but let's be conservative.)
- */
- if (tgform->tgrelid != RelationGetRelid(rel))
- *otherrelids = list_append_unique_oid(*otherrelids,
- tgform->tgrelid);
-
- /*
- * Update deferrability of RI_FKey_noaction_del,
- * RI_FKey_noaction_upd, RI_FKey_check_ins and RI_FKey_check_upd
- * triggers, but not others; see createForeignKeyActionTriggers
- * and CreateFKCheckTrigger.
- */
- if (tgform->tgfoid != F_RI_FKEY_NOACTION_DEL &&
- tgform->tgfoid != F_RI_FKEY_NOACTION_UPD &&
- tgform->tgfoid != F_RI_FKEY_CHECK_INS &&
- tgform->tgfoid != F_RI_FKEY_CHECK_UPD)
- continue;
-
- tgCopyTuple = heap_copytuple(tgtuple);
- copy_tg = (Form_pg_trigger) GETSTRUCT(tgCopyTuple);
-
- copy_tg->tgdeferrable = cmdcon->deferrable;
- copy_tg->tginitdeferred = cmdcon->initdeferred;
- CatalogTupleUpdate(tgrel, &tgCopyTuple->t_self, tgCopyTuple);
-
- InvokeObjectPostAlterHook(TriggerRelationId, tgform->oid, 0);
-
- heap_freetuple(tgCopyTuple);
- }
-
- systable_endscan(tgscan);
+ AlterConstrTriggerDeferrability(conoid, tgrel, rel, cmdcon->deferrable,
+ cmdcon->initdeferred, otherrelids);
}
/*
@@ -11928,36 +11886,120 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
*/
if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)
- {
- ScanKeyData pkey;
- SysScanDesc pscan;
- HeapTuple childtup;
-
- ScanKeyInit(&pkey,
- Anum_pg_constraint_conparentid,
- BTEqualStrategyNumber, F_OIDEQ,
- ObjectIdGetDatum(conoid));
-
- pscan = systable_beginscan(conrel, ConstraintParentIndexId,
- true, NULL, 1, &pkey);
-
- while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
- {
- Form_pg_constraint childcon = (Form_pg_constraint) GETSTRUCT(childtup);
- Relation childrel;
-
- childrel = table_open(childcon->conrelid, lockmode);
- ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, childrel, childtup,
- otherrelids, lockmode);
- table_close(childrel, NoLock);
- }
-
- systable_endscan(pscan);
- }
+ ATExecAlterChildConstr(cmdcon, conrel, tgrel, rel, contuple,
+ otherrelids, lockmode);
return changed;
}
+/*
+ * A subroutine of ATExecAlterConstrRecurse that updated constraint trigger's
+ * deferrability.
+ *
+ * The arguments to this function have the same meaning as the arguments to
+ * ATExecAlterConstrRecurse.
+ */
+static void
+AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
+ bool deferrable, bool initdeferred,
+ List **otherrelids)
+{
+ HeapTuple tgtuple;
+ ScanKeyData tgkey;
+ SysScanDesc tgscan;
+
+ ScanKeyInit(&tgkey,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+ tgscan = systable_beginscan(tgrel, TriggerConstraintIndexId, true,
+ NULL, 1, &tgkey);
+ while (HeapTupleIsValid(tgtuple = systable_getnext(tgscan)))
+ {
+ Form_pg_trigger tgform = (Form_pg_trigger) GETSTRUCT(tgtuple);
+ Form_pg_trigger copy_tg;
+ HeapTuple tgCopyTuple;
+
+ /*
+ * Remember OIDs of other relation(s) involved in FK constraint.
+ * (Note: it's likely that we could skip forcing a relcache inval for
+ * other rels that don't have a trigger whose properties change, but
+ * let's be conservative.)
+ */
+ if (tgform->tgrelid != RelationGetRelid(rel))
+ *otherrelids = list_append_unique_oid(*otherrelids,
+ tgform->tgrelid);
+
+ /*
+ * Update enable status and deferrability of RI_FKey_noaction_del,
+ * RI_FKey_noaction_upd, RI_FKey_check_ins and RI_FKey_check_upd
+ * triggers, but not others; see createForeignKeyActionTriggers and
+ * CreateFKCheckTrigger.
+ */
+ if (tgform->tgfoid != F_RI_FKEY_NOACTION_DEL &&
+ tgform->tgfoid != F_RI_FKEY_NOACTION_UPD &&
+ tgform->tgfoid != F_RI_FKEY_CHECK_INS &&
+ tgform->tgfoid != F_RI_FKEY_CHECK_UPD)
+ continue;
+
+ tgCopyTuple = heap_copytuple(tgtuple);
+ copy_tg = (Form_pg_trigger) GETSTRUCT(tgCopyTuple);
+
+ copy_tg->tgdeferrable = deferrable;
+ copy_tg->tginitdeferred = initdeferred;
+ CatalogTupleUpdate(tgrel, &tgCopyTuple->t_self, tgCopyTuple);
+
+ InvokeObjectPostAlterHook(TriggerRelationId, tgform->oid, 0);
+
+ heap_freetuple(tgCopyTuple);
+ }
+
+ systable_endscan(tgscan);
+}
+
+/*
+ * Invokes ATExecAlterConstrRecurse for each constraint that is a child of the
+ * specified constraint.
+ *
+ * The arguments to this function have the same meaning as the arguments to
+ * ATExecAlterConstrRecurse.
+ */
+static void
+ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
+ Relation rel, HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode)
+{
+ Form_pg_constraint currcon;
+ Oid conoid;
+ ScanKeyData pkey;
+ SysScanDesc pscan;
+ HeapTuple childtup;
+
+ currcon = (Form_pg_constraint) GETSTRUCT(contuple);
+ conoid = currcon->oid;
+
+ ScanKeyInit(&pkey,
+ Anum_pg_constraint_conparentid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+
+ pscan = systable_beginscan(conrel, ConstraintParentIndexId,
+ true, NULL, 1, &pkey);
+
+ while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
+ {
+ Form_pg_constraint childcon = (Form_pg_constraint) GETSTRUCT(childtup);
+ Relation childrel;
+
+ childrel = table_open(childcon->conrelid, lockmode);
+ ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, childrel, childtup,
+ otherrelids, lockmode);
+ table_close(childrel, NoLock);
+ }
+
+ systable_endscan(pscan);
+}
+
/*
* ALTER TABLE VALIDATE CONSTRAINT
*
--
2.43.5
v6-0003-refactor-Change-ATExecAlterConstrRecurse-argument.patchapplication/x-patch; name=v6-0003-refactor-Change-ATExecAlterConstrRecurse-argument.patchDownload
From d314e28602658f24900f72123b4b6a9c3ef033b0 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 23 Sep 2024 12:37:05 +0530
Subject: [PATCH v6 3/5] refactor: Change ATExecAlterConstrRecurse() argument.
Instead of passing the Relation of the referencing relation, we will
pass a relid. Opening the relation using the relid again should not
cause significant issues. However, this approach makes recursion more
efficient, especially in the later patch where we will be recreating
the referencing and referred triggers. In addition to that passing
relid of the referred relation too and Oids of triggers.
----
NOTE: This patch is not meant to be committed separately. It should
be squashed with the main patch that adds ENFORCED/NOT ENFORCED.
----
---
src/backend/commands/tablecmds.c | 66 ++++++++++++++++++++++----------
1 file changed, 45 insertions(+), 21 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 1e839642e26..93cbfba6384 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -392,14 +392,22 @@ static void AlterSeqNamespaces(Relation classRel, Relation rel,
static ObjectAddress ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd,
bool recurse, bool recursing, LOCKMODE lockmode);
static bool ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode);
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger);
static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
bool deferrable, bool initdeferred,
List **otherrelids);
static void ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode);
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger);
static ObjectAddress ATExecValidateConstraint(List **wqueue,
Relation rel, char *constrName,
bool recurse, bool recursing, LOCKMODE lockmode);
@@ -11792,8 +11800,10 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
currcon->condeferred != cmdcon->initdeferred ||
rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
- if (ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, rel, contuple,
- &otherrelids, lockmode))
+ if (ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, currcon->conrelid,
+ currcon->confrelid, contuple, &otherrelids,
+ lockmode, InvalidOid, InvalidOid,
+ InvalidOid, InvalidOid))
ObjectAddressSet(address, ConstraintRelationId, currcon->oid);
}
@@ -11826,13 +11836,18 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
*/
static bool
ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode)
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger)
{
Form_pg_constraint currcon;
Oid conoid;
Oid refrelid;
bool changed = false;
+ Relation rel;
/* since this function recurses, it could be driven to stack overflow */
check_stack_depth();
@@ -11841,6 +11856,8 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
conoid = currcon->oid;
refrelid = currcon->confrelid;
+ rel = table_open(currcon->conrelid, lockmode);
+
/*
* Update pg_constraint with the flags from cmdcon.
*
@@ -11886,8 +11903,14 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
*/
if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)
- ATExecAlterChildConstr(cmdcon, conrel, tgrel, rel, contuple,
- otherrelids, lockmode);
+ ATExecAlterChildConstr(cmdcon, conrel, tgrel, fkrelid, pkrelid,
+ contuple, otherrelids, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
+
+ table_close(rel, NoLock);
return changed;
}
@@ -11966,8 +11989,12 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
*/
static void
ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode)
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger)
{
Form_pg_constraint currcon;
Oid conoid;
@@ -11987,15 +12014,12 @@ ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
true, NULL, 1, &pkey);
while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
- {
- Form_pg_constraint childcon = (Form_pg_constraint) GETSTRUCT(childtup);
- Relation childrel;
-
- childrel = table_open(childcon->conrelid, lockmode);
- ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, childrel, childtup,
- otherrelids, lockmode);
- table_close(childrel, NoLock);
- }
+ ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, fkrelid, pkrelid,
+ childtup, otherrelids, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
systable_endscan(pscan);
}
--
2.43.5
v6-0004-Remove-hastriggers-flag-check-before-fetching-FK-.patchapplication/x-patch; name=v6-0004-Remove-hastriggers-flag-check-before-fetching-FK-.patchDownload
From 983ae4ab4fdafab9ebef2427aacdd9768b5e5d1d Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 7 Oct 2024 12:40:07 +0530
Subject: [PATCH v6 4/5] Remove hastriggers flag check before fetching FK
constraints.
With NOT ENFORCED, foreign key constraints will be created without
triggers. Therefore, the criteria for fetching foreign keys based on
the presence of triggers no longer apply.
----
NOTE: This patch is intended to reduce the diff noise from the main
patch and is not meant to be committed separately. It should be
squashed with the main patch that adds ENFORCED/NOT ENFORCED.
----
---
src/backend/utils/cache/relcache.c | 5 -
src/bin/pg_dump/pg_dump.c | 8 +-
src/bin/psql/describe.c | 226 ++++++++++++++---------------
3 files changed, 108 insertions(+), 131 deletions(-)
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 25db64dc440..0809f03aca2 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4669,11 +4669,6 @@ RelationGetFKeyList(Relation relation)
if (relation->rd_fkeyvalid)
return relation->rd_fkeylist;
- /* Fast path: non-partitioned tables without triggers can't have FKs */
- if (!relation->rd_rel->relhastriggers &&
- relation->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
- return NIL;
-
/*
* We build the list we intend to return (in the caller's context) while
* doing the scan. After successfully completing the scan, we copy that
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index ec0cdf4ed74..9ada4cdc182 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -7839,13 +7839,7 @@ getConstraints(Archive *fout, TableInfo tblinfo[], int numTables)
{
TableInfo *tinfo = &tblinfo[i];
- /*
- * For partitioned tables, foreign keys have no triggers so they must
- * be included anyway in case some foreign keys are defined.
- */
- if ((!tinfo->hastriggers &&
- tinfo->relkind != RELKIND_PARTITIONED_TABLE) ||
- !(tinfo->dobj.dump & DUMP_COMPONENT_DEFINITION))
+ if (!(tinfo->dobj.dump & DUMP_COMPONENT_DEFINITION))
continue;
/* OK, we need info for this table */
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 2657abdc72d..054e8a2d821 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2528,136 +2528,124 @@ describeOneTableDetails(const char *schemaname,
PQclear(result);
}
- /*
- * Print foreign-key constraints (there are none if no triggers,
- * except if the table is partitioned, in which case the triggers
- * appear in the partitions)
- */
- if (tableinfo.hastriggers ||
- tableinfo.relkind == RELKIND_PARTITIONED_TABLE)
+ /* Print foreign-key constraints */
+ if (pset.sversion >= 120000 &&
+ (tableinfo.ispartition || tableinfo.relkind == RELKIND_PARTITIONED_TABLE))
{
- if (pset.sversion >= 120000 &&
- (tableinfo.ispartition || tableinfo.relkind == RELKIND_PARTITIONED_TABLE))
+ /*
+ * Put the constraints defined in this table first, followed
+ * by the constraints defined in ancestor partitioned tables.
+ */
+ printfPQExpBuffer(&buf,
+ "SELECT conrelid = '%s'::pg_catalog.regclass AS sametable,\n"
+ " conname,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef,\n"
+ " conrelid::pg_catalog.regclass AS ontable\n"
+ " FROM pg_catalog.pg_constraint,\n"
+ " pg_catalog.pg_partition_ancestors('%s')\n"
+ " WHERE conrelid = relid AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
+ "ORDER BY sametable DESC, conname;",
+ oid, oid);
+ }
+ else
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT true as sametable, conname,\n"
+ " pg_catalog.pg_get_constraintdef(r.oid, true) as condef,\n"
+ " conrelid::pg_catalog.regclass AS ontable\n"
+ "FROM pg_catalog.pg_constraint r\n"
+ "WHERE r.conrelid = '%s' AND r.contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n",
+ oid);
+
+ if (pset.sversion >= 120000)
+ appendPQExpBufferStr(&buf, " AND conparentid = 0\n");
+ appendPQExpBufferStr(&buf, "ORDER BY conname");
+ }
+
+ result = PSQLexec(buf.data);
+ if (!result)
+ goto error_return;
+ else
+ tuples = PQntuples(result);
+
+ if (tuples > 0)
+ {
+ int i_sametable = PQfnumber(result, "sametable"),
+ i_conname = PQfnumber(result, "conname"),
+ i_condef = PQfnumber(result, "condef"),
+ i_ontable = PQfnumber(result, "ontable");
+
+ printTableAddFooter(&cont, _("Foreign-key constraints:"));
+ for (i = 0; i < tuples; i++)
{
/*
- * Put the constraints defined in this table first, followed
- * by the constraints defined in ancestor partitioned tables.
+ * Print untranslated constraint name and definition. Use
+ * a "TABLE tab" prefix when the constraint is defined in
+ * a parent partitioned table.
*/
- printfPQExpBuffer(&buf,
- "SELECT conrelid = '%s'::pg_catalog.regclass AS sametable,\n"
- " conname,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef,\n"
- " conrelid::pg_catalog.regclass AS ontable\n"
- " FROM pg_catalog.pg_constraint,\n"
- " pg_catalog.pg_partition_ancestors('%s')\n"
- " WHERE conrelid = relid AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
- "ORDER BY sametable DESC, conname;",
- oid, oid);
- }
- else
- {
- printfPQExpBuffer(&buf,
- "SELECT true as sametable, conname,\n"
- " pg_catalog.pg_get_constraintdef(r.oid, true) as condef,\n"
- " conrelid::pg_catalog.regclass AS ontable\n"
- "FROM pg_catalog.pg_constraint r\n"
- "WHERE r.conrelid = '%s' AND r.contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n",
- oid);
-
- if (pset.sversion >= 120000)
- appendPQExpBufferStr(&buf, " AND conparentid = 0\n");
- appendPQExpBufferStr(&buf, "ORDER BY conname");
- }
-
- result = PSQLexec(buf.data);
- if (!result)
- goto error_return;
- else
- tuples = PQntuples(result);
-
- if (tuples > 0)
- {
- int i_sametable = PQfnumber(result, "sametable"),
- i_conname = PQfnumber(result, "conname"),
- i_condef = PQfnumber(result, "condef"),
- i_ontable = PQfnumber(result, "ontable");
-
- printTableAddFooter(&cont, _("Foreign-key constraints:"));
- for (i = 0; i < tuples; i++)
- {
- /*
- * Print untranslated constraint name and definition. Use
- * a "TABLE tab" prefix when the constraint is defined in
- * a parent partitioned table.
- */
- if (strcmp(PQgetvalue(result, i, i_sametable), "f") == 0)
- printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
- PQgetvalue(result, i, i_ontable),
- PQgetvalue(result, i, i_conname),
- PQgetvalue(result, i, i_condef));
- else
- printfPQExpBuffer(&buf, " \"%s\" %s",
- PQgetvalue(result, i, i_conname),
- PQgetvalue(result, i, i_condef));
-
- printTableAddFooter(&cont, buf.data);
- }
- }
- PQclear(result);
- }
-
- /* print incoming foreign-key references */
- if (tableinfo.hastriggers ||
- tableinfo.relkind == RELKIND_PARTITIONED_TABLE)
- {
- if (pset.sversion >= 120000)
- {
- printfPQExpBuffer(&buf,
- "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
- " FROM pg_catalog.pg_constraint c\n"
- " WHERE confrelid IN (SELECT pg_catalog.pg_partition_ancestors('%s')\n"
- " UNION ALL VALUES ('%s'::pg_catalog.regclass))\n"
- " AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
- "ORDER BY conname;",
- oid, oid);
- }
- else
- {
- printfPQExpBuffer(&buf,
- "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
- " FROM pg_catalog.pg_constraint\n"
- " WHERE confrelid = %s AND contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n"
- "ORDER BY conname;",
- oid);
- }
-
- result = PSQLexec(buf.data);
- if (!result)
- goto error_return;
- else
- tuples = PQntuples(result);
-
- if (tuples > 0)
- {
- int i_conname = PQfnumber(result, "conname"),
- i_ontable = PQfnumber(result, "ontable"),
- i_condef = PQfnumber(result, "condef");
-
- printTableAddFooter(&cont, _("Referenced by:"));
- for (i = 0; i < tuples; i++)
- {
+ if (strcmp(PQgetvalue(result, i, i_sametable), "f") == 0)
printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
PQgetvalue(result, i, i_ontable),
PQgetvalue(result, i, i_conname),
PQgetvalue(result, i, i_condef));
+ else
+ printfPQExpBuffer(&buf, " \"%s\" %s",
+ PQgetvalue(result, i, i_conname),
+ PQgetvalue(result, i, i_condef));
- printTableAddFooter(&cont, buf.data);
- }
+ printTableAddFooter(&cont, buf.data);
}
- PQclear(result);
}
+ PQclear(result);
+
+ /* print incoming foreign-key references */
+ if (pset.sversion >= 120000)
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
+ " FROM pg_catalog.pg_constraint c\n"
+ " WHERE confrelid IN (SELECT pg_catalog.pg_partition_ancestors('%s')\n"
+ " UNION ALL VALUES ('%s'::pg_catalog.regclass))\n"
+ " AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
+ "ORDER BY conname;",
+ oid, oid);
+ }
+ else
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
+ " FROM pg_catalog.pg_constraint\n"
+ " WHERE confrelid = %s AND contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n"
+ "ORDER BY conname;",
+ oid);
+ }
+
+ result = PSQLexec(buf.data);
+ if (!result)
+ goto error_return;
+ else
+ tuples = PQntuples(result);
+
+ if (tuples > 0)
+ {
+ int i_conname = PQfnumber(result, "conname"),
+ i_ontable = PQfnumber(result, "ontable"),
+ i_condef = PQfnumber(result, "condef");
+
+ printTableAddFooter(&cont, _("Referenced by:"));
+ for (i = 0; i < tuples; i++)
+ {
+ printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
+ PQgetvalue(result, i, i_ontable),
+ PQgetvalue(result, i, i_conname),
+ PQgetvalue(result, i, i_condef));
+
+ printTableAddFooter(&cont, buf.data);
+ }
+ }
+ PQclear(result);
/* print any row-level policies */
if (pset.sversion >= 90500)
--
2.43.5
v6-0005-WIP-Add-support-for-NOT-ENFORCED-in-foreign-key-c.patchapplication/x-patch; name=v6-0005-WIP-Add-support-for-NOT-ENFORCED-in-foreign-key-c.patchDownload
From 37de480acd074613e1eb518b2e9b3d81d6038419 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Wed, 4 Dec 2024 15:23:38 +0530
Subject: [PATCH v6 5/5] WIP-Add support for NOT ENFORCED in foreign key
constraints.
Typically, when a foreign key (FK) constraint is created on a table,
action and check triggers are added to maintain data integrity. With
this patch, if a constraint is marked as NOT ENFORCED, integrity
checks are no longer required, making these triggers unnecessary.
Consequently, when creating a NOT ENFORCED FK constraint, triggers
will not be created, and the constraint will be marked as NOT VALID.
Similarly, if an existing FK constraint is changed to NOT ENFORCED,
the associated triggers will be dropped, and the constraint will also
be marked as NOT VALID. Conversely, if a NOT ENFORCED FK constraint is
changed to ENFORCED, the necessary triggers will be created, and the
constraint will remain in the NOT VALID state.
== WIP ==
The restriction preventing NOT VALID foreign key constraints on
partitioned tables has now been bypassed for NOT ENFORCED constraints,
but it could potentially be removed entirely. As per Alvaro Herrera,
this feature has not yet been implemented for partitioned tables.
Further investigation is needed to determine what additional changes,
aside from removing the error check, are necessary to fully support
NOT VALID foreign key constraints on partitioned tables. As a result,
this patch remains marked as WIP.
=========
---
doc/src/sgml/advanced.sgml | 13 +
doc/src/sgml/catalogs.sgml | 2 +-
doc/src/sgml/ddl.sgml | 12 +-
doc/src/sgml/ref/alter_table.sgml | 12 +-
doc/src/sgml/ref/create_table.sgml | 10 +-
src/backend/catalog/pg_constraint.c | 5 +-
src/backend/commands/tablecmds.c | 439 ++++++++++++++++------
src/backend/parser/gram.y | 5 +-
src/backend/parser/parse_utilcmd.c | 14 +-
src/backend/utils/cache/relcache.c | 1 +
src/include/utils/rel.h | 3 +
src/test/regress/expected/alter_table.out | 2 +-
src/test/regress/expected/constraints.out | 8 +-
src/test/regress/expected/foreign_key.out | 126 ++++++-
src/test/regress/sql/foreign_key.sql | 89 ++++-
15 files changed, 589 insertions(+), 152 deletions(-)
diff --git a/doc/src/sgml/advanced.sgml b/doc/src/sgml/advanced.sgml
index 755c9f14850..b7d90de69d7 100644
--- a/doc/src/sgml/advanced.sgml
+++ b/doc/src/sgml/advanced.sgml
@@ -125,6 +125,19 @@ ERROR: insert or update on table "weather" violates foreign key constraint "wea
DETAIL: Key (city)=(Berkeley) is not present in table "cities".
</screen>
</para>
+ <para>
+ If, for some reason, you do not want to enforce this constraint and wish to
+ avoid the error, you can modify the foreign key constraint to <literal>NOT ENFORCED</literal>,
+ or specify it in the <link linkend="sql-createtable"><command>CREATE TABLE</command></link>.
+ </para>
+ <para>
+ Let's change the constraint to <literal>NOT ENFORCED</literal>, and when you
+ attempt to reinsert the same record, it will not result in any error.
+<programlisting>
+ALTER TABLE weather ALTER CONSTRAINT weather_city_fkey NOT ENFORCED;
+INSERT INTO weather VALUES ('Berkeley', 45, 53, 0.0, '1994-11-28');
+</programlisting>
+ </para>
<para>
The behavior of foreign keys can be finely tuned to your
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index a56ef4df562..3bb04574481 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2608,7 +2608,7 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l
</para>
<para>
Is the constraint enforceable?
- Currently, can be false only for CHECK constraints
+ Currently, can be false only for foreign keys and CHECK constraints
</para></entry>
</row>
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index 9a20396dae4..cda36576670 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -1058,11 +1058,13 @@ CREATE TABLE example (
</indexterm>
<para>
- A foreign key constraint specifies that the values in a column (or
- a group of columns) must match the values appearing in some row
- of another table.
- We say this maintains the <firstterm>referential
- integrity</firstterm> between two related tables.
+ A foreign key constraint with <literal>ENFORCED</literal>, the default
+ setting, specifies that the values in a column (or a group of columns)
+ must match the values appearing in some row of another table. We say this
+ maintains the <firstterm>referential integrity</firstterm> between two
+ related tables. If a foreign key constraint is created with <literal>NOT ENFORCED</literal>,
+ the check for aforementioned <firstterm>referential integrity</firstterm>
+ will not be performed.
</para>
<para>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 938450fba18..fa807cb9f3a 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -58,7 +58,7 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable>
ALTER [ COLUMN ] <replaceable class="parameter">column_name</replaceable> SET COMPRESSION <replaceable class="parameter">compression_method</replaceable>
ADD <replaceable class="parameter">table_constraint</replaceable> [ NOT VALID ]
ADD <replaceable class="parameter">table_constraint_using_index</replaceable>
- ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+ ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]
VALIDATE CONSTRAINT <replaceable class="parameter">constraint_name</replaceable>
DROP CONSTRAINT [ IF EXISTS ] <replaceable class="parameter">constraint_name</replaceable> [ RESTRICT | CASCADE ]
DISABLE TRIGGER [ <replaceable class="parameter">trigger_name</replaceable> | ALL | USER ]
@@ -555,6 +555,13 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
This form alters the attributes of a constraint that was previously
created. Currently only foreign key constraints may be altered.
</para>
+ <para>
+ Note that when a constraint is changed to <literal>NOT ENFORCED</literal>,
+ it is also marked as <literal>NOT VALID</literal>. While this might seem
+ unnecessary, it is done for consistency and should be set accordingly.
+ This ensures that if the constraint is later changed to <literal>ENFORCED</literal>,
+ it will automatically be in the correct <literal>NOT VALID</literal> state.
+ </para>
</listitem>
</varlistentry>
@@ -565,7 +572,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
This form validates a foreign key or check constraint that was
previously created as <literal>NOT VALID</literal>, by scanning the
table to ensure there are no rows for which the constraint is not
- satisfied. Nothing happens if the constraint is already marked valid.
+ satisfied. If the constraint is not enforced, an error is thrown.
+ Nothing happens if the constraint is already marked valid.
(See <xref linkend="sql-altertable-notes"/> below for an explanation
of the usefulness of this command.)
</para>
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index cfd01c4ef2b..89fc7354c00 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -1382,11 +1382,11 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
<term><literal>NOT ENFORCED</literal></term>
<listitem>
<para>
- This is currently only allowed for <literal>CHECK</literal> constraints.
- If the constraint is <literal>NOT ENFORCED</literal>, this clause
- specifies that the constraint check will be skipped. When the constraint
- is <literal>ENFORCED</literal>, check is performed after each statement;
- the latter is the default.
+ This is currently only allowed for foreign key and <literal>CHECK</literal>
+ constraints. If the constraint is <literal>NOT ENFORCED</literal>, this
+ clause specifies that the constraint check will be skipped. When the
+ constraint is <literal>ENFORCED</literal>, check is performed after each
+ statement; the latter is the default.
</para>
<para>
Note that if the constraint is <literal>NOT ENFORCED</literal>, it is also
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index f9e4c3d3cfa..3e8a6eb94f3 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -100,8 +100,9 @@ CreateConstraintEntry(const char *constraintName,
ObjectAddresses *addrs_auto;
ObjectAddresses *addrs_normal;
- /* Only CHECK constraint can be not enforced */
- Assert(isEnforced || constraintType == CONSTRAINT_CHECK);
+ /* Only CHECK or FOREIGN KEY constraint can be not enforced */
+ Assert(isEnforced || constraintType == CONSTRAINT_CHECK ||
+ constraintType == CONSTRAINT_FOREIGN);
/* NOT ENFORCED constraint must be NOT VALID */
Assert(isEnforced || !isValidated);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 93cbfba6384..a73e992cf63 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -389,19 +389,29 @@ static void AlterIndexNamespaces(Relation classRel, Relation rel,
static void AlterSeqNamespaces(Relation classRel, Relation rel,
Oid oldNspOid, Oid newNspOid, ObjectAddresses *objsMoved,
LOCKMODE lockmode);
-static ObjectAddress ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd,
+static ObjectAddress ATExecAlterConstraint(List **wqueue, Relation rel, AlterTableCmd *cmd,
bool recurse, bool recursing, LOCKMODE lockmode);
-static bool ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
+static bool ATExecAlterConstrRecurse(List **wqueue, Constraint *cmdcon,
+ Relation conrel, Relation tgrel,
const Oid fkrelid, const Oid pkrelid,
HeapTuple contuple, List **otherrelids,
LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
Oid ReferencedParentUpdTrigger,
Oid ReferencingParentInsTrigger,
Oid ReferencingParentUpdTrigger);
-static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
+static void ATExecAlterConstrEnforceability(List **wqueue, Constraint *cmdcon,
+ Relation conrel, Relation tgrel,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger);
+static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Oid fkrel,
bool deferrable, bool initdeferred,
List **otherrelids);
-static void ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
+static void ATExecAlterChildConstr(List **wqueue, Constraint *cmdcon,
+ Relation conrel, Relation tgrel,
const Oid fkrelid, const Oid pkrelid,
HeapTuple contuple, List **otherrelids,
LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
@@ -5424,7 +5434,7 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
lockmode);
break;
case AT_AlterConstraint: /* ALTER CONSTRAINT */
- address = ATExecAlterConstraint(rel, cmd, false, false, lockmode);
+ address = ATExecAlterConstraint(wqueue, rel, cmd, false, false, lockmode);
break;
case AT_ValidateConstraint: /* VALIDATE CONSTRAINT */
address = ATExecValidateConstraint(wqueue, rel, cmd->name, cmd->recurse,
@@ -9768,7 +9778,24 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
errmsg("cannot use ONLY for foreign key on partitioned table \"%s\" referencing relation \"%s\"",
RelationGetRelationName(rel),
RelationGetRelationName(pkrel))));
- if (fkconstraint->skip_validation && !fkconstraint->initially_valid)
+
+
+ /*
+ * TODO: I'm not sure why this restriction exists; NOT VALID foreign
+ * keys seem to work fine on partitioned tables, with behavior matching
+ * that of NOT VALID foreign keys on regular tables. I still need to
+ * confirm the necessity of this restriction, and it might be something
+ * that can be removed in the future. For now, I have bypassed this
+ * error for NOT ENFORCED foreign key constraints.
+ *
+ * TODO: UPDATE: As per Alvaro Herrera, this feature has not been
+ * implemented for partitioned tables. I still need to investigate what
+ * additional changes, beyond removing the following error check, are
+ * required to support NOT VALID foreign key constraints on partitioned
+ * tables.
+ */
+ if (fkconstraint->is_enforced && fkconstraint->skip_validation &&
+ !fkconstraint->initially_valid)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot add NOT VALID foreign key on partitioned table \"%s\" referencing relation \"%s\"",
@@ -10442,7 +10469,7 @@ addFkConstraint(addFkConstraintSides fkside,
CONSTRAINT_FOREIGN,
fkconstraint->deferrable,
fkconstraint->initdeferred,
- true, /* Is Enforced */
+ fkconstraint->is_enforced,
fkconstraint->initially_valid,
parentConstr,
RelationGetRelid(rel),
@@ -10560,20 +10587,22 @@ addFkRecurseReferenced(Constraint *fkconstraint, Relation rel,
Oid parentDelTrigger, Oid parentUpdTrigger,
bool with_period)
{
- Oid deleteTriggerOid,
- updateTriggerOid;
+ Oid deleteTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
Assert(CheckRelationLockedByMe(pkrel, ShareRowExclusiveLock, true));
Assert(CheckRelationLockedByMe(rel, ShareRowExclusiveLock, true));
/*
- * Create the action triggers that enforce the constraint.
+ * Create action triggers to enforce the constraint, or skip them if the
+ * constraint is not enforced.
*/
- createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
- fkconstraint,
- parentConstr, indexOid,
- parentDelTrigger, parentUpdTrigger,
- &deleteTriggerOid, &updateTriggerOid);
+ if (fkconstraint->is_enforced)
+ createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
+ fkconstraint,
+ parentConstr, indexOid,
+ parentDelTrigger, parentUpdTrigger,
+ &deleteTriggerOid, &updateTriggerOid);
/*
* If the referenced table is partitioned, recurse on ourselves to handle
@@ -10694,8 +10723,8 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
Oid parentInsTrigger, Oid parentUpdTrigger,
bool with_period)
{
- Oid insertTriggerOid,
- updateTriggerOid;
+ Oid insertTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
Assert(OidIsValid(parentConstr));
Assert(CheckRelationLockedByMe(rel, ShareRowExclusiveLock, true));
@@ -10707,29 +10736,32 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
errmsg("foreign key constraints are not supported on foreign tables")));
/*
- * Add the check triggers to it and, if necessary, schedule it to be
- * checked in Phase 3.
+ * Add check triggers if the constraint is enforced, and if needed,
+ * schedule them to be checked in Phase 3.
*
* If the relation is partitioned, drill down to do it to its partitions.
*/
- createForeignKeyCheckTriggers(RelationGetRelid(rel),
- RelationGetRelid(pkrel),
- fkconstraint,
- parentConstr,
- indexOid,
- parentInsTrigger, parentUpdTrigger,
- &insertTriggerOid, &updateTriggerOid);
+ if (fkconstraint->is_enforced)
+ createForeignKeyCheckTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
+ fkconstraint,
+ parentConstr,
+ indexOid,
+ parentInsTrigger, parentUpdTrigger,
+ &insertTriggerOid, &updateTriggerOid);
if (rel->rd_rel->relkind == RELKIND_RELATION)
{
/*
* Tell Phase 3 to check that the constraint is satisfied by existing
- * rows. We can skip this during table creation, when requested
- * explicitly by specifying NOT VALID in an ADD FOREIGN KEY command,
- * and when we're recreating a constraint following a SET DATA TYPE
- * operation that did not impugn its validity.
+ * rows. We can skip this during table creation, when constraint is
+ * specified as NOT ENFORCED, when requested explicitly by specifying
+ * NOT VALID in an ADD FOREIGN KEY command, and when we're recreating
+ * a constraint following a SET DATA TYPE operation that did not
+ * impugn its validity.
*/
- if (wqueue && !old_check_ok && !fkconstraint->skip_validation)
+ if (wqueue && !old_check_ok && !fkconstraint->skip_validation &&
+ fkconstraint->is_enforced)
{
NewConstraint *newcon;
AlteredTableInfo *tab;
@@ -10964,8 +10996,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
AttrNumber confdelsetcols[INDEX_MAX_KEYS];
Constraint *fkconstraint;
ObjectAddress address;
- Oid deleteTriggerOid,
- updateTriggerOid;
+ Oid deleteTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(constrOid));
if (!HeapTupleIsValid(tuple))
@@ -11025,8 +11057,9 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
fkconstraint->fk_del_set_cols = NIL;
fkconstraint->old_conpfeqop = NIL;
fkconstraint->old_pktable_oid = InvalidOid;
+ fkconstraint->is_enforced = constrForm->conenforced;
fkconstraint->skip_validation = false;
- fkconstraint->initially_valid = true;
+ fkconstraint->initially_valid = constrForm->convalidated;
/* set up colnames that are used to generate the constraint name */
for (int i = 0; i < numfks; i++)
@@ -11054,9 +11087,10 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
* parent OIDs for similar triggers that will be created on the
* partition in addFkRecurseReferenced().
*/
- GetForeignKeyActionTriggers(trigrel, constrOid,
- constrForm->confrelid, constrForm->conrelid,
- &deleteTriggerOid, &updateTriggerOid);
+ if (constrForm->conenforced)
+ GetForeignKeyActionTriggers(trigrel, constrOid,
+ constrForm->confrelid, constrForm->conrelid,
+ &deleteTriggerOid, &updateTriggerOid);
/* Add this constraint ... */
address = addFkConstraint(addFkReferencedSide,
@@ -11222,17 +11256,18 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
mapped_conkey[i] = attmap->attnums[conkey[i] - 1];
/*
- * Get the "check" triggers belonging to the constraint to pass as
- * parent OIDs for similar triggers that will be created on the
- * partition in addFkRecurseReferencing(). They are also passed to
- * tryAttachPartitionForeignKey() below to simply assign as parents to
- * the partition's existing "check" triggers, that is, if the
- * corresponding constraints is deemed attachable to the parent
- * constraint.
+ * Get the "check" triggers belonging to the constraint, if it is
+ * enforced, to pass as parent OIDs for similar triggers that will be
+ * created on the partition in addFkRecurseReferencing(). They are
+ * also passed to tryAttachPartitionForeignKey() below to simply
+ * assign as parents to the partition's existing "check" triggers,
+ * that is, if the corresponding constraints is deemed attachable to
+ * the parent constraint.
*/
- GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
- constrForm->confrelid, constrForm->conrelid,
- &insertTriggerOid, &updateTriggerOid);
+ if (constrForm->conenforced)
+ GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
+ constrForm->confrelid, constrForm->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
/*
* Before creating a new constraint, see whether any existing FKs are
@@ -11284,6 +11319,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
fkconstraint->fk_del_set_cols = NIL;
fkconstraint->old_conpfeqop = NIL;
fkconstraint->old_pktable_oid = InvalidOid;
+ fkconstraint->is_enforced = constrForm->conenforced;
fkconstraint->skip_validation = false;
fkconstraint->initially_valid = constrForm->convalidated;
for (int i = 0; i < numfks; i++)
@@ -11368,8 +11404,6 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
ScanKeyData key;
SysScanDesc scan;
HeapTuple trigtup;
- Oid insertTriggerOid,
- updateTriggerOid;
parentConstrTup = SearchSysCache1(CONSTROID,
ObjectIdGetDatum(parentConstrOid));
@@ -11411,6 +11445,7 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
!partConstr->convalidated ||
partConstr->condeferrable != parentConstr->condeferrable ||
partConstr->condeferred != parentConstr->condeferred ||
+ partConstr->conenforced != parentConstr->conenforced ||
partConstr->confupdtype != parentConstr->confupdtype ||
partConstr->confdeltype != parentConstr->confdeltype ||
partConstr->confmatchtype != parentConstr->confmatchtype)
@@ -11470,18 +11505,25 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
ConstraintSetParentConstraint(fk->conoid, parentConstrOid, partRelid);
/*
- * Like the constraint, attach partition's "check" triggers to the
- * corresponding parent triggers.
+ * Similar to the constraint, attach the partition's "check" triggers to
+ * the corresponding parent triggers if the constraint is enforced.
+ * Non-enforced constraints do not have these triggers.
*/
- GetForeignKeyCheckTriggers(trigrel,
- fk->conoid, fk->confrelid, fk->conrelid,
- &insertTriggerOid, &updateTriggerOid);
- Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
- TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
- partRelid);
- Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
- TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
- partRelid);
+ if (fk->conenforced)
+ {
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
+ partRelid);
+ Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
+ partRelid);
+ }
/*
* If the referenced table is partitioned, then the partition we're
@@ -11695,8 +11737,8 @@ GetForeignKeyCheckTriggers(Relation trigrel,
* InvalidObjectAddress.
*/
static ObjectAddress
-ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
- bool recursing, LOCKMODE lockmode)
+ATExecAlterConstraint(List **wqueue, Relation rel, AlterTableCmd *cmd,
+ bool recurse, bool recursing, LOCKMODE lockmode)
{
Constraint *cmdcon;
Relation conrel;
@@ -11798,12 +11840,14 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
address = InvalidObjectAddress;
if (currcon->condeferrable != cmdcon->deferrable ||
currcon->condeferred != cmdcon->initdeferred ||
+ currcon->conenforced != cmdcon->is_enforced ||
rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
- if (ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, currcon->conrelid,
- currcon->confrelid, contuple, &otherrelids,
- lockmode, InvalidOid, InvalidOid,
- InvalidOid, InvalidOid))
+ if (ATExecAlterConstrRecurse(wqueue, cmdcon, conrel, tgrel,
+ currcon->conrelid, currcon->confrelid,
+ contuple, &otherrelids, lockmode,
+ InvalidOid, InvalidOid, InvalidOid,
+ InvalidOid))
ObjectAddressSet(address, ConstraintRelationId, currcon->oid);
}
@@ -11835,8 +11879,8 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
* but existing releases don't do that.)
*/
static bool
-ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
- const Oid fkrelid, const Oid pkrelid,
+ATExecAlterConstrRecurse(List **wqueue, Constraint *cmdcon, Relation conrel,
+ Relation tgrel, const Oid fkrelid, const Oid pkrelid,
HeapTuple contuple, List **otherrelids,
LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
Oid ReferencedParentUpdTrigger,
@@ -11845,19 +11889,18 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
{
Form_pg_constraint currcon;
Oid conoid;
+ Oid conrelid;
Oid refrelid;
bool changed = false;
- Relation rel;
/* since this function recurses, it could be driven to stack overflow */
check_stack_depth();
currcon = (Form_pg_constraint) GETSTRUCT(contuple);
conoid = currcon->oid;
+ conrelid = currcon->conrelid;
refrelid = currcon->confrelid;
- rel = table_open(currcon->conrelid, lockmode);
-
/*
* Update pg_constraint with the flags from cmdcon.
*
@@ -11865,7 +11908,8 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
* silently do nothing.
*/
if (currcon->condeferrable != cmdcon->deferrable ||
- currcon->condeferred != cmdcon->initdeferred)
+ currcon->condeferred != cmdcon->initdeferred ||
+ currcon->conenforced != cmdcon->is_enforced)
{
HeapTuple copyTuple;
Form_pg_constraint copy_con;
@@ -11874,6 +11918,17 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
copy_con->condeferrable = cmdcon->deferrable;
copy_con->condeferred = cmdcon->initdeferred;
+ copy_con->conenforced = cmdcon->is_enforced;
+
+ /*
+ * NB: The convalidated status is irrelevant when the constraint is set to
+ * NOT ENFORCED, but for consistency, it should be set accordingly.
+ * This ensures that if the constraint is later changed to ENFORCED, it
+ * will automatically be in the correct NOT VALID state.
+ */
+ if (!cmdcon->is_enforced)
+ copy_con->convalidated = false;
+
CatalogTupleUpdate(conrel, ©Tuple->t_self, copyTuple);
InvokeObjectPostAlterHook(ConstraintRelationId,
@@ -11883,38 +11938,187 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
changed = true;
/* Make new constraint flags visible to others */
- CacheInvalidateRelcache(rel);
+ CacheInvalidateRelcacheByRelid(conrelid);
+ }
+ if (currcon->conenforced != cmdcon->is_enforced)
+ {
+ ATExecAlterConstrEnforceability(wqueue, cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, contuple, otherrelids, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
+ }
+ else
+ {
/*
* Now we need to update the multiple entries in pg_trigger that
* implement the constraint.
*/
- AlterConstrTriggerDeferrability(conoid, tgrel, rel, cmdcon->deferrable,
- cmdcon->initdeferred, otherrelids);
+ AlterConstrTriggerDeferrability(conoid, tgrel, conrelid,
+ cmdcon->deferrable,
+ cmdcon->initdeferred,
+ otherrelids);
+
+ /*
+ * If the table at either end of the constraint is partitioned, we
+ * need to recurse and handle every constraint that is a child of this
+ * one.
+ *
+ * (This assumes that the recurse flag is forcibly set for partitioned
+ * tables, and not set for legacy inheritance, though we don't check
+ * for that here.)
+ */
+ if (get_rel_relkind(conrelid) == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)
+ ATExecAlterChildConstr(wqueue, cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, contuple, otherrelids, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
}
- /*
- * If the table at either end of the constraint is partitioned, we need to
- * recurse and handle every constraint that is a child of this one.
- *
- * (This assumes that the recurse flag is forcibly set for partitioned
- * tables, and not set for legacy inheritance, though we don't check for
- * that here.)
- */
- if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
- get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)
- ATExecAlterChildConstr(cmdcon, conrel, tgrel, fkrelid, pkrelid,
- contuple, otherrelids, lockmode,
- ReferencedParentDelTrigger,
- ReferencedParentUpdTrigger,
- ReferencingParentInsTrigger,
- ReferencingParentUpdTrigger);
-
- table_close(rel, NoLock);
-
return changed;
}
+/*
+ * A subroutine of ATExecAlterConstrRecurse that updates the enforceability of
+ * a foreign key constraint. Depending on whether the constraint is being set
+ * to enforced or not enforced, it creates or drops the trigger accordingly.
+ *
+ * The arguments to this function have the same meaning as the arguments to
+ * ATExecAlterConstrRecurse.
+ */
+static void
+ATExecAlterConstrEnforceability(List **wqueue, Constraint *cmdcon,
+ Relation conrel, Relation tgrel,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger)
+{
+ Form_pg_constraint currcon;
+ Oid conoid;
+
+ currcon = (Form_pg_constraint) GETSTRUCT(contuple);
+ conoid = currcon->oid;
+
+ /* Drop triggers */
+ if (!cmdcon->is_enforced)
+ {
+ HeapTuple tgtuple;
+ ScanKeyData tgkey;
+ SysScanDesc tgscan;
+
+ /*
+ * When setting a constraint to NOT ENFORCED, the constraint triggers
+ * need to be dropped. Therefore, we must process the child relations
+ * first, followed by the parent, to account for dependencies.
+ */
+ if (get_rel_relkind(currcon->conrelid) == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE)
+ ATExecAlterChildConstr(wqueue, cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, contuple, otherrelids, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
+
+ ScanKeyInit(&tgkey,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+ tgscan = systable_beginscan(tgrel, TriggerConstraintIndexId, true,
+ NULL, 1, &tgkey);
+ while (HeapTupleIsValid(tgtuple = systable_getnext(tgscan)))
+ {
+ Form_pg_trigger tgform = (Form_pg_trigger) GETSTRUCT(tgtuple);
+ ObjectAddress trigger;
+
+ /*
+ * Delete only RI_FKey_noaction_del, RI_FKey_noaction_upd,
+ * RI_FKey_check_ins and RI_FKey_check_upd triggers, but not
+ * others; see createForeignKeyActionTriggers and
+ * CreateFKCheckTrigger.
+ */
+ if (tgform->tgfoid != F_RI_FKEY_NOACTION_DEL &&
+ tgform->tgfoid != F_RI_FKEY_NOACTION_UPD &&
+ tgform->tgfoid != F_RI_FKEY_CHECK_INS &&
+ tgform->tgfoid != F_RI_FKEY_CHECK_UPD)
+ continue;
+
+ deleteDependencyRecordsFor(TriggerRelationId, tgform->oid,
+ false);
+ /* make dependency deletion visible to performDeletion */
+ CommandCounterIncrement();
+ ObjectAddressSet(trigger, TriggerRelationId, tgform->oid);
+ performDeletion(&trigger, DROP_RESTRICT, 0);
+ /* make trigger drop visible, in case the loop iterates */
+ CommandCounterIncrement();
+ }
+
+ systable_endscan(tgscan);
+ }
+ else /* Create triggers */
+ {
+ Relation rel;
+ Oid ReferencedDelTriggerOid = InvalidOid,
+ ReferencedUpdTriggerOid = InvalidOid,
+ ReferencingInsTriggerOid = InvalidOid,
+ ReferencingUpdTriggerOid = InvalidOid;
+
+ /* Update the foreign key action required for trigger creation. */
+ cmdcon->fk_matchtype = currcon->confmatchtype;
+ cmdcon->fk_upd_action = currcon->confupdtype;
+ cmdcon->fk_del_action = currcon->confdeltype;
+
+ rel = table_open(currcon->conrelid, lockmode);
+
+ /* Create referenced triggers */
+ if (currcon->conrelid == fkrelid)
+ createForeignKeyActionTriggers(rel,
+ currcon->confrelid,
+ cmdcon,
+ conoid,
+ currcon->conindid,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ &ReferencedDelTriggerOid,
+ &ReferencedUpdTriggerOid);
+
+ /* Create referencing triggers */
+ if (currcon->confrelid == pkrelid)
+ createForeignKeyCheckTriggers(currcon->conrelid,
+ pkrelid,
+ cmdcon,
+ conoid,
+ currcon->conindid,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger,
+ &ReferencingInsTriggerOid,
+ &ReferencingUpdTriggerOid);
+
+ /*
+ * If the table at either end of the constraint is partitioned, we
+ * need to recurse and create triggers for each constraint that is a
+ * child of this one.
+ */
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE)
+ ATExecAlterChildConstr(wqueue, cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, contuple, otherrelids, lockmode,
+ ReferencedDelTriggerOid,
+ ReferencedUpdTriggerOid,
+ ReferencingInsTriggerOid,
+ ReferencingUpdTriggerOid);
+ table_close(rel, NoLock);
+ }
+}
+
/*
* A subroutine of ATExecAlterConstrRecurse that updated constraint trigger's
* deferrability.
@@ -11923,7 +12127,7 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
* ATExecAlterConstrRecurse.
*/
static void
-AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
+AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Oid fkrel,
bool deferrable, bool initdeferred,
List **otherrelids)
{
@@ -11949,7 +12153,7 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
* other rels that don't have a trigger whose properties change, but
* let's be conservative.)
*/
- if (tgform->tgrelid != RelationGetRelid(rel))
+ if (tgform->tgrelid != fkrel)
*otherrelids = list_append_unique_oid(*otherrelids,
tgform->tgrelid);
@@ -11988,8 +12192,8 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
* ATExecAlterConstrRecurse.
*/
static void
-ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
- const Oid fkrelid, const Oid pkrelid,
+ATExecAlterChildConstr(List **wqueue, Constraint *cmdcon, Relation conrel,
+ Relation tgrel, const Oid fkrelid, const Oid pkrelid,
HeapTuple contuple, List **otherrelids,
LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
Oid ReferencedParentUpdTrigger,
@@ -12014,9 +12218,9 @@ ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
true, NULL, 1, &pkey);
while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
- ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, fkrelid, pkrelid,
- childtup, otherrelids, lockmode,
- ReferencedParentDelTrigger,
+ ATExecAlterConstrRecurse(wqueue, cmdcon, conrel, tgrel,
+ fkrelid, pkrelid, childtup, otherrelids,
+ lockmode, ReferencedParentDelTrigger,
ReferencedParentUpdTrigger,
ReferencingParentInsTrigger,
ReferencingParentUpdTrigger);
@@ -19946,8 +20150,6 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
Form_pg_constraint conform;
- Oid insertTriggerOid,
- updateTriggerOid;
contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
if (!HeapTupleIsValid(contup))
@@ -19970,17 +20172,25 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
/*
* Also, look up the partition's "check" triggers corresponding to the
- * constraint being detached and detach them from the parent triggers.
+ * enforced constraint being detached and detach them from the parent
+ * triggers. Non-enforced constraints do not have these triggers;
+ * therefore, this step is not needed.
*/
- GetForeignKeyCheckTriggers(trigrel,
- fk->conoid, fk->confrelid, fk->conrelid,
- &insertTriggerOid, &updateTriggerOid);
- Assert(OidIsValid(insertTriggerOid));
- TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
- RelationGetRelid(partRel));
- Assert(OidIsValid(updateTriggerOid));
- TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
- RelationGetRelid(partRel));
+ if (fk->conenforced)
+ {
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ Assert(OidIsValid(updateTriggerOid));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ }
/*
* Lastly, create the action triggers on the referenced table, using
@@ -20020,8 +20230,9 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
fkconstraint->conname = pstrdup(NameStr(conform->conname));
fkconstraint->deferrable = conform->condeferrable;
fkconstraint->initdeferred = conform->condeferred;
+ fkconstraint->is_enforced = conform->conenforced;
fkconstraint->skip_validation = true;
- fkconstraint->initially_valid = true;
+ fkconstraint->initially_valid = conform->convalidated;
/* a few irrelevant fields omitted here */
fkconstraint->pktable = NULL;
fkconstraint->fk_attrs = NIL;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 833b3be02be..e8119d90914 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -2660,7 +2660,7 @@ alter_table_cmd:
processCASbits($4, @4, "ALTER CONSTRAINT statement",
&c->deferrable,
&c->initdeferred,
- NULL, NULL, NULL, yyscanner);
+ &c->is_enforced, NULL, NULL, yyscanner);
$$ = (Node *) n;
}
/* ALTER TABLE <name> VALIDATE CONSTRAINT ... */
@@ -4027,6 +4027,7 @@ ColConstraintElem:
n->fk_del_set_cols = ($5)->deleteAction->cols;
n->skip_validation = false;
n->initially_valid = true;
+ n->is_enforced = true;
$$ = (Node *) n;
}
;
@@ -4302,7 +4303,7 @@ ConstraintElem:
n->fk_del_set_cols = ($11)->deleteAction->cols;
processCASbits($12, @12, "FOREIGN KEY",
&n->deferrable, &n->initdeferred,
- NULL, &n->skip_validation,
+ &n->is_enforced, &n->skip_validation,
NULL,
yyscanner);
n->initially_valid = !n->skip_validation;
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 5ab44149e59..7f41c9de256 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -2961,8 +2961,10 @@ transformFKConstraints(CreateStmtContext *cxt,
/*
* If CREATE TABLE or adding a column with NULL default, we can safely
- * skip validation of FK constraints, and nonetheless mark them valid.
- * (This will override any user-supplied NOT VALID flag.)
+ * skip validation of FK constraints, and mark them as valid based on the
+ * constraint enforcement flag, since NOT ENFORCED constraints must always
+ * be marked as NOT VALID. (This will override any user-supplied NOT VALID
+ * flag.)
*/
if (skipValidation)
{
@@ -2971,7 +2973,7 @@ transformFKConstraints(CreateStmtContext *cxt,
Constraint *constraint = (Constraint *) lfirst(fkclist);
constraint->skip_validation = true;
- constraint->initially_valid = true;
+ constraint->initially_valid = constraint->is_enforced;
}
}
@@ -3966,7 +3968,8 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
case CONSTR_ATTR_ENFORCED:
if (lastprimarycon &&
- lastprimarycon->contype != CONSTR_CHECK)
+ lastprimarycon->contype != CONSTR_CHECK &&
+ lastprimarycon->contype != CONSTR_FOREIGN)
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("misplaced ENFORCED clause"),
@@ -3982,7 +3985,8 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
case CONSTR_ATTR_NOT_ENFORCED:
if (lastprimarycon &&
- lastprimarycon->contype != CONSTR_CHECK)
+ lastprimarycon->contype != CONSTR_CHECK &&
+ lastprimarycon->contype != CONSTR_FOREIGN)
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("misplaced NOT ENFORCED clause"),
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 0809f03aca2..0efbcca190d 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4700,6 +4700,7 @@ RelationGetFKeyList(Relation relation)
info->conoid = constraint->oid;
info->conrelid = constraint->conrelid;
info->confrelid = constraint->confrelid;
+ info->conenforced = constraint->conenforced;
DeconstructFkConstraintRow(htup, &info->nkeys,
info->conkey,
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 87002049538..2db011ddfb7 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -284,6 +284,9 @@ typedef struct ForeignKeyCacheInfo
/* number of columns in the foreign key */
int nkeys;
+ /* Is enforced ? */
+ bool conenforced;
+
/*
* these arrays each have nkeys valid entries:
*/
diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out
index b68ac47a3cd..c0b6f204223 100644
--- a/src/test/regress/expected/alter_table.out
+++ b/src/test/regress/expected/alter_table.out
@@ -514,7 +514,7 @@ DELETE FROM attmp3 WHERE NOT b > 10;
ALTER TABLE attmp3 VALIDATE CONSTRAINT b_greater_than_ten; -- succeeds
ALTER TABLE attmp3 VALIDATE CONSTRAINT b_greater_than_ten; -- succeeds
ALTER TABLE attmp3 VALIDATE CONSTRAINT b_greater_than_ten_not_enforced; -- fail
-ERROR: cannot validated NOT ENFORCED constraint
+ERROR: cannot validate NOT ENFORCED constraint
-- Test inherited NOT VALID CHECK constraints
select * from attmp3;
a | b
diff --git a/src/test/regress/expected/constraints.out b/src/test/regress/expected/constraints.out
index 692a69fe457..f8767c5cacf 100644
--- a/src/test/regress/expected/constraints.out
+++ b/src/test/regress/expected/constraints.out
@@ -746,13 +746,9 @@ LINE 1: CREATE TABLE UNIQUE_NOTEN_TBL(i int UNIQUE NOT ENFORCED);
^
-- XXX: error message is misleading here
ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
-ERROR: ALTER CONSTRAINT statement constraints cannot be marked ENFORCED
-LINE 1: ...TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
- ^
+ERROR: constraint "unique_tbl_i_key" of relation "unique_tbl" is not a foreign key constraint
ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORCED;
-ERROR: ALTER CONSTRAINT statement constraints cannot be marked NOT ENFORCED
-LINE 1: ...ABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORC...
- ^
+ERROR: constraint "unique_tbl_i_key" of relation "unique_tbl" is not a foreign key constraint
DROP TABLE unique_tbl;
--
-- EXCLUDE constraints
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 3f459f70ac1..d448af38454 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -1,12 +1,44 @@
--
-- FOREIGN KEY
--
--- MATCH FULL
+-- NOT ENFORCED
--
-- First test, check and cascade
--
CREATE TABLE PKTABLE ( ptest1 int PRIMARY KEY, ptest2 text );
-CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE, ftest2 int );
+CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE NOT ENFORCED,
+ ftest2 int );
+-- Inserting into the foreign key table will not result in an error, even if
+-- there is no matching key in the referenced table.
+INSERT INTO FKTABLE VALUES (1, 2);
+INSERT INTO FKTABLE VALUES (2, 3);
+-- Check FKTABLE
+SELECT * FROM FKTABLE;
+ ftest1 | ftest2
+--------+--------
+ 1 | 2
+ 2 | 3
+(2 rows)
+
+-- Changing it back to ENFORCED will leave the constraint in the NOT VALID state
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+-- Which needs to be explicitly validated.
+ALTER TABLE FKTABLE VALIDATE CONSTRAINT fktable_ftest1_fkey;
+ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_fkey"
+DETAIL: Key (ftest1)=(1) is not present in table "pktable".
+-- Remove any existing rows that violate the constraint, then attempt to change
+-- it.
+TRUNCATE FKTABLE;
+ALTER TABLE FKTABLE VALIDATE CONSTRAINT fktable_ftest1_fkey;
+-- Any further inserts will fail due to the enforcement.
+INSERT INTO FKTABLE VALUES (3, 4);
+ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_fkey"
+DETAIL: Key (ftest1)=(3) is not present in table "pktable".
+--
+-- MATCH FULL
+--
+-- First test, check and cascade
+--
-- Insert test data into PKTABLE
INSERT INTO PKTABLE VALUES (1, 'Test1');
INSERT INTO PKTABLE VALUES (2, 'Test2');
@@ -351,6 +383,8 @@ INSERT INTO FKTABLE VALUES (1, NULL);
ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL;
ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_ftest2_fkey"
DETAIL: MATCH FULL does not allow mixing of null and nonnull key values.
+-- NOT ENFORCED will bypass the initial check.
+ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL NOT ENFORCED;
DROP TABLE FKTABLE;
DROP TABLE PKTABLE;
-- MATCH SIMPLE
@@ -1276,13 +1310,28 @@ INSERT INTO fktable VALUES (0, 20);
ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_fk_fkey"
DETAIL: Key (fk)=(20) is not present in table "pktable".
COMMIT;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT ENFORCED;
+BEGIN;
+-- doesn't match FK, no error.
+UPDATE pktable SET id = 10 WHERE id = 5;
+-- doesn't match PK, no error.
+INSERT INTO fktable VALUES (0, 20);
+ROLLBACK;
-- try additional syntax
-ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE ENFORCED;
-- illegal option
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE INITIALLY DEFERRED;
ERROR: constraint declared INITIALLY DEFERRED must be DEFERRABLE
LINE 1: ...e ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE INITIALLY ...
^
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORCED;
+ERROR: conflicting constraint properties
+LINE 1: ...fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORC...
+ ^
+CREATE TEMP TABLE fktable2 (fk int references pktable ENFORCED NOT ENFORCED);
+ERROR: multiple ENFORCED/NOT ENFORCED clauses not allowed
+LINE 1: ...ABLE fktable2 (fk int references pktable ENFORCED NOT ENFORC...
+ ^
-- test order of firing of FK triggers when several RI-induced changes need to
-- be made to the same row. This was broken by subtransaction-related
-- changes in 8.0.
@@ -1580,7 +1629,8 @@ ALTER TABLE fk_partitioned_fk DROP COLUMN fdrop1;
CREATE TABLE fk_partitioned_fk_1 (fdrop1 int, fdrop2 int, a int, fdrop3 int, b int);
ALTER TABLE fk_partitioned_fk_1 DROP COLUMN fdrop1, DROP COLUMN fdrop2, DROP COLUMN fdrop3;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FROM (0,0) TO (1000,1000);
-ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk;
+ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
@@ -1663,7 +1713,38 @@ DETAIL: Key (a, b)=(2500, 2502) is still referenced from table "fk_partitioned_
Indexes:
"fk_notpartitioned_pk_pkey" PRIMARY KEY, btree (a, b)
Referenced by:
- TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b)
+ TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) NOT VALID
+
+-- Check the exsting FK trigger
+CREATE TEMP TABLE tmp_trg_info AS SELECT tgtype, tgrelid::regclass, tgconstraint
+FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+SELECT count(1) FROM tmp_trg_info;
+ count
+-------
+ 14
+(1 row)
+
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+-- No triggers
+SELECT count(1) FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+ count
+-------
+ 0
+(1 row)
+
+-- Changing it back to ENFORCED will recreate the necessary triggers.
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+-- Should be exactly the same number of triggers found as before
+SELECT COUNT(1) FROM pg_trigger pgt JOIN tmp_trg_info tt
+ON (pgt.tgtype = tt.tgtype AND pgt.tgrelid = tt.tgrelid AND pgt.tgconstraint = tt.tgconstraint);
+ count
+-------
+ 14
+(1 row)
ALTER TABLE fk_partitioned_fk DROP CONSTRAINT fk_partitioned_fk_a_b_fkey;
-- done.
@@ -1868,6 +1949,41 @@ Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
Foreign-key constraints:
TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
+DROP TABLE fk_partitioned_fk_2;
+CREATE TABLE fk_partitioned_fk_2 (b int, a int,
+ FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
+BEGIN;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have two constraints
+\d fk_partitioned_fk_2
+ Table "public.fk_partitioned_fk_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ b | integer | | |
+ a | integer | | |
+Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
+Foreign-key constraints:
+ "fk_partitioned_fk_2_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED
+ TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
+
+DROP TABLE fk_partitioned_fk_2;
+ROLLBACK;
+BEGIN;
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have only one constraint
+\d fk_partitioned_fk_2
+ Table "public.fk_partitioned_fk_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ b | integer | | |
+ a | integer | | |
+Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
+Foreign-key constraints:
+ "fk_partitioned_fk_2_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED
+ TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED
+
+ROLLBACK;
DROP TABLE fk_partitioned_fk_2;
CREATE TABLE fk_partitioned_fk_4 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE) PARTITION BY RANGE (b, a);
CREATE TABLE fk_partitioned_fk_4_1 PARTITION OF fk_partitioned_fk_4 FOR VALUES FROM (1,1) TO (100,100);
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index 2e710e419c2..9b23fe8cf8f 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -2,13 +2,40 @@
-- FOREIGN KEY
--
--- MATCH FULL
+-- NOT ENFORCED
--
-- First test, check and cascade
--
CREATE TABLE PKTABLE ( ptest1 int PRIMARY KEY, ptest2 text );
-CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE, ftest2 int );
+CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE NOT ENFORCED,
+ ftest2 int );
+-- Inserting into the foreign key table will not result in an error, even if
+-- there is no matching key in the referenced table.
+INSERT INTO FKTABLE VALUES (1, 2);
+INSERT INTO FKTABLE VALUES (2, 3);
+
+-- Check FKTABLE
+SELECT * FROM FKTABLE;
+
+-- Changing it back to ENFORCED will leave the constraint in the NOT VALID state
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+-- Which needs to be explicitly validated.
+ALTER TABLE FKTABLE VALIDATE CONSTRAINT fktable_ftest1_fkey;
+
+-- Remove any existing rows that violate the constraint, then attempt to change
+-- it.
+TRUNCATE FKTABLE;
+ALTER TABLE FKTABLE VALIDATE CONSTRAINT fktable_ftest1_fkey;
+
+-- Any further inserts will fail due to the enforcement.
+INSERT INTO FKTABLE VALUES (3, 4);
+
+--
+-- MATCH FULL
+--
+-- First test, check and cascade
+--
-- Insert test data into PKTABLE
INSERT INTO PKTABLE VALUES (1, 'Test1');
INSERT INTO PKTABLE VALUES (2, 'Test2');
@@ -230,6 +257,9 @@ INSERT INTO FKTABLE VALUES (1, NULL);
ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL;
+-- NOT ENFORCED will bypass the initial check.
+ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL NOT ENFORCED;
+
DROP TABLE FKTABLE;
DROP TABLE PKTABLE;
@@ -968,10 +998,23 @@ INSERT INTO fktable VALUES (0, 20);
COMMIT;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT ENFORCED;
+
+BEGIN;
+
+-- doesn't match FK, no error.
+UPDATE pktable SET id = 10 WHERE id = 5;
+-- doesn't match PK, no error.
+INSERT INTO fktable VALUES (0, 20);
+
+ROLLBACK;
+
-- try additional syntax
-ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE ENFORCED;
-- illegal option
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORCED;
+CREATE TEMP TABLE fktable2 (fk int references pktable ENFORCED NOT ENFORCED);
-- test order of firing of FK triggers when several RI-induced changes need to
-- be made to the same row. This was broken by subtransaction-related
@@ -1182,7 +1225,8 @@ ALTER TABLE fk_partitioned_fk DROP COLUMN fdrop1;
CREATE TABLE fk_partitioned_fk_1 (fdrop1 int, fdrop2 int, a int, fdrop3 int, b int);
ALTER TABLE fk_partitioned_fk_1 DROP COLUMN fdrop1, DROP COLUMN fdrop2, DROP COLUMN fdrop3;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FROM (0,0) TO (1000,1000);
-ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk;
+ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
@@ -1236,6 +1280,27 @@ UPDATE fk_notpartitioned_pk SET b = 1502 WHERE a = 1500;
UPDATE fk_notpartitioned_pk SET b = 2504 WHERE a = 2500;
-- check psql behavior
\d fk_notpartitioned_pk
+
+-- Check the exsting FK trigger
+CREATE TEMP TABLE tmp_trg_info AS SELECT tgtype, tgrelid::regclass, tgconstraint
+FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+SELECT count(1) FROM tmp_trg_info;
+
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+-- No triggers
+SELECT count(1) FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+
+-- Changing it back to ENFORCED will recreate the necessary triggers.
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+
+-- Should be exactly the same number of triggers found as before
+SELECT COUNT(1) FROM pg_trigger pgt JOIN tmp_trg_info tt
+ON (pgt.tgtype = tt.tgtype AND pgt.tgrelid = tt.tgrelid AND pgt.tgconstraint = tt.tgconstraint);
+
ALTER TABLE fk_partitioned_fk DROP CONSTRAINT fk_partitioned_fk_a_b_fkey;
-- done.
DROP TABLE fk_notpartitioned_pk, fk_partitioned_fk;
@@ -1372,6 +1437,22 @@ ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN
\d fk_partitioned_fk_2
DROP TABLE fk_partitioned_fk_2;
+CREATE TABLE fk_partitioned_fk_2 (b int, a int,
+ FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
+BEGIN;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have two constraints
+\d fk_partitioned_fk_2
+DROP TABLE fk_partitioned_fk_2;
+ROLLBACK;
+BEGIN;
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have only one constraint
+\d fk_partitioned_fk_2
+ROLLBACK;
+DROP TABLE fk_partitioned_fk_2;
+
CREATE TABLE fk_partitioned_fk_4 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE) PARTITION BY RANGE (b, a);
CREATE TABLE fk_partitioned_fk_4_1 PARTITION OF fk_partitioned_fk_4 FOR VALUES FROM (1,1) TO (100,100);
CREATE TABLE fk_partitioned_fk_4_2 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE SET NULL);
--
2.43.5
hi.
accidentally hit segfault.
create table c11 (a int not enforced);
create table c11 (a int enforced);
we can solve it via the following or changing SUPPORTS_ATTRS accordingly.
diff --git a/src/backend/parser/parse_utilcmd.c
b/src/backend/parser/parse_utilcmd.c
index 5ab44149e5..fe1116c092 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -3965,7 +3965,7 @@ transformConstraintAttrs(CreateStmtContext *cxt,
List *constraintList)
break;
case CONSTR_ATTR_ENFORCED:
- if (lastprimarycon &&
+ if (lastprimarycon == NULL ||
lastprimarycon->contype != CONSTR_CHECK)
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
@@ -3981,7 +3981,7 @@ transformConstraintAttrs(CreateStmtContext *cxt,
List *constraintList)
break;
case CONSTR_ATTR_NOT_ENFORCED:
- if (lastprimarycon &&
+ if (lastprimarycon == NULL ||
lastprimarycon->contype != CONSTR_CHECK)
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
ALTER DOMAIN constraint_comments_dom ADD CONSTRAINT the_constraint
CHECK (value > 0) NOT ENFORCED;
ERROR: CHECK constraints cannot be marked NOT ENFORCED
the error message is not good? maybe better option would be:
ERROR: DOMAIN CHECK constraints cannot be marked NOT ENFORCED
we can do it like:
index 833b3be02b..4a7ab0c2a3 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -4342,7 +4342,7 @@ DomainConstraintElem:
n->location = @1;
n->raw_expr = $3;
n->cooked_expr = NULL;
- processCASbits($5, @5, "CHECK",
+ processCASbits($5, @5, "DOMAIN CHECK",
NULL, NULL, NULL, &n->skip_validation,
&n->is_no_inherit, yyscanner);
On 2024-Dec-03, Peter Eisentraut wrote:
The handling of merging check constraints seems incomplete. What
should be the behavior of this:=> create table p1 (a int check (a > 0) not enforced);
CREATE TABLE
=> create table c1 (a int check (a > 0) enforced) inherits (p1);
CREATE TABLE
Hmm. Because the constraints are unnamed, and the chosen names are
different, I don't think they should be merged; I tried with 0001 in
place, and I think it does the right thing. If c1's creation specifies
a name that matches the parent name, we get this:
55432 18devel 61349=# create table c1 (a int constraint p1_a_check check (a > 0)) inherits (p1);
NOTICE: merging column "a" with inherited definition
ERROR: constraint "p1_a_check" conflicts with NOT VALID constraint on relation "c1"
I think this is bogus on two counts. First, NOT VALID has nowhere been
specified, so the error shouldn't be about that. But second, the child
should have the constraint marked as enforced as requested, and marked
as conislocal=t, coninhcount=1; the user can turn it into NOT ENFORCED
if they want, and no expectation breaks, because the parent is also
already marked NOT ENFORCED.
The other way around shall not be accepted: if the parent has it as
ENFORCED, then the child is not allowed to have it as NOT ENFORCED,
neither during creation nor during ALTER TABLE. The only way to mark
c1's constraint as NOT ENFORCED is to mark p1's constraint as NOINHERIT,
so that c1's constraint's inhcount becomes 0. Then, the constraint has
no parent with an enforced constraint, so it's okay to mark it as not
enforced.
Or this?
=> create table p2 (a int check (a > 0) enforced);
CREATE TABLE
=> create table c2 () inherits (p1, p2);
CREATE TABLEShould we catch these and error?
Here we end up with constraints p1_a_check and p2_a_check, which have
identical definitions except the NOT ENFORCED bits differ. I think this
is okay, since we don't attempt to match these constraints when the
names differ. If both parents had the constraint with the same name, we
should try to consider them as one and merge them. In that case, c2's
constraint inhcount should be 2, and at least one of the parent
constraints is marked enforced, so the child shall have it as enforce
also. Trying to mark c2's constraint as NOT ENFORCED shall give an
error because it inherits from p2. But if you deinherit from p2, or
mark the constraint in p2 as NOINHERIT, then c2's constraint can become
NOT ENFORCE if the user asks for it.
--
Álvaro Herrera PostgreSQL Developer — https://www.EnterpriseDB.com/
On Thu, Dec 5, 2024 at 11:02 AM jian he <jian.universality@gmail.com> wrote:
hi.
accidentally hit segfault.
create table c11 (a int not enforced);
create table c11 (a int enforced);
we can solve it via the following or changing SUPPORTS_ATTRS accordingly.diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c index 5ab44149e5..fe1116c092 100644 --- a/src/backend/parser/parse_utilcmd.c +++ b/src/backend/parser/parse_utilcmd.c @@ -3965,7 +3965,7 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList) break; case CONSTR_ATTR_ENFORCED: - if (lastprimarycon && + if (lastprimarycon == NULL || lastprimarycon->contype != CONSTR_CHECK) ereport(ERROR,(errcode(ERRCODE_SYNTAX_ERROR), @@ -3981,7 +3981,7 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList) break; case CONSTR_ATTR_NOT_ENFORCED: - if (lastprimarycon && + if (lastprimarycon == NULL || lastprimarycon->contype != CONSTR_CHECK) ereport(ERROR,(errcode(ERRCODE_SYNTAX_ERROR),
Yes, that was a logical oversight on my part. Your suggestion looks
good to me, thanks.
ALTER DOMAIN constraint_comments_dom ADD CONSTRAINT the_constraint
CHECK (value > 0) NOT ENFORCED;
ERROR: CHECK constraints cannot be marked NOT ENFORCEDthe error message is not good? maybe better option would be:
ERROR: DOMAIN CHECK constraints cannot be marked NOT ENFORCEDwe can do it like: index 833b3be02b..4a7ab0c2a3 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -4342,7 +4342,7 @@ DomainConstraintElem: n->location = @1; n->raw_expr = $3; n->cooked_expr = NULL; - processCASbits($5, @5, "CHECK", + processCASbits($5, @5, "DOMAIN CHECK",NULL, NULL, NULL, &n->skip_validation,
&n->is_no_inherit, yyscanner);
I believe this should either be a separate patch or potentially
included in your "Refactor AlterDomainAddConstraint" proposal[1].
Regards,
Amul
1] /messages/by-id/CACJufxHitd5LGLBSSAPShhtDWxT0ViVKTHinkYW-skBX93TcpA@mail.gmail.com
On Thu, Dec 5, 2024 at 4:40 PM Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
On 2024-Dec-03, Peter Eisentraut wrote:
The handling of merging check constraints seems incomplete. What
should be the behavior of this:=> create table p1 (a int check (a > 0) not enforced);
CREATE TABLE
=> create table c1 (a int check (a > 0) enforced) inherits (p1);
CREATE TABLEHmm. Because the constraints are unnamed, and the chosen names are
different, I don't think they should be merged; I tried with 0001 in
place, and I think it does the right thing. If c1's creation specifies
a name that matches the parent name, we get this:55432 18devel 61349=# create table c1 (a int constraint p1_a_check check (a > 0)) inherits (p1);
NOTICE: merging column "a" with inherited definition
ERROR: constraint "p1_a_check" conflicts with NOT VALID constraint on relation "c1"I think this is bogus on two counts. First, NOT VALID has nowhere been
specified, so the error shouldn't be about that. But second, the child
should have the constraint marked as enforced as requested, and marked
as conislocal=t, coninhcount=1; the user can turn it into NOT ENFORCED
if they want, and no expectation breaks, because the parent is also
already marked NOT ENFORCED.The other way around shall not be accepted: if the parent has it as
ENFORCED, then the child is not allowed to have it as NOT ENFORCED,
neither during creation nor during ALTER TABLE. The only way to mark
c1's constraint as NOT ENFORCED is to mark p1's constraint as NOINHERIT,
so that c1's constraint's inhcount becomes 0. Then, the constraint has
no parent with an enforced constraint, so it's okay to mark it as not
enforced.
Makes sense, agreed.
Or this?
=> create table p2 (a int check (a > 0) enforced);
CREATE TABLE
=> create table c2 () inherits (p1, p2);
CREATE TABLEShould we catch these and error?
Here we end up with constraints p1_a_check and p2_a_check, which have
identical definitions except the NOT ENFORCED bits differ. I think this
is okay, since we don't attempt to match these constraints when the
names differ. If both parents had the constraint with the same name, we
should try to consider them as one and merge them. In that case, c2's
constraint inhcount should be 2, and at least one of the parent
constraints is marked enforced, so the child shall have it as enforce
also. Trying to mark c2's constraint as NOT ENFORCED shall give an
error because it inherits from p2. But if you deinherit from p2, or
mark the constraint in p2 as NOINHERIT, then c2's constraint can become
NOT ENFORCE if the user asks for it.
Agreed to this as well. I have made the changes to align with the
suggested behavior in the attached version. Thank you.
Regards,
Amul
Attachments:
v7-0001-Add-support-for-NOT-ENFORCED-in-CHECK-constraints.patchapplication/octet-stream; name=v7-0001-Add-support-for-NOT-ENFORCED-in-CHECK-constraints.patchDownload
From 0333196d40ecb9fedb4b8f5e3e72e51c0eb7e567 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Wed, 4 Dec 2024 10:24:40 +0530
Subject: [PATCH v7 1/5] Add support for NOT ENFORCED in CHECK constraints.
This added basic infrastructure for the NOT ENFORCED/ENFORCED
constraint supports where incluing grammar and catalog flags.
Note that CHECK constraints do not currently support ALTER operations,
so changing the enforceability of an existing constraint isn't
possible without dropping and recreating it. We could consider
adding support for altering CHECK constraints either in this
patch series or as a separatly.
---
doc/src/sgml/catalogs.sgml | 10 ++
doc/src/sgml/ddl.sgml | 8 +-
doc/src/sgml/information_schema.sgml | 4 +-
doc/src/sgml/ref/alter_table.sgml | 12 +-
doc/src/sgml/ref/create_table.sgml | 25 ++++-
src/backend/access/common/tupdesc.c | 2 +
src/backend/catalog/heap.c | 56 ++++++++--
src/backend/catalog/index.c | 1 +
src/backend/catalog/information_schema.sql | 2 +-
src/backend/catalog/pg_constraint.c | 7 ++
src/backend/commands/tablecmds.c | 50 ++++++++-
src/backend/commands/trigger.c | 1 +
src/backend/commands/typecmds.c | 16 +++
src/backend/executor/execMain.c | 8 +-
src/backend/nodes/makefuncs.c | 1 +
src/backend/optimizer/util/plancat.c | 12 ++
src/backend/parser/gram.y | 104 ++++++++++++++----
src/backend/parser/parse_utilcmd.c | 53 ++++++++-
src/backend/utils/adt/ruleutils.c | 6 +-
src/backend/utils/cache/relcache.c | 1 +
src/include/access/tupdesc.h | 1 +
src/include/catalog/catversion.h | 2 +-
src/include/catalog/heap.h | 1 +
src/include/catalog/pg_constraint.h | 2 +
src/include/nodes/parsenodes.h | 3 +
src/include/parser/kwlist.h | 1 +
src/test/regress/expected/alter_table.out | 3 +
src/test/regress/expected/constraints.out | 40 ++++++-
.../regress/expected/create_table_like.out | 10 +-
src/test/regress/expected/domain.out | 15 +++
src/test/regress/expected/inherit.out | 94 ++++++++++++++--
src/test/regress/sql/alter_table.sql | 2 +
src/test/regress/sql/constraints.sql | 22 +++-
src/test/regress/sql/create_table_like.sql | 3 +-
src/test/regress/sql/domain.sql | 8 ++
src/test/regress/sql/inherit.sql | 47 +++++++-
36 files changed, 563 insertions(+), 70 deletions(-)
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index bf3cee08a93..a56ef4df562 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2602,6 +2602,16 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l
</para></entry>
</row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>conenforced</structfield> <type>bool</type>
+ </para>
+ <para>
+ Is the constraint enforceable?
+ Currently, can be false only for CHECK constraints
+ </para></entry>
+ </row>
+
<row>
<entry role="catalog_table_entry"><para role="column_definition">
<structfield>convalidated</structfield> <type>bool</type>
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index dea04d64db6..9a20396dae4 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -584,7 +584,10 @@ CREATE TABLE products (
consists of the key word <literal>CHECK</literal> followed by an
expression in parentheses. The check constraint expression should
involve the column thus constrained, otherwise the constraint
- would not make too much sense.
+ would not make too much sense. The check constraint expression can be
+ followed by the <literal>ENFORCED</literal>, which is the default, or
+ <literal>NOT ENFORCED</literal> keyword, determining whether the
+ constraint check is performed or skipped after each statement.
</para>
<indexterm>
@@ -720,7 +723,8 @@ CREATE TABLE products (
to implement that. (This approach avoids the dump/restore problem because
<application>pg_dump</application> does not reinstall triggers until after
restoring data, so that the check will not be enforced during a
- dump/restore.)
+ dump/restore.) You can add that CHECK constraint with <literal>NOT ENFORCED</literal>,
+ allowing the underlying constraint rule to be represented without being enforced.
</para>
</note>
diff --git a/doc/src/sgml/information_schema.sgml b/doc/src/sgml/information_schema.sgml
index 9442b0718c0..19dffe7be6a 100644
--- a/doc/src/sgml/information_schema.sgml
+++ b/doc/src/sgml/information_schema.sgml
@@ -6896,9 +6896,7 @@ ORDER BY c.ordinal_position;
<structfield>enforced</structfield> <type>yes_or_no</type>
</para>
<para>
- Applies to a feature not available in
- <productname>PostgreSQL</productname> (currently always
- <literal>YES</literal>)
+ <literal>YES</literal> if the constraint is enforced, <literal>NO</literal> if not
</para></entry>
</row>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index c8f7ab7d956..938450fba18 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -108,7 +108,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
PRIMARY KEY <replaceable class="parameter">index_parameters</replaceable> |
REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> ) ] [ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ]
[ ON DELETE <replaceable class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ] }
-[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]
<phrase>and <replaceable class="parameter">table_constraint</replaceable> is:</phrase>
@@ -120,7 +120,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
EXCLUDE [ USING <replaceable class="parameter">index_method</replaceable> ] ( <replaceable class="parameter">exclude_element</replaceable> WITH <replaceable class="parameter">operator</replaceable> [, ... ] ) <replaceable class="parameter">index_parameters</replaceable> [ WHERE ( <replaceable class="parameter">predicate</replaceable> ) ] |
FOREIGN KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">column_name</replaceable> ] ) REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">refcolumn</replaceable> ] ) ]
[ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ] [ ON DELETE <replaceable class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ] }
-[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]
<phrase>and <replaceable class="parameter">table_constraint_using_index</replaceable> is:</phrase>
@@ -1423,9 +1423,11 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</para>
<para>
- Adding a <literal>CHECK</literal> or <literal>NOT NULL</literal> constraint requires
- scanning the table to verify that existing rows meet the constraint,
- but does not require a table rewrite.
+ Adding an enforced <literal>CHECK</literal> or <literal>NOT NULL</literal>
+ constraint requires scanning the table to verify that existing rows meet the
+ constraint, but does not require a table rewrite. If a <literal>CHECK</literal>
+ constraint is added as <literal>NOT ENFORCED</literal>, the validation will
+ not be performed.
</para>
<para>
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index 70fa929caa4..cfd01c4ef2b 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -71,7 +71,7 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI
PRIMARY KEY <replaceable class="parameter">index_parameters</replaceable> |
REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> ) ] [ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ]
[ ON DELETE <replaceable class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ] }
-[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]
<phrase>and <replaceable class="parameter">table_constraint</replaceable> is:</phrase>
@@ -84,7 +84,7 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI
FOREIGN KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">column_name</replaceable> ] ) REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">refcolumn</replaceable> ] ) ]
[ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ] [ ON DELETE <replaceable
class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ] }
-[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]
<phrase>and <replaceable class="parameter">like_option</replaceable> is:</phrase>
@@ -1377,6 +1377,27 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</listitem>
</varlistentry>
+ <varlistentry id="sql-createtable-parms-enforce">
+ <term><literal>ENFORCED</literal></term>
+ <term><literal>NOT ENFORCED</literal></term>
+ <listitem>
+ <para>
+ This is currently only allowed for <literal>CHECK</literal> constraints.
+ If the constraint is <literal>NOT ENFORCED</literal>, this clause
+ specifies that the constraint check will be skipped. When the constraint
+ is <literal>ENFORCED</literal>, check is performed after each statement;
+ the latter is the default.
+ </para>
+ <para>
+ Note that if the constraint is <literal>NOT ENFORCED</literal>, it is also
+ marked as <literal>NOT VALID</literal>. While this might seem unnecessary,
+ it is done for consistency and should be set accordingly. This ensures
+ that if the constraint is later changed to <literal>ENFORCED</literal>, it
+ will automatically be in the correct <literal>NOT VALID</literal> state.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="sql-createtable-method">
<term><literal>USING <replaceable class="parameter">method</replaceable></literal></term>
<listitem>
diff --git a/src/backend/access/common/tupdesc.c b/src/backend/access/common/tupdesc.c
index 47379fef220..6c64e5b9e71 100644
--- a/src/backend/access/common/tupdesc.c
+++ b/src/backend/access/common/tupdesc.c
@@ -224,6 +224,7 @@ CreateTupleDescCopyConstr(TupleDesc tupdesc)
{
cpy->check[i].ccname = pstrdup(constr->check[i].ccname);
cpy->check[i].ccbin = pstrdup(constr->check[i].ccbin);
+ cpy->check[i].ccenforced = constr->check[i].ccenforced;
cpy->check[i].ccvalid = constr->check[i].ccvalid;
cpy->check[i].ccnoinherit = constr->check[i].ccnoinherit;
}
@@ -547,6 +548,7 @@ equalTupleDescs(TupleDesc tupdesc1, TupleDesc tupdesc2)
if (!(strcmp(check1->ccname, check2->ccname) == 0 &&
strcmp(check1->ccbin, check2->ccbin) == 0 &&
+ check1->ccenforced == check2->ccenforced &&
check1->ccvalid == check2->ccvalid &&
check1->ccnoinherit == check2->ccnoinherit))
return false;
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index d7b88b61dcc..23e276b2df1 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -101,13 +101,14 @@ static ObjectAddress AddNewRelationType(const char *typeName,
Oid new_row_type,
Oid new_array_type);
static void RelationRemoveInheritance(Oid relid);
-static Oid StoreRelCheck(Relation rel, const char *ccname, Node *expr,
- bool is_validated, bool is_local, int16 inhcount,
- bool is_no_inherit, bool is_internal);
+static Oid StoreRelCheck(Relation rel, const char *ccname, Node *expr,
+ bool is_enforced, bool is_validated, bool is_local,
+ int16 inhcount, bool is_no_inherit, bool is_internal);
static void StoreConstraints(Relation rel, List *cooked_constraints,
bool is_internal);
static bool MergeWithExistingConstraint(Relation rel, const char *ccname, Node *expr,
bool allow_merge, bool is_local,
+ bool is_enforced,
bool is_initially_valid,
bool is_no_inherit);
static void SetRelationNumChecks(Relation rel, int numchecks);
@@ -2074,8 +2075,8 @@ SetAttrMissing(Oid relid, char *attname, char *value)
*/
static Oid
StoreRelCheck(Relation rel, const char *ccname, Node *expr,
- bool is_validated, bool is_local, int16 inhcount,
- bool is_no_inherit, bool is_internal)
+ bool is_enforced, bool is_validated, bool is_local,
+ int16 inhcount, bool is_no_inherit, bool is_internal)
{
char *ccbin;
List *varList;
@@ -2140,6 +2141,7 @@ StoreRelCheck(Relation rel, const char *ccname, Node *expr,
CONSTRAINT_CHECK, /* Constraint Type */
false, /* Is Deferrable */
false, /* Is Deferred */
+ is_enforced, /* Is Enforced */
is_validated,
InvalidOid, /* no parent constraint */
RelationGetRelid(rel), /* relation */
@@ -2193,6 +2195,7 @@ StoreRelNotNull(Relation rel, const char *nnname, AttrNumber attnum,
CONSTRAINT_NOTNULL,
false,
false,
+ true, /* Is Enforced */
is_validated,
InvalidOid,
RelationGetRelid(rel),
@@ -2262,9 +2265,9 @@ StoreConstraints(Relation rel, List *cooked_constraints, bool is_internal)
case CONSTR_CHECK:
con->conoid =
StoreRelCheck(rel, con->name, con->expr,
- !con->skip_validation, con->is_local,
- con->inhcount, con->is_no_inherit,
- is_internal);
+ con->is_enforced, !con->skip_validation,
+ con->is_local, con->inhcount,
+ con->is_no_inherit, is_internal);
numchecks++;
break;
@@ -2405,6 +2408,7 @@ AddRelationNewConstraints(Relation rel,
cooked->name = NULL;
cooked->attnum = colDef->attnum;
cooked->expr = expr;
+ cooked->is_enforced = true;
cooked->skip_validation = false;
cooked->is_local = is_local;
cooked->inhcount = is_local ? 0 : 1;
@@ -2476,6 +2480,7 @@ AddRelationNewConstraints(Relation rel,
*/
if (MergeWithExistingConstraint(rel, ccname, expr,
allow_merge, is_local,
+ cdef->is_enforced,
cdef->initially_valid,
cdef->is_no_inherit))
continue;
@@ -2524,8 +2529,10 @@ AddRelationNewConstraints(Relation rel,
* OK, store it.
*/
constrOid =
- StoreRelCheck(rel, ccname, expr, cdef->initially_valid, is_local,
- is_local ? 0 : 1, cdef->is_no_inherit, is_internal);
+ StoreRelCheck(rel, ccname, expr, cdef->is_enforced,
+ cdef->initially_valid, is_local,
+ is_local ? 0 : 1, cdef->is_no_inherit,
+ is_internal);
numchecks++;
@@ -2535,6 +2542,7 @@ AddRelationNewConstraints(Relation rel,
cooked->name = ccname;
cooked->attnum = 0;
cooked->expr = expr;
+ cooked->is_enforced = cdef->is_enforced;
cooked->skip_validation = cdef->skip_validation;
cooked->is_local = is_local;
cooked->inhcount = is_local ? 0 : 1;
@@ -2639,6 +2647,7 @@ AddRelationNewConstraints(Relation rel,
static bool
MergeWithExistingConstraint(Relation rel, const char *ccname, Node *expr,
bool allow_merge, bool is_local,
+ bool is_enforced,
bool is_initially_valid,
bool is_no_inherit)
{
@@ -2729,12 +2738,24 @@ MergeWithExistingConstraint(Relation rel, const char *ccname, Node *expr,
* If the child constraint is "not valid" then cannot merge with a
* valid parent constraint.
*/
- if (is_initially_valid && !con->convalidated)
+ if (is_initially_valid && con->conenforced && !con->convalidated)
ereport(ERROR,
(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
errmsg("constraint \"%s\" conflicts with NOT VALID constraint on relation \"%s\"",
ccname, RelationGetRelationName(rel))));
+ /*
+ * A non-enforced child constraint cannot be merged with an enforced
+ * parent constraint. However, the reverse is allowed, where the child
+ * constraint is enforced.
+ */
+ if ((!is_local && is_enforced && !con->conenforced) ||
+ (is_local && !is_enforced && con->conenforced))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("constraint \"%s\" conflicts with NOT ENFORCED constraint on relation \"%s\"",
+ ccname, RelationGetRelationName(rel))));
+
/* OK to update the tuple */
ereport(NOTICE,
(errmsg("merging constraint \"%s\" with inherited definition",
@@ -2770,6 +2791,19 @@ MergeWithExistingConstraint(Relation rel, const char *ccname, Node *expr,
con->connoinherit = true;
}
+ /*
+ * If the child constraint is required to be enforced while the parent
+ * 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.
+ */
+ if (is_enforced && !con->conenforced)
+ {
+ Assert(is_local);
+ con->conenforced = true;
+ con->convalidated = true;
+ }
+
CatalogTupleUpdate(conDesc, &tup->t_self, tup);
}
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 1c3a9e06d37..f4cbaf8953a 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -1957,6 +1957,7 @@ index_constraint_create(Relation heapRelation,
constraintType,
deferrable,
initdeferred,
+ true, /* Is Enforced */
true,
parentConstraintId,
RelationGetRelid(heapRelation),
diff --git a/src/backend/catalog/information_schema.sql b/src/backend/catalog/information_schema.sql
index 76c78c0d184..bd241130381 100644
--- a/src/backend/catalog/information_schema.sql
+++ b/src/backend/catalog/information_schema.sql
@@ -1844,7 +1844,7 @@ CREATE VIEW table_constraints AS
AS is_deferrable,
CAST(CASE WHEN c.condeferred THEN 'YES' ELSE 'NO' END AS yes_or_no)
AS initially_deferred,
- CAST('YES' AS yes_or_no) AS enforced,
+ CAST(CASE WHEN c.conenforced THEN 'YES' ELSE 'NO' END AS yes_or_no) AS enforced,
CAST(CASE WHEN c.contype = 'u'
THEN CASE WHEN (SELECT NOT indnullsnotdistinct FROM pg_index WHERE indexrelid = conindid) THEN 'YES' ELSE 'NO' END
END
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index 9c05a98d28c..f9e4c3d3cfa 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -53,6 +53,7 @@ CreateConstraintEntry(const char *constraintName,
char constraintType,
bool isDeferrable,
bool isDeferred,
+ bool isEnforced,
bool isValidated,
Oid parentConstrId,
Oid relId,
@@ -99,6 +100,11 @@ CreateConstraintEntry(const char *constraintName,
ObjectAddresses *addrs_auto;
ObjectAddresses *addrs_normal;
+ /* Only CHECK constraint can be not enforced */
+ Assert(isEnforced || constraintType == CONSTRAINT_CHECK);
+ /* NOT ENFORCED constraint must be NOT VALID */
+ Assert(isEnforced || !isValidated);
+
conDesc = table_open(ConstraintRelationId, RowExclusiveLock);
Assert(constraintName);
@@ -182,6 +188,7 @@ CreateConstraintEntry(const char *constraintName,
values[Anum_pg_constraint_contype - 1] = CharGetDatum(constraintType);
values[Anum_pg_constraint_condeferrable - 1] = BoolGetDatum(isDeferrable);
values[Anum_pg_constraint_condeferred - 1] = BoolGetDatum(isDeferred);
+ values[Anum_pg_constraint_conenforced - 1] = BoolGetDatum(isEnforced);
values[Anum_pg_constraint_convalidated - 1] = BoolGetDatum(isValidated);
values[Anum_pg_constraint_conrelid - 1] = ObjectIdGetDatum(relId);
values[Anum_pg_constraint_contypid - 1] = ObjectIdGetDatum(domainId);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 6ccae4cb4a8..5deba7382e0 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -373,7 +373,7 @@ static void RangeVarCallbackForTruncate(const RangeVar *relation,
static List *MergeAttributes(List *columns, const List *supers, char relpersistence,
bool is_partition, List **supconstr,
List **supnotnulls);
-static List *MergeCheckConstraint(List *constraints, const char *name, Node *expr);
+static List *MergeCheckConstraint(List *constraints, const char *name, Node *expr, bool is_enforced);
static void MergeChildAttribute(List *inh_columns, int exist_attno, int newcol_attno, const ColumnDef *newdef);
static ColumnDef *MergeInheritedAttribute(List *inh_columns, int exist_attno, const ColumnDef *newdef);
static void MergeAttributesIntoExisting(Relation child_rel, Relation parent_rel, bool ispartition);
@@ -2885,7 +2885,8 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
name,
RelationGetRelationName(relation))));
- constraints = MergeCheckConstraint(constraints, name, expr);
+ constraints = MergeCheckConstraint(constraints, name, expr,
+ check[i].ccenforced);
}
}
@@ -3099,7 +3100,7 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
* the list.
*/
static List *
-MergeCheckConstraint(List *constraints, const char *name, Node *expr)
+MergeCheckConstraint(List *constraints, const char *name, Node *expr, bool is_enforced)
{
ListCell *lc;
CookedConstraint *newcon;
@@ -3122,6 +3123,17 @@ MergeCheckConstraint(List *constraints, const char *name, Node *expr)
ereport(ERROR,
errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
errmsg("too many inheritance parents"));
+
+ /*
+ * When enforceability differs, the merged constraint should be
+ * marked as ENFORCED because one of the parents is ENFORCED.
+ */
+ if (!ccon->is_enforced && is_enforced)
+ {
+ ccon->is_enforced = true;
+ ccon->skip_validation = false;
+ }
+
return constraints;
}
@@ -3140,6 +3152,8 @@ MergeCheckConstraint(List *constraints, const char *name, Node *expr)
newcon->name = pstrdup(name);
newcon->expr = expr;
newcon->inhcount = 1;
+ newcon->is_enforced = is_enforced;
+ newcon->skip_validation = !is_enforced;
return lappend(constraints, newcon);
}
@@ -10418,6 +10432,7 @@ addFkConstraint(addFkConstraintSides fkside,
CONSTRAINT_FOREIGN,
fkconstraint->deferrable,
fkconstraint->initdeferred,
+ true, /* Is Enforced */
fkconstraint->initially_valid,
parentConstr,
RelationGetRelid(rel),
@@ -12004,6 +12019,11 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
errmsg("constraint \"%s\" of relation \"%s\" is not a foreign key or check constraint",
constrName, RelationGetRelationName(rel))));
+ if (!con->conenforced)
+ ereport(ERROR,
+ (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("cannot validate NOT ENFORCED constraint")));
+
if (!con->convalidated)
{
AlteredTableInfo *tab;
@@ -16249,6 +16269,9 @@ decompile_conbin(HeapTuple contup, TupleDesc tupdesc)
* The test we apply is to see whether they reverse-compile to the same
* source string. This insulates us from issues like whether attributes
* have the same physical column numbers in parent and child relations.
+ *
+ * Note that we ignore enforceability as there are cases where constraints
+ * with differing enforceability are allowed.
*/
static bool
constraints_equivalent(HeapTuple a, HeapTuple b, TupleDesc tupleDesc)
@@ -16518,12 +16541,24 @@ MergeConstraintsIntoExisting(Relation child_rel, Relation parent_rel)
* If the child constraint is "not valid" then cannot merge with a
* valid parent constraint
*/
- if (parent_con->convalidated && !child_con->convalidated)
+ if (parent_con->convalidated && child_con->conenforced &&
+ !child_con->convalidated)
ereport(ERROR,
(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
errmsg("constraint \"%s\" conflicts with NOT VALID constraint on child table \"%s\"",
NameStr(child_con->conname), RelationGetRelationName(child_rel))));
+ /*
+ * A non-enforced child constraint cannot be merged with an
+ * enforced parent constraint. However, the reverse is allowed,
+ * where the child constraint is enforced.
+ */
+ if (parent_con->conenforced && !child_con->conenforced)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("constraint \"%s\" conflicts with NOT ENFORCED constraint on child table \"%s\"",
+ NameStr(child_con->conname), RelationGetRelationName(child_rel))));
+
/*
* OK, bump the child constraint's inheritance count. (If we fail
* later on, this change will just roll back.)
@@ -18875,6 +18910,12 @@ ConstraintImpliedByRelConstraint(Relation scanrel, List *testConstraint, List *p
if (!constr->check[i].ccvalid)
continue;
+ /*
+ * NOT ENFORCED constraints are always marked as invalid, which
+ * should have been ignored.
+ */
+ Assert(constr->check[i].ccenforced);
+
cexpr = stringToNode(constr->check[i].ccbin);
/*
@@ -20185,6 +20226,7 @@ DetachAddConstraintIfNeeded(List **wqueue, Relation partRel)
n->is_no_inherit = false;
n->raw_expr = NULL;
n->cooked_expr = nodeToString(make_ands_explicit(constraintExpr));
+ n->is_enforced = true;
n->initially_valid = true;
n->skip_validation = true;
/* It's a re-add, since it nominally already exists */
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 09356e46d16..49d3fc22983 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -809,6 +809,7 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
CONSTRAINT_TRIGGER,
stmt->deferrable,
stmt->initdeferred,
+ true, /* Is Enforced */
true,
InvalidOid, /* no parent */
RelationGetRelid(rel),
diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c
index da591c0922b..59f3cd9a5a3 100644
--- a/src/backend/commands/typecmds.c
+++ b/src/backend/commands/typecmds.c
@@ -1018,6 +1018,13 @@ DefineDomain(CreateDomainStmt *stmt)
errmsg("specifying GENERATED not supported for domains")));
break;
+ case CONSTR_ATTR_ENFORCED:
+ case CONSTR_ATTR_NOT_ENFORCED:
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("specifying constraint enforceability not supported for domains")));
+ break;
+
/* no default, to let compiler warn about missing case */
}
}
@@ -2975,6 +2982,13 @@ AlterDomainAddConstraint(List *names, Node *newConstraint,
errmsg("specifying constraint deferrability not supported for domains")));
break;
+ case CONSTR_ATTR_ENFORCED:
+ case CONSTR_ATTR_NOT_ENFORCED:
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("specifying constraint enforceability not supported for domains")));
+ break;
+
default:
elog(ERROR, "unrecognized constraint subtype: %d",
(int) constr->contype);
@@ -3604,6 +3618,7 @@ domainAddCheckConstraint(Oid domainOid, Oid domainNamespace, Oid baseTypeOid,
CONSTRAINT_CHECK, /* Constraint Type */
false, /* Is Deferrable */
false, /* Is Deferred */
+ true, /* Is Enforced */
!constr->skip_validation, /* Is Validated */
InvalidOid, /* no parent constraint */
InvalidOid, /* not a relation constraint */
@@ -3711,6 +3726,7 @@ domainAddNotNullConstraint(Oid domainOid, Oid domainNamespace, Oid baseTypeOid,
CONSTRAINT_NOTNULL, /* Constraint Type */
false, /* Is Deferrable */
false, /* Is Deferred */
+ true, /* Is Enforced */
!constr->skip_validation, /* Is Validated */
InvalidOid, /* no parent constraint */
InvalidOid, /* not a relation constraint */
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 5ca856fd279..1878f4b8d94 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1751,11 +1751,15 @@ ExecRelCheck(ResultRelInfo *resultRelInfo,
{
oldContext = MemoryContextSwitchTo(estate->es_query_cxt);
resultRelInfo->ri_ConstraintExprs =
- (ExprState **) palloc(ncheck * sizeof(ExprState *));
+ (ExprState **) palloc0(ncheck * sizeof(ExprState *));
for (i = 0; i < ncheck; i++)
{
Expr *checkconstr;
+ /* Skip not enforced constraint */
+ if (!check[i].ccenforced)
+ continue;
+
checkconstr = stringToNode(check[i].ccbin);
resultRelInfo->ri_ConstraintExprs[i] =
ExecPrepareExpr(checkconstr, estate);
@@ -1782,7 +1786,7 @@ ExecRelCheck(ResultRelInfo *resultRelInfo,
* is not to be treated as a failure. Therefore, use ExecCheck not
* ExecQual.
*/
- if (!ExecCheck(checkconstr, econtext))
+ if (checkconstr && !ExecCheck(checkconstr, econtext))
return check[i].ccname;
}
diff --git a/src/backend/nodes/makefuncs.c b/src/backend/nodes/makefuncs.c
index 7e5df7bea4d..72e6a0e89be 100644
--- a/src/backend/nodes/makefuncs.c
+++ b/src/backend/nodes/makefuncs.c
@@ -455,6 +455,7 @@ makeNotNullConstraint(String *colname)
notnull->keys = list_make1(colname);
notnull->skip_validation = false;
notnull->initially_valid = true;
+ notnull->is_enforced = true;
return notnull;
}
diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c
index 37b0ca2e439..155453e1ba0 100644
--- a/src/backend/optimizer/util/plancat.c
+++ b/src/backend/optimizer/util/plancat.c
@@ -1303,9 +1303,21 @@ get_relation_constraints(PlannerInfo *root,
*/
if (!constr->check[i].ccvalid)
continue;
+
+ /*
+ * NOT ENFORCED constraints are always marked as invalid, which
+ * should have been ignored.
+ */
+ Assert(constr->check[i].ccenforced);
+
+ /*
+ * Also ignore if NO INHERIT and we weren't told
+ * that that's safe.
+ */
if (constr->check[i].ccnoinherit && !include_noinherit)
continue;
+
cexpr = stringToNode(constr->check[i].ccbin);
/*
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 67eb96396af..833b3be02be 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -143,6 +143,8 @@ typedef struct KeyActions
#define CAS_INITIALLY_DEFERRED 0x08
#define CAS_NOT_VALID 0x10
#define CAS_NO_INHERIT 0x20
+#define CAS_NOT_ENFORCED 0x40
+#define CAS_ENFORCED 0x80
#define parser_yyerror(msg) scanner_yyerror(msg, yyscanner)
@@ -196,8 +198,8 @@ static void SplitColQualList(List *qualList,
List **constraintList, CollateClause **collClause,
core_yyscan_t yyscanner);
static void processCASbits(int cas_bits, int location, const char *constrType,
- bool *deferrable, bool *initdeferred, bool *not_valid,
- bool *no_inherit, core_yyscan_t yyscanner);
+ bool *deferrable, bool *initdeferred, bool *is_enforced,
+ bool *not_valid, bool *no_inherit, core_yyscan_t yyscanner);
static PartitionStrategy parsePartitionStrategy(char *strategy, int location,
core_yyscan_t yyscanner);
static void preprocess_pubobj_list(List *pubobjspec_list,
@@ -711,9 +713,9 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
DETACH DICTIONARY DISABLE_P DISCARD DISTINCT DO DOCUMENT_P DOMAIN_P
DOUBLE_P DROP
- EACH ELSE EMPTY_P ENABLE_P ENCODING ENCRYPTED END_P ENUM_P ERROR_P ESCAPE
- EVENT EXCEPT EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION
- EXTENSION EXTERNAL EXTRACT
+ EACH ELSE EMPTY_P ENABLE_P ENCODING ENCRYPTED END_P ENFORCED ENUM_P ERROR_P
+ ESCAPE EVENT EXCEPT EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN
+ EXPRESSION EXTENSION EXTERNAL EXTRACT
FALSE_P FAMILY FETCH FILTER FINALIZE FIRST_P FLOAT_P FOLLOWING FOR
FORCE FOREIGN FORMAT FORWARD FREEZE FROM FULL FUNCTION FUNCTIONS
@@ -2658,7 +2660,7 @@ alter_table_cmd:
processCASbits($4, @4, "ALTER CONSTRAINT statement",
&c->deferrable,
&c->initdeferred,
- NULL, NULL, yyscanner);
+ NULL, NULL, NULL, yyscanner);
$$ = (Node *) n;
}
/* ALTER TABLE <name> VALIDATE CONSTRAINT ... */
@@ -3961,6 +3963,7 @@ ColConstraintElem:
n->is_no_inherit = $5;
n->raw_expr = $3;
n->cooked_expr = NULL;
+ n->is_enforced = true;
n->skip_validation = false;
n->initially_valid = true;
$$ = (Node *) n;
@@ -4087,6 +4090,22 @@ ConstraintAttr:
n->location = @1;
$$ = (Node *) n;
}
+ | ENFORCED
+ {
+ Constraint *n = makeNode(Constraint);
+
+ n->contype = CONSTR_ATTR_ENFORCED;
+ n->location = @1;
+ $$ = (Node *) n;
+ }
+ | NOT ENFORCED
+ {
+ Constraint *n = makeNode(Constraint);
+
+ n->contype = CONSTR_ATTR_NOT_ENFORCED;
+ n->location = @1;
+ $$ = (Node *) n;
+ }
;
@@ -4148,7 +4167,8 @@ ConstraintElem:
n->raw_expr = $3;
n->cooked_expr = NULL;
processCASbits($5, @5, "CHECK",
- NULL, NULL, &n->skip_validation,
+ NULL, NULL, &n->is_enforced,
+ &n->skip_validation,
&n->is_no_inherit, yyscanner);
n->initially_valid = !n->skip_validation;
$$ = (Node *) n;
@@ -4162,7 +4182,7 @@ ConstraintElem:
n->keys = list_make1(makeString($3));
/* no NOT VALID support yet */
processCASbits($4, @4, "NOT NULL",
- NULL, NULL, NULL,
+ NULL, NULL, NULL, NULL,
&n->is_no_inherit, yyscanner);
n->initially_valid = true;
$$ = (Node *) n;
@@ -4183,7 +4203,7 @@ ConstraintElem:
n->indexspace = $9;
processCASbits($10, @10, "UNIQUE",
&n->deferrable, &n->initdeferred, NULL,
- NULL, yyscanner);
+ NULL, NULL, yyscanner);
$$ = (Node *) n;
}
| UNIQUE ExistingIndex ConstraintAttributeSpec
@@ -4199,7 +4219,7 @@ ConstraintElem:
n->indexspace = NULL;
processCASbits($3, @3, "UNIQUE",
&n->deferrable, &n->initdeferred, NULL,
- NULL, yyscanner);
+ NULL, NULL, yyscanner);
$$ = (Node *) n;
}
| PRIMARY KEY '(' columnList opt_without_overlaps ')' opt_c_include opt_definition OptConsTableSpace
@@ -4217,7 +4237,7 @@ ConstraintElem:
n->indexspace = $9;
processCASbits($10, @10, "PRIMARY KEY",
&n->deferrable, &n->initdeferred, NULL,
- NULL, yyscanner);
+ NULL, NULL, yyscanner);
$$ = (Node *) n;
}
| PRIMARY KEY ExistingIndex ConstraintAttributeSpec
@@ -4233,7 +4253,7 @@ ConstraintElem:
n->indexspace = NULL;
processCASbits($4, @4, "PRIMARY KEY",
&n->deferrable, &n->initdeferred, NULL,
- NULL, yyscanner);
+ NULL, NULL, yyscanner);
$$ = (Node *) n;
}
| EXCLUDE access_method_clause '(' ExclusionConstraintList ')'
@@ -4253,7 +4273,7 @@ ConstraintElem:
n->where_clause = $9;
processCASbits($10, @10, "EXCLUDE",
&n->deferrable, &n->initdeferred, NULL,
- NULL, yyscanner);
+ NULL, NULL, yyscanner);
$$ = (Node *) n;
}
| FOREIGN KEY '(' columnList optionalPeriodName ')' REFERENCES qualified_name
@@ -4282,7 +4302,8 @@ ConstraintElem:
n->fk_del_set_cols = ($11)->deleteAction->cols;
processCASbits($12, @12, "FOREIGN KEY",
&n->deferrable, &n->initdeferred,
- &n->skip_validation, NULL,
+ NULL, &n->skip_validation,
+ NULL,
yyscanner);
n->initially_valid = !n->skip_validation;
$$ = (Node *) n;
@@ -4322,8 +4343,9 @@ DomainConstraintElem:
n->raw_expr = $3;
n->cooked_expr = NULL;
processCASbits($5, @5, "CHECK",
- NULL, NULL, &n->skip_validation,
+ NULL, NULL, NULL, &n->skip_validation,
&n->is_no_inherit, yyscanner);
+ n->is_enforced = true;
n->initially_valid = !n->skip_validation;
$$ = (Node *) n;
}
@@ -4337,7 +4359,7 @@ DomainConstraintElem:
/* no NOT VALID, NO INHERIT support */
processCASbits($3, @3, "NOT NULL",
NULL, NULL, NULL,
- NULL, yyscanner);
+ NULL, NULL, yyscanner);
n->initially_valid = true;
$$ = (Node *) n;
}
@@ -6000,7 +6022,7 @@ CreateTrigStmt:
n->transitionRels = NIL;
processCASbits($11, @11, "TRIGGER",
&n->deferrable, &n->initdeferred, NULL,
- NULL, yyscanner);
+ NULL, NULL, yyscanner);
n->constrrel = $10;
$$ = (Node *) n;
}
@@ -6169,7 +6191,8 @@ ConstraintAttributeSpec:
parser_errposition(@2)));
/* generic message for other conflicts */
if ((newspec & (CAS_NOT_DEFERRABLE | CAS_DEFERRABLE)) == (CAS_NOT_DEFERRABLE | CAS_DEFERRABLE) ||
- (newspec & (CAS_INITIALLY_IMMEDIATE | CAS_INITIALLY_DEFERRED)) == (CAS_INITIALLY_IMMEDIATE | CAS_INITIALLY_DEFERRED))
+ (newspec & (CAS_INITIALLY_IMMEDIATE | CAS_INITIALLY_DEFERRED)) == (CAS_INITIALLY_IMMEDIATE | CAS_INITIALLY_DEFERRED) ||
+ (newspec & (CAS_NOT_ENFORCED | CAS_ENFORCED)) == (CAS_NOT_ENFORCED | CAS_ENFORCED))
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("conflicting constraint properties"),
@@ -6185,6 +6208,8 @@ ConstraintAttributeElem:
| INITIALLY DEFERRED { $$ = CAS_INITIALLY_DEFERRED; }
| NOT VALID { $$ = CAS_NOT_VALID; }
| NO INHERIT { $$ = CAS_NO_INHERIT; }
+ | NOT ENFORCED { $$ = CAS_NOT_ENFORCED; }
+ | ENFORCED { $$ = CAS_ENFORCED; }
;
@@ -17688,6 +17713,7 @@ unreserved_keyword:
| ENABLE_P
| ENCODING
| ENCRYPTED
+ | ENFORCED
| ENUM_P
| ERROR_P
| ESCAPE
@@ -18265,6 +18291,7 @@ bare_label_keyword:
| ENCODING
| ENCRYPTED
| END_P
+ | ENFORCED
| ENUM_P
| ERROR_P
| ESCAPE
@@ -19403,8 +19430,8 @@ SplitColQualList(List *qualList,
*/
static void
processCASbits(int cas_bits, int location, const char *constrType,
- bool *deferrable, bool *initdeferred, bool *not_valid,
- bool *no_inherit, core_yyscan_t yyscanner)
+ bool *deferrable, bool *initdeferred, bool *is_enforced,
+ bool *not_valid, bool *no_inherit, core_yyscan_t yyscanner)
{
/* defaults */
if (deferrable)
@@ -19413,6 +19440,8 @@ processCASbits(int cas_bits, int location, const char *constrType,
*initdeferred = false;
if (not_valid)
*not_valid = false;
+ if (is_enforced)
+ *is_enforced = true;
if (cas_bits & (CAS_DEFERRABLE | CAS_INITIALLY_DEFERRED))
{
@@ -19465,6 +19494,41 @@ processCASbits(int cas_bits, int location, const char *constrType,
constrType),
parser_errposition(location)));
}
+
+ if (cas_bits & CAS_NOT_ENFORCED)
+ {
+ if (is_enforced)
+ *is_enforced = false;
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ /* translator: %s is CHECK, UNIQUE, or similar */
+ errmsg("%s constraints cannot be marked NOT ENFORCED",
+ constrType),
+ parser_errposition(location)));
+
+ /*
+ * NB: The validated status is irrelevant when the constraint is set to
+ * NOT ENFORCED, but for consistency, it should be set accordingly.
+ * This ensures that if the constraint is later changed to ENFORCED, it
+ * will automatically be in the correct NOT VALIDATED state.
+ */
+ if (not_valid)
+ *not_valid = true;
+ }
+
+ if (cas_bits & CAS_ENFORCED)
+ {
+ if (is_enforced)
+ *is_enforced = true;
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ /* translator: %s is CHECK, UNIQUE, or similar */
+ errmsg("%s constraints cannot be marked ENFORCED",
+ constrType),
+ parser_errposition(location)));
+ }
}
/*
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 0f324ee4e31..fe1116c0922 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -954,6 +954,8 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column)
case CONSTR_ATTR_NOT_DEFERRABLE:
case CONSTR_ATTR_DEFERRED:
case CONSTR_ATTR_IMMEDIATE:
+ case CONSTR_ATTR_ENFORCED:
+ case CONSTR_ATTR_NOT_ENFORCED:
/* transformConstraintAttrs took care of these */
break;
@@ -1093,6 +1095,8 @@ transformTableConstraint(CreateStmtContext *cxt, Constraint *constraint)
case CONSTR_ATTR_NOT_DEFERRABLE:
case CONSTR_ATTR_DEFERRED:
case CONSTR_ATTR_IMMEDIATE:
+ case CONSTR_ATTR_ENFORCED:
+ case CONSTR_ATTR_NOT_ENFORCED:
elog(ERROR, "invalid context for constraint type %d",
constraint->contype);
break;
@@ -1433,6 +1437,8 @@ expandTableLikeClause(RangeVar *heapRel, TableLikeClause *table_like_clause)
{
char *ccname = constr->check[ccnum].ccname;
char *ccbin = constr->check[ccnum].ccbin;
+ bool ccenforced = constr->check[ccnum].ccenforced;
+ bool ccvalid = constr->check[ccnum].ccvalid;
bool ccnoinherit = constr->check[ccnum].ccnoinherit;
Node *ccbin_node;
bool found_whole_row;
@@ -1462,13 +1468,14 @@ expandTableLikeClause(RangeVar *heapRel, TableLikeClause *table_like_clause)
n->contype = CONSTR_CHECK;
n->conname = pstrdup(ccname);
n->location = -1;
+ n->is_enforced = ccenforced;
+ n->initially_valid = ccvalid;
n->is_no_inherit = ccnoinherit;
n->raw_expr = NULL;
n->cooked_expr = nodeToString(ccbin_node);
/* We can skip validation, since the new table should be empty. */
n->skip_validation = true;
- n->initially_valid = true;
atsubcmd = makeNode(AlterTableCmd);
atsubcmd->subtype = AT_AddConstraint;
@@ -2921,9 +2928,11 @@ transformCheckConstraints(CreateStmtContext *cxt, bool skipValidation)
return;
/*
- * If creating a new table (but not a foreign table), we can safely skip
- * validation of check constraints, and nonetheless mark them valid. (This
- * will override any user-supplied NOT VALID flag.)
+ * When creating a new table (but not a foreign table), we can safely skip
+ * the validation of check constraints and mark them as valid based on the
+ * constraint enforcement flag, since NOT ENFORCED constraints must always
+ * be marked as NOT VALID. (This will override any user-supplied
+ * NOT VALID flag.)
*/
if (skipValidation)
{
@@ -2932,7 +2941,7 @@ transformCheckConstraints(CreateStmtContext *cxt, bool skipValidation)
Constraint *constraint = (Constraint *) lfirst(ckclist);
constraint->skip_validation = true;
- constraint->initially_valid = true;
+ constraint->initially_valid = constraint->is_enforced;
}
}
}
@@ -3859,6 +3868,7 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
Constraint *lastprimarycon = NULL;
bool saw_deferrability = false;
bool saw_initially = false;
+ bool saw_enforced = false;
ListCell *clist;
#define SUPPORTS_ATTRS(node) \
@@ -3954,12 +3964,45 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
lastprimarycon->initdeferred = false;
break;
+ case CONSTR_ATTR_ENFORCED:
+ if (lastprimarycon == NULL ||
+ lastprimarycon->contype != CONSTR_CHECK)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("misplaced ENFORCED clause"),
+ parser_errposition(cxt->pstate, con->location)));
+ if (saw_enforced)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("multiple ENFORCED/NOT ENFORCED clauses not allowed"),
+ parser_errposition(cxt->pstate, con->location)));
+ saw_enforced = true;
+ lastprimarycon->is_enforced = true;
+ break;
+
+ case CONSTR_ATTR_NOT_ENFORCED:
+ if (lastprimarycon == NULL ||
+ lastprimarycon->contype != CONSTR_CHECK)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("misplaced NOT ENFORCED clause"),
+ parser_errposition(cxt->pstate, con->location)));
+ if (saw_enforced)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("multiple ENFORCED/NOT ENFORCED clauses not allowed"),
+ parser_errposition(cxt->pstate, con->location)));
+ saw_enforced = true;
+ lastprimarycon->is_enforced = false;
+ break;
+
default:
/* Otherwise it's not an attribute */
lastprimarycon = con;
/* reset flags for new primary node */
saw_deferrability = false;
saw_initially = false;
+ saw_enforced = false;
break;
}
}
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 2194ab3dfa5..7332f37c736 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -2591,7 +2591,11 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
appendStringInfoString(&buf, " DEFERRABLE");
if (conForm->condeferred)
appendStringInfoString(&buf, " INITIALLY DEFERRED");
- if (!conForm->convalidated)
+
+ /* Validated status is irrelevant when the constraint is NOT ENFORCED. */
+ if (!conForm->conenforced)
+ appendStringInfoString(&buf, " NOT ENFORCED");
+ else if (!conForm->convalidated)
appendStringInfoString(&buf, " NOT VALID");
/* Cleanup */
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 422509f18d7..25db64dc440 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4585,6 +4585,7 @@ CheckConstraintFetch(Relation relation)
break;
}
+ check[found].ccenforced = conform->conenforced;
check[found].ccvalid = conform->convalidated;
check[found].ccnoinherit = conform->connoinherit;
check[found].ccname = MemoryContextStrdup(CacheMemoryContext,
diff --git a/src/include/access/tupdesc.h b/src/include/access/tupdesc.h
index 8930a28d660..908f8e7a57c 100644
--- a/src/include/access/tupdesc.h
+++ b/src/include/access/tupdesc.h
@@ -29,6 +29,7 @@ typedef struct ConstrCheck
{
char *ccname;
char *ccbin; /* nodeToString representation of expr */
+ bool ccenforced;
bool ccvalid;
bool ccnoinherit; /* this is a non-inheritable constraint */
} ConstrCheck;
diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h
index 3ca5dbf9e83..17250cc0393 100644
--- a/src/include/catalog/catversion.h
+++ b/src/include/catalog/catversion.h
@@ -57,6 +57,6 @@
*/
/* yyyymmddN */
-#define CATALOG_VERSION_NO 202412021
+#define CATALOG_VERSION_NO 202412041
#endif
diff --git a/src/include/catalog/heap.h b/src/include/catalog/heap.h
index 8c278f202b4..19d25244b89 100644
--- a/src/include/catalog/heap.h
+++ b/src/include/catalog/heap.h
@@ -40,6 +40,7 @@ typedef struct CookedConstraint
char *name; /* name, or NULL if none */
AttrNumber attnum; /* which attr (only for NOTNULL, DEFAULT) */
Node *expr; /* transformed default or check expr */
+ bool is_enforced; /* is enforced? (only for CHECK) */
bool skip_validation; /* skip validation? (only for CHECK) */
bool is_local; /* constraint has local (non-inherited) def */
int16 inhcount; /* number of times constraint is inherited */
diff --git a/src/include/catalog/pg_constraint.h b/src/include/catalog/pg_constraint.h
index 4b4476738a2..8e13f5ed710 100644
--- a/src/include/catalog/pg_constraint.h
+++ b/src/include/catalog/pg_constraint.h
@@ -51,6 +51,7 @@ CATALOG(pg_constraint,2606,ConstraintRelationId)
char contype; /* constraint type; see codes below */
bool condeferrable; /* deferrable constraint? */
bool condeferred; /* deferred by default? */
+ bool conenforced; /* enforced constraint? */
bool convalidated; /* constraint has been validated? */
/*
@@ -222,6 +223,7 @@ extern Oid CreateConstraintEntry(const char *constraintName,
char constraintType,
bool isDeferrable,
bool isDeferred,
+ bool isEnforced,
bool isValidated,
Oid parentConstrId,
Oid relId,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 0f9462493e3..912aabfa4ca 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2736,6 +2736,8 @@ typedef enum ConstrType /* types of constraints */
CONSTR_ATTR_NOT_DEFERRABLE,
CONSTR_ATTR_DEFERRED,
CONSTR_ATTR_IMMEDIATE,
+ CONSTR_ATTR_ENFORCED,
+ CONSTR_ATTR_NOT_ENFORCED,
} ConstrType;
/* Foreign key action codes */
@@ -2757,6 +2759,7 @@ typedef struct Constraint
char *conname; /* Constraint name, or NULL if unnamed */
bool deferrable; /* DEFERRABLE? */
bool initdeferred; /* INITIALLY DEFERRED? */
+ bool is_enforced; /* enforced constraint? */
bool skip_validation; /* skip validation of existing rows? */
bool initially_valid; /* mark the new constraint as valid? */
bool is_no_inherit; /* is constraint non-inheritable? */
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 899d64ad55f..f2bc378394e 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -153,6 +153,7 @@ PG_KEYWORD("enable", ENABLE_P, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("encoding", ENCODING, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("encrypted", ENCRYPTED, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("end", END_P, RESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("enforced", ENFORCED, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("enum", ENUM_P, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("error", ERROR_P, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("escape", ESCAPE, UNRESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out
index 2212c8dbb59..b68ac47a3cd 100644
--- a/src/test/regress/expected/alter_table.out
+++ b/src/test/regress/expected/alter_table.out
@@ -507,11 +507,14 @@ ALTER TABLE attmp3 validate constraint attmpconstr;
ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten CHECK (b > 10); -- fail
ERROR: check constraint "b_greater_than_ten" of relation "attmp3" is violated by some row
ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten CHECK (b > 10) NOT VALID; -- succeeds
+ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten_not_enforced CHECK (b > 10) NOT ENFORCED; -- succeeds
ALTER TABLE attmp3 VALIDATE CONSTRAINT b_greater_than_ten; -- fails
ERROR: check constraint "b_greater_than_ten" of relation "attmp3" is violated by some row
DELETE FROM attmp3 WHERE NOT b > 10;
ALTER TABLE attmp3 VALIDATE CONSTRAINT b_greater_than_ten; -- succeeds
ALTER TABLE attmp3 VALIDATE CONSTRAINT b_greater_than_ten; -- succeeds
+ALTER TABLE attmp3 VALIDATE CONSTRAINT b_greater_than_ten_not_enforced; -- fail
+ERROR: cannot validated NOT ENFORCED constraint
-- Test inherited NOT VALID CHECK constraints
select * from attmp3;
a | b
diff --git a/src/test/regress/expected/constraints.out b/src/test/regress/expected/constraints.out
index 71200c90ed3..692a69fe457 100644
--- a/src/test/regress/expected/constraints.out
+++ b/src/test/regress/expected/constraints.out
@@ -87,6 +87,25 @@ SELECT * FROM CHECK_TBL;
6
(3 rows)
+CREATE TABLE NE_CHECK_TBL (x int,
+ CONSTRAINT CHECK_CON CHECK (x > 3) NOT ENFORCED);
+INSERT INTO NE_CHECK_TBL VALUES (5);
+INSERT INTO NE_CHECK_TBL VALUES (4);
+INSERT INTO NE_CHECK_TBL VALUES (3);
+INSERT INTO NE_CHECK_TBL VALUES (2);
+INSERT INTO NE_CHECK_TBL VALUES (6);
+INSERT INTO NE_CHECK_TBL VALUES (1);
+SELECT * FROM NE_CHECK_TBL;
+ x
+---
+ 5
+ 4
+ 3
+ 2
+ 6
+ 1
+(6 rows)
+
CREATE SEQUENCE CHECK_SEQ;
CREATE TABLE CHECK2_TBL (x int, y text, z int,
CONSTRAINT SEQUENCE_CON
@@ -120,7 +139,8 @@ CREATE TABLE INSERT_TBL (x INT DEFAULT nextval('insert_seq'),
y TEXT DEFAULT '-NULL-',
z INT DEFAULT -1 * currval('insert_seq'),
CONSTRAINT INSERT_TBL_CON CHECK (x >= 3 AND y <> 'check failed' AND x < 8),
- CHECK (x + z = 0));
+ CHECK (x + z = 0) ENFORCED, /* no change it is a default */
+ CONSTRAINT NE_INSERT_TBL_CON CHECK (x + z = 1) NOT ENFORCED);
INSERT INTO INSERT_TBL(x,z) VALUES (2, -2);
ERROR: new row for relation "insert_tbl" violates check constraint "insert_tbl_con"
DETAIL: Failing row contains (2, -NULL-, -2).
@@ -715,6 +735,24 @@ SELECT * FROM unique_tbl;
3 | threex
(5 rows)
+-- enforcibility cannot be specified or set for unique constrain
+CREATE TABLE UNIQUE_EN_TBL(i int UNIQUE ENFORCED);
+ERROR: misplaced ENFORCED clause
+LINE 1: CREATE TABLE UNIQUE_EN_TBL(i int UNIQUE ENFORCED);
+ ^
+CREATE TABLE UNIQUE_NOTEN_TBL(i int UNIQUE NOT ENFORCED);
+ERROR: misplaced NOT ENFORCED clause
+LINE 1: CREATE TABLE UNIQUE_NOTEN_TBL(i int UNIQUE NOT ENFORCED);
+ ^
+-- XXX: error message is misleading here
+ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
+ERROR: ALTER CONSTRAINT statement constraints cannot be marked ENFORCED
+LINE 1: ...TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
+ ^
+ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORCED;
+ERROR: ALTER CONSTRAINT statement constraints cannot be marked NOT ENFORCED
+LINE 1: ...ABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORC...
+ ^
DROP TABLE unique_tbl;
--
-- EXCLUDE constraints
diff --git a/src/test/regress/expected/create_table_like.out b/src/test/regress/expected/create_table_like.out
index d091da5a1ef..e0613891351 100644
--- a/src/test/regress/expected/create_table_like.out
+++ b/src/test/regress/expected/create_table_like.out
@@ -315,7 +315,8 @@ Referenced by:
DROP TABLE inhz;
-- including storage and comments
-CREATE TABLE ctlt1 (a text CHECK (length(a) > 2) PRIMARY KEY, b text);
+CREATE TABLE ctlt1 (a text CHECK (length(a) > 2) ENFORCED PRIMARY KEY,
+ b text CHECK (length(b) > 100) NOT ENFORCED);
CREATE INDEX ctlt1_b_key ON ctlt1 (b);
CREATE INDEX ctlt1_fnidx ON ctlt1 ((a || b));
CREATE STATISTICS ctlt1_a_b_stat ON a,b FROM ctlt1;
@@ -366,6 +367,7 @@ CREATE TABLE ctlt1_inh (LIKE ctlt1 INCLUDING CONSTRAINTS INCLUDING COMMENTS) INH
NOTICE: merging column "a" with inherited definition
NOTICE: merging column "b" with inherited definition
NOTICE: merging constraint "ctlt1_a_check" with inherited definition
+NOTICE: merging constraint "ctlt1_b_check" with inherited definition
\d+ ctlt1_inh
Table "public.ctlt1_inh"
Column | Type | Collation | Nullable | Default | Storage | Stats target | Description
@@ -374,6 +376,7 @@ NOTICE: merging constraint "ctlt1_a_check" with inherited definition
b | text | | | | extended | | B
Check constraints:
"ctlt1_a_check" CHECK (length(a) > 2)
+ "ctlt1_b_check" CHECK (length(b) > 100) NOT ENFORCED
Not-null constraints:
"ctlt1_a_not_null" NOT NULL "a" (local, inherited)
Inherits: ctlt1
@@ -395,6 +398,7 @@ NOTICE: merging multiple inherited definitions of column "a"
c | text | | | | external | |
Check constraints:
"ctlt1_a_check" CHECK (length(a) > 2)
+ "ctlt1_b_check" CHECK (length(b) > 100) NOT ENFORCED
"ctlt3_a_check" CHECK (length(a) < 5)
"ctlt3_c_check" CHECK (length(c) < 7)
Not-null constraints:
@@ -415,6 +419,7 @@ Indexes:
"ctlt13_like_expr_idx" btree ((a || c))
Check constraints:
"ctlt1_a_check" CHECK (length(a) > 2)
+ "ctlt1_b_check" CHECK (length(b) > 100) NOT ENFORCED
"ctlt3_a_check" CHECK (length(a) < 5)
"ctlt3_c_check" CHECK (length(c) < 7)
Not-null constraints:
@@ -440,6 +445,7 @@ Indexes:
"ctlt_all_expr_idx" btree ((a || b))
Check constraints:
"ctlt1_a_check" CHECK (length(a) > 2)
+ "ctlt1_b_check" CHECK (length(b) > 100) NOT ENFORCED
Statistics objects:
"public.ctlt_all_a_b_stat" ON a, b FROM ctlt_all
"public.ctlt_all_expr_stat" ON (a || b) FROM ctlt_all
@@ -482,6 +488,7 @@ Indexes:
"pg_attrdef_expr_idx" btree ((a || b))
Check constraints:
"ctlt1_a_check" CHECK (length(a) > 2)
+ "ctlt1_b_check" CHECK (length(b) > 100) NOT ENFORCED
Statistics objects:
"public.pg_attrdef_a_b_stat" ON a, b FROM public.pg_attrdef
"public.pg_attrdef_expr_stat" ON (a || b) FROM public.pg_attrdef
@@ -506,6 +513,7 @@ Indexes:
"ctlt1_expr_idx" btree ((a || b))
Check constraints:
"ctlt1_a_check" CHECK (length(a) > 2)
+ "ctlt1_b_check" CHECK (length(b) > 100) NOT ENFORCED
Statistics objects:
"ctl_schema.ctlt1_a_b_stat" ON a, b FROM ctlt1
"ctl_schema.ctlt1_expr_stat" ON (a || b) FROM ctlt1
diff --git a/src/test/regress/expected/domain.out b/src/test/regress/expected/domain.out
index 42b6559f9c8..d894122ce61 100644
--- a/src/test/regress/expected/domain.out
+++ b/src/test/regress/expected/domain.out
@@ -1297,6 +1297,21 @@ select pg_basetype(1); -- expect NULL not error
drop domain mytext cascade;
NOTICE: drop cascades to type mytext_child_1
--
+-- Explicit enforceability specification not allowed
+---
+CREATE DOMAIN constraint_enforced_dom AS int CONSTRAINT the_constraint CHECK (value > 0) ENFORCED;
+ERROR: specifying constraint enforceability not supported for domains
+CREATE DOMAIN constraint_not_enforced_dom AS int CONSTRAINT the_constraint CHECK (value > 0) NOT ENFORCED;
+ERROR: specifying constraint enforceability not supported for domains
+ALTER DOMAIN constraint_comments_dom ADD CONSTRAINT the_constraint CHECK (value > 0) ENFORCED;
+ERROR: CHECK constraints cannot be marked ENFORCED
+LINE 1: ...om ADD CONSTRAINT the_constraint CHECK (value > 0) ENFORCED;
+ ^
+ALTER DOMAIN constraint_comments_dom ADD CONSTRAINT the_constraint CHECK (value > 0) NOT ENFORCED;
+ERROR: CHECK constraints cannot be marked NOT ENFORCED
+LINE 1: ...m ADD CONSTRAINT the_constraint CHECK (value > 0) NOT ENFORC...
+ ^
+--
-- Information schema
--
SELECT * FROM information_schema.column_domain_usage
diff --git a/src/test/regress/expected/inherit.out b/src/test/regress/expected/inherit.out
index bb81f6d2b4d..dbf3835cb14 100644
--- a/src/test/regress/expected/inherit.out
+++ b/src/test/regress/expected/inherit.out
@@ -1319,19 +1319,97 @@ NOTICE: merging constraint "inh_check_constraint1" with inherited definition
alter table p1_c1 add constraint inh_check_constraint2 check (f1 < 10);
alter table p1 add constraint inh_check_constraint2 check (f1 < 10);
NOTICE: merging constraint "inh_check_constraint2" with inherited definition
-select conrelid::regclass::text as relname, conname, conislocal, coninhcount
+alter table p1 add constraint inh_check_constraint3 check (f1 > 0) not enforced;
+alter table p1_c1 add constraint inh_check_constraint3 check (f1 > 0) not enforced;
+NOTICE: merging constraint "inh_check_constraint3" with inherited definition
+alter table p1_c1 add constraint inh_check_constraint4 check (f1 < 10) not enforced;
+alter table p1 add constraint inh_check_constraint4 check (f1 < 10) not enforced;
+NOTICE: merging constraint "inh_check_constraint4" with inherited definition
+-- allowed to merge enforced constraint with parent's not enforced constraint
+alter table p1_c1 add constraint inh_check_constraint5 check (f1 < 10) enforced;
+alter table p1 add constraint inh_check_constraint5 check (f1 < 10) not enforced;
+NOTICE: merging constraint "inh_check_constraint5" with inherited definition
+alter table p1 add constraint inh_check_constraint6 check (f1 < 10) not enforced;
+alter table p1_c1 add constraint inh_check_constraint6 check (f1 < 10) enforced;
+NOTICE: merging constraint "inh_check_constraint6" with inherited definition
+create table p1_c2(f1 int constraint inh_check_constraint4 check (f1 < 10)) inherits(p1);
+NOTICE: merging column "f1" with inherited definition
+NOTICE: merging constraint "inh_check_constraint4" with inherited definition
+-- but reverse is not allowed
+alter table p1_c1 add constraint inh_check_constraint7 check (f1 < 10) not enforced;
+alter table p1 add constraint inh_check_constraint7 check (f1 < 10) enforced;
+ERROR: constraint "inh_check_constraint7" conflicts with NOT ENFORCED constraint on relation "p1_c1"
+alter table p1 add constraint inh_check_constraint8 check (f1 < 10) enforced;
+alter table p1_c1 add constraint inh_check_constraint8 check (f1 < 10) not enforced;
+ERROR: constraint "inh_check_constraint8" conflicts with NOT ENFORCED constraint on relation "p1_c1"
+create table p1_fail(f1 int constraint inh_check_constraint2 check (f1 < 10) not enforced) inherits(p1);
+NOTICE: merging column "f1" with inherited definition
+ERROR: constraint "inh_check_constraint2" conflicts with NOT ENFORCED constraint on relation "p1_fail"
+-- constraints with different enforceability can be merged by marking them as ENFORCED
+create table p1_c3() inherits(p1, p1_c1);
+NOTICE: merging multiple inherited definitions of column "f1"
+-- but not allowed if the child constraint is explicitly asked to be NOT ENFORCED
+create table p1_fail(f1 int constraint inh_check_constraint6 check (f1 < 10) not enforced) inherits(p1, p1_c1);
+NOTICE: merging multiple inherited definitions of column "f1"
+NOTICE: merging column "f1" with inherited definition
+ERROR: constraint "inh_check_constraint6" conflicts with NOT ENFORCED constraint on relation "p1_fail"
+select conrelid::regclass::text as relname, conname, conislocal, coninhcount, conenforced
from pg_constraint where conname like 'inh\_check\_constraint%'
order by 1, 2;
- relname | conname | conislocal | coninhcount
----------+-----------------------+------------+-------------
- p1 | inh_check_constraint1 | t | 0
- p1 | inh_check_constraint2 | t | 0
- p1_c1 | inh_check_constraint1 | t | 1
- p1_c1 | inh_check_constraint2 | t | 1
-(4 rows)
+ relname | conname | conislocal | coninhcount | conenforced
+---------+-----------------------+------------+-------------+-------------
+ p1 | inh_check_constraint1 | t | 0 | t
+ p1 | inh_check_constraint2 | t | 0 | t
+ p1 | inh_check_constraint3 | t | 0 | f
+ p1 | inh_check_constraint4 | t | 0 | f
+ p1 | inh_check_constraint5 | t | 0 | f
+ p1 | inh_check_constraint6 | t | 0 | f
+ p1 | inh_check_constraint8 | t | 0 | t
+ p1_c1 | inh_check_constraint1 | t | 1 | t
+ p1_c1 | inh_check_constraint2 | t | 1 | t
+ p1_c1 | inh_check_constraint3 | t | 1 | f
+ p1_c1 | inh_check_constraint4 | t | 1 | f
+ p1_c1 | inh_check_constraint5 | t | 1 | t
+ p1_c1 | inh_check_constraint6 | t | 1 | t
+ p1_c1 | inh_check_constraint7 | t | 0 | f
+ p1_c1 | inh_check_constraint8 | f | 1 | t
+ p1_c2 | inh_check_constraint1 | f | 1 | t
+ p1_c2 | inh_check_constraint2 | f | 1 | t
+ p1_c2 | inh_check_constraint3 | f | 1 | f
+ p1_c2 | inh_check_constraint4 | t | 1 | t
+ p1_c2 | inh_check_constraint5 | f | 1 | f
+ p1_c2 | inh_check_constraint6 | f | 1 | f
+ p1_c2 | inh_check_constraint8 | f | 1 | t
+ p1_c3 | inh_check_constraint1 | f | 2 | t
+ p1_c3 | inh_check_constraint2 | f | 2 | t
+ p1_c3 | inh_check_constraint3 | f | 2 | f
+ p1_c3 | inh_check_constraint4 | f | 2 | f
+ p1_c3 | inh_check_constraint5 | f | 2 | t
+ p1_c3 | inh_check_constraint6 | f | 2 | t
+ p1_c3 | inh_check_constraint7 | f | 1 | f
+ p1_c3 | inh_check_constraint8 | f | 2 | t
+(30 rows)
+drop table p1 cascade;
+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
+--
+-- Similarly, check the merging of existing constraints; a parent constraint
+-- marked as NOT ENFORCED can merge with an ENFORCED child constraint, but the
+-- reverse is not allowed.
+--
+create table p1(f1 int constraint p1_a_check check (f1 > 0) not enforced);
+create table p1_c1(f1 int constraint p1_a_check check (f1 > 0) enforced);
+alter table p1_c1 inherit p1;
drop table p1 cascade;
NOTICE: drop cascades to table p1_c1
+create table p1(f1 int constraint p1_a_check check (f1 > 0) enforced);
+create table p1_c1(f1 int constraint p1_a_check check (f1 > 0) not enforced);
+alter table p1_c1 inherit p1;
+ERROR: constraint "p1_a_check" conflicts with NOT ENFORCED constraint on child table "p1_c1"
+drop table p1, p1_c1;
--
-- Test DROP behavior of multiply-defined CHECK constraints
--
diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql
index 637e3dac389..d8bdb91b367 100644
--- a/src/test/regress/sql/alter_table.sql
+++ b/src/test/regress/sql/alter_table.sql
@@ -387,10 +387,12 @@ ALTER TABLE attmp3 validate constraint attmpconstr;
-- Try a non-verified CHECK constraint
ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten CHECK (b > 10); -- fail
ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten CHECK (b > 10) NOT VALID; -- succeeds
+ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten_not_enforced CHECK (b > 10) NOT ENFORCED; -- succeeds
ALTER TABLE attmp3 VALIDATE CONSTRAINT b_greater_than_ten; -- fails
DELETE FROM attmp3 WHERE NOT b > 10;
ALTER TABLE attmp3 VALIDATE CONSTRAINT b_greater_than_ten; -- succeeds
ALTER TABLE attmp3 VALIDATE CONSTRAINT b_greater_than_ten; -- succeeds
+ALTER TABLE attmp3 VALIDATE CONSTRAINT b_greater_than_ten_not_enforced; -- fail
-- Test inherited NOT VALID CHECK constraints
select * from attmp3;
diff --git a/src/test/regress/sql/constraints.sql b/src/test/regress/sql/constraints.sql
index e607eb1fddb..d6742f83fb9 100644
--- a/src/test/regress/sql/constraints.sql
+++ b/src/test/regress/sql/constraints.sql
@@ -67,6 +67,18 @@ INSERT INTO CHECK_TBL VALUES (1);
SELECT * FROM CHECK_TBL;
+CREATE TABLE NE_CHECK_TBL (x int,
+ CONSTRAINT CHECK_CON CHECK (x > 3) NOT ENFORCED);
+
+INSERT INTO NE_CHECK_TBL VALUES (5);
+INSERT INTO NE_CHECK_TBL VALUES (4);
+INSERT INTO NE_CHECK_TBL VALUES (3);
+INSERT INTO NE_CHECK_TBL VALUES (2);
+INSERT INTO NE_CHECK_TBL VALUES (6);
+INSERT INTO NE_CHECK_TBL VALUES (1);
+
+SELECT * FROM NE_CHECK_TBL;
+
CREATE SEQUENCE CHECK_SEQ;
CREATE TABLE CHECK2_TBL (x int, y text, z int,
@@ -92,7 +104,8 @@ CREATE TABLE INSERT_TBL (x INT DEFAULT nextval('insert_seq'),
y TEXT DEFAULT '-NULL-',
z INT DEFAULT -1 * currval('insert_seq'),
CONSTRAINT INSERT_TBL_CON CHECK (x >= 3 AND y <> 'check failed' AND x < 8),
- CHECK (x + z = 0));
+ CHECK (x + z = 0) ENFORCED, /* no change it is a default */
+ CONSTRAINT NE_INSERT_TBL_CON CHECK (x + z = 1) NOT ENFORCED);
INSERT INTO INSERT_TBL(x,z) VALUES (2, -2);
@@ -518,6 +531,13 @@ COMMIT;
SELECT * FROM unique_tbl;
+-- enforcibility cannot be specified or set for unique constrain
+CREATE TABLE UNIQUE_EN_TBL(i int UNIQUE ENFORCED);
+CREATE TABLE UNIQUE_NOTEN_TBL(i int UNIQUE NOT ENFORCED);
+-- XXX: error message is misleading here
+ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
+ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORCED;
+
DROP TABLE unique_tbl;
--
diff --git a/src/test/regress/sql/create_table_like.sql b/src/test/regress/sql/create_table_like.sql
index dea8942c71f..a41f8b83d77 100644
--- a/src/test/regress/sql/create_table_like.sql
+++ b/src/test/regress/sql/create_table_like.sql
@@ -128,7 +128,8 @@ CREATE TABLE inhz (x text REFERENCES inhz, LIKE inhx INCLUDING INDEXES);
DROP TABLE inhz;
-- including storage and comments
-CREATE TABLE ctlt1 (a text CHECK (length(a) > 2) PRIMARY KEY, b text);
+CREATE TABLE ctlt1 (a text CHECK (length(a) > 2) ENFORCED PRIMARY KEY,
+ b text CHECK (length(b) > 100) NOT ENFORCED);
CREATE INDEX ctlt1_b_key ON ctlt1 (b);
CREATE INDEX ctlt1_fnidx ON ctlt1 ((a || b));
CREATE STATISTICS ctlt1_a_b_stat ON a,b FROM ctlt1;
diff --git a/src/test/regress/sql/domain.sql b/src/test/regress/sql/domain.sql
index ee07b03174e..518455c058a 100644
--- a/src/test/regress/sql/domain.sql
+++ b/src/test/regress/sql/domain.sql
@@ -866,6 +866,14 @@ select pg_basetype(1); -- expect NULL not error
drop domain mytext cascade;
+--
+-- Explicit enforceability specification not allowed
+---
+CREATE DOMAIN constraint_enforced_dom AS int CONSTRAINT the_constraint CHECK (value > 0) ENFORCED;
+CREATE DOMAIN constraint_not_enforced_dom AS int CONSTRAINT the_constraint CHECK (value > 0) NOT ENFORCED;
+ALTER DOMAIN constraint_comments_dom ADD CONSTRAINT the_constraint CHECK (value > 0) ENFORCED;
+ALTER DOMAIN constraint_comments_dom ADD CONSTRAINT the_constraint CHECK (value > 0) NOT ENFORCED;
+
--
-- Information schema
diff --git a/src/test/regress/sql/inherit.sql b/src/test/regress/sql/inherit.sql
index f51c70d6b03..49aae426f3c 100644
--- a/src/test/regress/sql/inherit.sql
+++ b/src/test/regress/sql/inherit.sql
@@ -468,12 +468,57 @@ alter table p1_c1 add constraint inh_check_constraint1 check (f1 > 0);
alter table p1_c1 add constraint inh_check_constraint2 check (f1 < 10);
alter table p1 add constraint inh_check_constraint2 check (f1 < 10);
-select conrelid::regclass::text as relname, conname, conislocal, coninhcount
+alter table p1 add constraint inh_check_constraint3 check (f1 > 0) not enforced;
+alter table p1_c1 add constraint inh_check_constraint3 check (f1 > 0) not enforced;
+
+alter table p1_c1 add constraint inh_check_constraint4 check (f1 < 10) not enforced;
+alter table p1 add constraint inh_check_constraint4 check (f1 < 10) not enforced;
+
+-- allowed to merge enforced constraint with parent's not enforced constraint
+alter table p1_c1 add constraint inh_check_constraint5 check (f1 < 10) enforced;
+alter table p1 add constraint inh_check_constraint5 check (f1 < 10) not enforced;
+
+alter table p1 add constraint inh_check_constraint6 check (f1 < 10) not enforced;
+alter table p1_c1 add constraint inh_check_constraint6 check (f1 < 10) enforced;
+
+create table p1_c2(f1 int constraint inh_check_constraint4 check (f1 < 10)) inherits(p1);
+
+-- but reverse is not allowed
+alter table p1_c1 add constraint inh_check_constraint7 check (f1 < 10) not enforced;
+alter table p1 add constraint inh_check_constraint7 check (f1 < 10) enforced;
+
+alter table p1 add constraint inh_check_constraint8 check (f1 < 10) enforced;
+alter table p1_c1 add constraint inh_check_constraint8 check (f1 < 10) not enforced;
+
+create table p1_fail(f1 int constraint inh_check_constraint2 check (f1 < 10) not enforced) inherits(p1);
+
+-- constraints with different enforceability can be merged by marking them as ENFORCED
+create table p1_c3() inherits(p1, p1_c1);
+
+-- but not allowed if the child constraint is explicitly asked to be NOT ENFORCED
+create table p1_fail(f1 int constraint inh_check_constraint6 check (f1 < 10) not enforced) inherits(p1, p1_c1);
+
+select conrelid::regclass::text as relname, conname, conislocal, coninhcount, conenforced
from pg_constraint where conname like 'inh\_check\_constraint%'
order by 1, 2;
drop table p1 cascade;
+--
+-- Similarly, check the merging of existing constraints; a parent constraint
+-- marked as NOT ENFORCED can merge with an ENFORCED child constraint, but the
+-- reverse is not allowed.
+--
+create table p1(f1 int constraint p1_a_check check (f1 > 0) not enforced);
+create table p1_c1(f1 int constraint p1_a_check check (f1 > 0) enforced);
+alter table p1_c1 inherit p1;
+drop table p1 cascade;
+
+create table p1(f1 int constraint p1_a_check check (f1 > 0) enforced);
+create table p1_c1(f1 int constraint p1_a_check check (f1 > 0) not enforced);
+alter table p1_c1 inherit p1;
+drop table p1, p1_c1;
+
--
-- Test DROP behavior of multiply-defined CHECK constraints
--
--
2.43.5
v7-0002-refactor-split-ATExecAlterConstrRecurse.patchapplication/octet-stream; name=v7-0002-refactor-split-ATExecAlterConstrRecurse.patchDownload
From 27545d7e871ab4708e12fd56051993419267510e Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 23 Sep 2024 12:31:07 +0530
Subject: [PATCH v7 2/5] refactor: split ATExecAlterConstrRecurse()
When changing a NOT ENFORCED foreign key constraint to ENFORCED, we
need to create the necessary triggers for enforcing the constraint. We
can set the deferrability at the time of creation and skip the code
used for other constraint alteration operations.
Additionally, move the code that iterates over each child constraint
of the constraint being altered into a separate function.
---
src/backend/commands/tablecmds.c | 194 +++++++++++++++++++------------
1 file changed, 118 insertions(+), 76 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 5deba7382e0..1c55da7d98a 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -394,6 +394,12 @@ static ObjectAddress ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd,
static bool ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
Relation rel, HeapTuple contuple, List **otherrelids,
LOCKMODE lockmode);
+static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
+ bool deferrable, bool initdeferred,
+ List **otherrelids);
+static void ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
+ Relation rel, HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode);
static ObjectAddress ATExecValidateConstraint(List **wqueue,
Relation rel, char *constrName,
bool recurse, bool recursing, LOCKMODE lockmode);
@@ -11850,9 +11856,6 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
{
HeapTuple copyTuple;
Form_pg_constraint copy_con;
- HeapTuple tgtuple;
- ScanKeyData tgkey;
- SysScanDesc tgscan;
copyTuple = heap_copytuple(contuple);
copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
@@ -11873,53 +11876,8 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
* Now we need to update the multiple entries in pg_trigger that
* implement the constraint.
*/
- ScanKeyInit(&tgkey,
- Anum_pg_trigger_tgconstraint,
- BTEqualStrategyNumber, F_OIDEQ,
- ObjectIdGetDatum(conoid));
- tgscan = systable_beginscan(tgrel, TriggerConstraintIndexId, true,
- NULL, 1, &tgkey);
- while (HeapTupleIsValid(tgtuple = systable_getnext(tgscan)))
- {
- Form_pg_trigger tgform = (Form_pg_trigger) GETSTRUCT(tgtuple);
- Form_pg_trigger copy_tg;
- HeapTuple tgCopyTuple;
-
- /*
- * Remember OIDs of other relation(s) involved in FK constraint.
- * (Note: it's likely that we could skip forcing a relcache inval
- * for other rels that don't have a trigger whose properties
- * change, but let's be conservative.)
- */
- if (tgform->tgrelid != RelationGetRelid(rel))
- *otherrelids = list_append_unique_oid(*otherrelids,
- tgform->tgrelid);
-
- /*
- * Update deferrability of RI_FKey_noaction_del,
- * RI_FKey_noaction_upd, RI_FKey_check_ins and RI_FKey_check_upd
- * triggers, but not others; see createForeignKeyActionTriggers
- * and CreateFKCheckTrigger.
- */
- if (tgform->tgfoid != F_RI_FKEY_NOACTION_DEL &&
- tgform->tgfoid != F_RI_FKEY_NOACTION_UPD &&
- tgform->tgfoid != F_RI_FKEY_CHECK_INS &&
- tgform->tgfoid != F_RI_FKEY_CHECK_UPD)
- continue;
-
- tgCopyTuple = heap_copytuple(tgtuple);
- copy_tg = (Form_pg_trigger) GETSTRUCT(tgCopyTuple);
-
- copy_tg->tgdeferrable = cmdcon->deferrable;
- copy_tg->tginitdeferred = cmdcon->initdeferred;
- CatalogTupleUpdate(tgrel, &tgCopyTuple->t_self, tgCopyTuple);
-
- InvokeObjectPostAlterHook(TriggerRelationId, tgform->oid, 0);
-
- heap_freetuple(tgCopyTuple);
- }
-
- systable_endscan(tgscan);
+ AlterConstrTriggerDeferrability(conoid, tgrel, rel, cmdcon->deferrable,
+ cmdcon->initdeferred, otherrelids);
}
/*
@@ -11932,36 +11890,120 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
*/
if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)
- {
- ScanKeyData pkey;
- SysScanDesc pscan;
- HeapTuple childtup;
-
- ScanKeyInit(&pkey,
- Anum_pg_constraint_conparentid,
- BTEqualStrategyNumber, F_OIDEQ,
- ObjectIdGetDatum(conoid));
-
- pscan = systable_beginscan(conrel, ConstraintParentIndexId,
- true, NULL, 1, &pkey);
-
- while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
- {
- Form_pg_constraint childcon = (Form_pg_constraint) GETSTRUCT(childtup);
- Relation childrel;
-
- childrel = table_open(childcon->conrelid, lockmode);
- ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, childrel, childtup,
- otherrelids, lockmode);
- table_close(childrel, NoLock);
- }
-
- systable_endscan(pscan);
- }
+ ATExecAlterChildConstr(cmdcon, conrel, tgrel, rel, contuple,
+ otherrelids, lockmode);
return changed;
}
+/*
+ * A subroutine of ATExecAlterConstrRecurse that updated constraint trigger's
+ * deferrability.
+ *
+ * The arguments to this function have the same meaning as the arguments to
+ * ATExecAlterConstrRecurse.
+ */
+static void
+AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
+ bool deferrable, bool initdeferred,
+ List **otherrelids)
+{
+ HeapTuple tgtuple;
+ ScanKeyData tgkey;
+ SysScanDesc tgscan;
+
+ ScanKeyInit(&tgkey,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+ tgscan = systable_beginscan(tgrel, TriggerConstraintIndexId, true,
+ NULL, 1, &tgkey);
+ while (HeapTupleIsValid(tgtuple = systable_getnext(tgscan)))
+ {
+ Form_pg_trigger tgform = (Form_pg_trigger) GETSTRUCT(tgtuple);
+ Form_pg_trigger copy_tg;
+ HeapTuple tgCopyTuple;
+
+ /*
+ * Remember OIDs of other relation(s) involved in FK constraint.
+ * (Note: it's likely that we could skip forcing a relcache inval for
+ * other rels that don't have a trigger whose properties change, but
+ * let's be conservative.)
+ */
+ if (tgform->tgrelid != RelationGetRelid(rel))
+ *otherrelids = list_append_unique_oid(*otherrelids,
+ tgform->tgrelid);
+
+ /*
+ * Update enable status and deferrability of RI_FKey_noaction_del,
+ * RI_FKey_noaction_upd, RI_FKey_check_ins and RI_FKey_check_upd
+ * triggers, but not others; see createForeignKeyActionTriggers and
+ * CreateFKCheckTrigger.
+ */
+ if (tgform->tgfoid != F_RI_FKEY_NOACTION_DEL &&
+ tgform->tgfoid != F_RI_FKEY_NOACTION_UPD &&
+ tgform->tgfoid != F_RI_FKEY_CHECK_INS &&
+ tgform->tgfoid != F_RI_FKEY_CHECK_UPD)
+ continue;
+
+ tgCopyTuple = heap_copytuple(tgtuple);
+ copy_tg = (Form_pg_trigger) GETSTRUCT(tgCopyTuple);
+
+ copy_tg->tgdeferrable = deferrable;
+ copy_tg->tginitdeferred = initdeferred;
+ CatalogTupleUpdate(tgrel, &tgCopyTuple->t_self, tgCopyTuple);
+
+ InvokeObjectPostAlterHook(TriggerRelationId, tgform->oid, 0);
+
+ heap_freetuple(tgCopyTuple);
+ }
+
+ systable_endscan(tgscan);
+}
+
+/*
+ * Invokes ATExecAlterConstrRecurse for each constraint that is a child of the
+ * specified constraint.
+ *
+ * The arguments to this function have the same meaning as the arguments to
+ * ATExecAlterConstrRecurse.
+ */
+static void
+ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
+ Relation rel, HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode)
+{
+ Form_pg_constraint currcon;
+ Oid conoid;
+ ScanKeyData pkey;
+ SysScanDesc pscan;
+ HeapTuple childtup;
+
+ currcon = (Form_pg_constraint) GETSTRUCT(contuple);
+ conoid = currcon->oid;
+
+ ScanKeyInit(&pkey,
+ Anum_pg_constraint_conparentid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+
+ pscan = systable_beginscan(conrel, ConstraintParentIndexId,
+ true, NULL, 1, &pkey);
+
+ while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
+ {
+ Form_pg_constraint childcon = (Form_pg_constraint) GETSTRUCT(childtup);
+ Relation childrel;
+
+ childrel = table_open(childcon->conrelid, lockmode);
+ ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, childrel, childtup,
+ otherrelids, lockmode);
+ table_close(childrel, NoLock);
+ }
+
+ systable_endscan(pscan);
+}
+
/*
* ALTER TABLE VALIDATE CONSTRAINT
*
--
2.43.5
v7-0003-refactor-Change-ATExecAlterConstrRecurse-argument.patchapplication/octet-stream; name=v7-0003-refactor-Change-ATExecAlterConstrRecurse-argument.patchDownload
From b2c4a56333b455dda33fc50d782da1aaaeaba5a9 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 23 Sep 2024 12:37:05 +0530
Subject: [PATCH v7 3/5] refactor: Change ATExecAlterConstrRecurse() argument.
Instead of passing the Relation of the referencing relation, we will
pass a relid. Opening the relation using the relid again should not
cause significant issues. However, this approach makes recursion more
efficient, especially in the later patch where we will be recreating
the referencing and referred triggers. In addition to that passing
relid of the referred relation too and Oids of triggers.
----
NOTE: This patch is not meant to be committed separately. It should
be squashed with the main patch that adds ENFORCED/NOT ENFORCED.
----
---
src/backend/commands/tablecmds.c | 66 ++++++++++++++++++++++----------
1 file changed, 45 insertions(+), 21 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 1c55da7d98a..cba3f1714c0 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -392,14 +392,22 @@ static void AlterSeqNamespaces(Relation classRel, Relation rel,
static ObjectAddress ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd,
bool recurse, bool recursing, LOCKMODE lockmode);
static bool ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode);
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger);
static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
bool deferrable, bool initdeferred,
List **otherrelids);
static void ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode);
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger);
static ObjectAddress ATExecValidateConstraint(List **wqueue,
Relation rel, char *constrName,
bool recurse, bool recursing, LOCKMODE lockmode);
@@ -11796,8 +11804,10 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
currcon->condeferred != cmdcon->initdeferred ||
rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
- if (ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, rel, contuple,
- &otherrelids, lockmode))
+ if (ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, currcon->conrelid,
+ currcon->confrelid, contuple, &otherrelids,
+ lockmode, InvalidOid, InvalidOid,
+ InvalidOid, InvalidOid))
ObjectAddressSet(address, ConstraintRelationId, currcon->oid);
}
@@ -11830,13 +11840,18 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
*/
static bool
ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode)
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger)
{
Form_pg_constraint currcon;
Oid conoid;
Oid refrelid;
bool changed = false;
+ Relation rel;
/* since this function recurses, it could be driven to stack overflow */
check_stack_depth();
@@ -11845,6 +11860,8 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
conoid = currcon->oid;
refrelid = currcon->confrelid;
+ rel = table_open(currcon->conrelid, lockmode);
+
/*
* Update pg_constraint with the flags from cmdcon.
*
@@ -11890,8 +11907,14 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
*/
if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)
- ATExecAlterChildConstr(cmdcon, conrel, tgrel, rel, contuple,
- otherrelids, lockmode);
+ ATExecAlterChildConstr(cmdcon, conrel, tgrel, fkrelid, pkrelid,
+ contuple, otherrelids, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
+
+ table_close(rel, NoLock);
return changed;
}
@@ -11970,8 +11993,12 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
*/
static void
ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode)
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger)
{
Form_pg_constraint currcon;
Oid conoid;
@@ -11991,15 +12018,12 @@ ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
true, NULL, 1, &pkey);
while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
- {
- Form_pg_constraint childcon = (Form_pg_constraint) GETSTRUCT(childtup);
- Relation childrel;
-
- childrel = table_open(childcon->conrelid, lockmode);
- ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, childrel, childtup,
- otherrelids, lockmode);
- table_close(childrel, NoLock);
- }
+ ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, fkrelid, pkrelid,
+ childtup, otherrelids, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
systable_endscan(pscan);
}
--
2.43.5
v7-0004-Remove-hastriggers-flag-check-before-fetching-FK-.patchapplication/octet-stream; name=v7-0004-Remove-hastriggers-flag-check-before-fetching-FK-.patchDownload
From c7fb9351dfb7eec9f7d53aa842d48bfca3015e16 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 7 Oct 2024 12:40:07 +0530
Subject: [PATCH v7 4/5] Remove hastriggers flag check before fetching FK
constraints.
With NOT ENFORCED, foreign key constraints will be created without
triggers. Therefore, the criteria for fetching foreign keys based on
the presence of triggers no longer apply.
----
NOTE: This patch is intended to reduce the diff noise from the main
patch and is not meant to be committed separately. It should be
squashed with the main patch that adds ENFORCED/NOT ENFORCED.
----
---
src/backend/utils/cache/relcache.c | 5 -
src/bin/pg_dump/pg_dump.c | 8 +-
src/bin/psql/describe.c | 226 ++++++++++++++---------------
3 files changed, 108 insertions(+), 131 deletions(-)
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 25db64dc440..0809f03aca2 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4669,11 +4669,6 @@ RelationGetFKeyList(Relation relation)
if (relation->rd_fkeyvalid)
return relation->rd_fkeylist;
- /* Fast path: non-partitioned tables without triggers can't have FKs */
- if (!relation->rd_rel->relhastriggers &&
- relation->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
- return NIL;
-
/*
* We build the list we intend to return (in the caller's context) while
* doing the scan. After successfully completing the scan, we copy that
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index ec0cdf4ed74..9ada4cdc182 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -7839,13 +7839,7 @@ getConstraints(Archive *fout, TableInfo tblinfo[], int numTables)
{
TableInfo *tinfo = &tblinfo[i];
- /*
- * For partitioned tables, foreign keys have no triggers so they must
- * be included anyway in case some foreign keys are defined.
- */
- if ((!tinfo->hastriggers &&
- tinfo->relkind != RELKIND_PARTITIONED_TABLE) ||
- !(tinfo->dobj.dump & DUMP_COMPONENT_DEFINITION))
+ if (!(tinfo->dobj.dump & DUMP_COMPONENT_DEFINITION))
continue;
/* OK, we need info for this table */
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 2657abdc72d..054e8a2d821 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2528,136 +2528,124 @@ describeOneTableDetails(const char *schemaname,
PQclear(result);
}
- /*
- * Print foreign-key constraints (there are none if no triggers,
- * except if the table is partitioned, in which case the triggers
- * appear in the partitions)
- */
- if (tableinfo.hastriggers ||
- tableinfo.relkind == RELKIND_PARTITIONED_TABLE)
+ /* Print foreign-key constraints */
+ if (pset.sversion >= 120000 &&
+ (tableinfo.ispartition || tableinfo.relkind == RELKIND_PARTITIONED_TABLE))
{
- if (pset.sversion >= 120000 &&
- (tableinfo.ispartition || tableinfo.relkind == RELKIND_PARTITIONED_TABLE))
+ /*
+ * Put the constraints defined in this table first, followed
+ * by the constraints defined in ancestor partitioned tables.
+ */
+ printfPQExpBuffer(&buf,
+ "SELECT conrelid = '%s'::pg_catalog.regclass AS sametable,\n"
+ " conname,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef,\n"
+ " conrelid::pg_catalog.regclass AS ontable\n"
+ " FROM pg_catalog.pg_constraint,\n"
+ " pg_catalog.pg_partition_ancestors('%s')\n"
+ " WHERE conrelid = relid AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
+ "ORDER BY sametable DESC, conname;",
+ oid, oid);
+ }
+ else
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT true as sametable, conname,\n"
+ " pg_catalog.pg_get_constraintdef(r.oid, true) as condef,\n"
+ " conrelid::pg_catalog.regclass AS ontable\n"
+ "FROM pg_catalog.pg_constraint r\n"
+ "WHERE r.conrelid = '%s' AND r.contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n",
+ oid);
+
+ if (pset.sversion >= 120000)
+ appendPQExpBufferStr(&buf, " AND conparentid = 0\n");
+ appendPQExpBufferStr(&buf, "ORDER BY conname");
+ }
+
+ result = PSQLexec(buf.data);
+ if (!result)
+ goto error_return;
+ else
+ tuples = PQntuples(result);
+
+ if (tuples > 0)
+ {
+ int i_sametable = PQfnumber(result, "sametable"),
+ i_conname = PQfnumber(result, "conname"),
+ i_condef = PQfnumber(result, "condef"),
+ i_ontable = PQfnumber(result, "ontable");
+
+ printTableAddFooter(&cont, _("Foreign-key constraints:"));
+ for (i = 0; i < tuples; i++)
{
/*
- * Put the constraints defined in this table first, followed
- * by the constraints defined in ancestor partitioned tables.
+ * Print untranslated constraint name and definition. Use
+ * a "TABLE tab" prefix when the constraint is defined in
+ * a parent partitioned table.
*/
- printfPQExpBuffer(&buf,
- "SELECT conrelid = '%s'::pg_catalog.regclass AS sametable,\n"
- " conname,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef,\n"
- " conrelid::pg_catalog.regclass AS ontable\n"
- " FROM pg_catalog.pg_constraint,\n"
- " pg_catalog.pg_partition_ancestors('%s')\n"
- " WHERE conrelid = relid AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
- "ORDER BY sametable DESC, conname;",
- oid, oid);
- }
- else
- {
- printfPQExpBuffer(&buf,
- "SELECT true as sametable, conname,\n"
- " pg_catalog.pg_get_constraintdef(r.oid, true) as condef,\n"
- " conrelid::pg_catalog.regclass AS ontable\n"
- "FROM pg_catalog.pg_constraint r\n"
- "WHERE r.conrelid = '%s' AND r.contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n",
- oid);
-
- if (pset.sversion >= 120000)
- appendPQExpBufferStr(&buf, " AND conparentid = 0\n");
- appendPQExpBufferStr(&buf, "ORDER BY conname");
- }
-
- result = PSQLexec(buf.data);
- if (!result)
- goto error_return;
- else
- tuples = PQntuples(result);
-
- if (tuples > 0)
- {
- int i_sametable = PQfnumber(result, "sametable"),
- i_conname = PQfnumber(result, "conname"),
- i_condef = PQfnumber(result, "condef"),
- i_ontable = PQfnumber(result, "ontable");
-
- printTableAddFooter(&cont, _("Foreign-key constraints:"));
- for (i = 0; i < tuples; i++)
- {
- /*
- * Print untranslated constraint name and definition. Use
- * a "TABLE tab" prefix when the constraint is defined in
- * a parent partitioned table.
- */
- if (strcmp(PQgetvalue(result, i, i_sametable), "f") == 0)
- printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
- PQgetvalue(result, i, i_ontable),
- PQgetvalue(result, i, i_conname),
- PQgetvalue(result, i, i_condef));
- else
- printfPQExpBuffer(&buf, " \"%s\" %s",
- PQgetvalue(result, i, i_conname),
- PQgetvalue(result, i, i_condef));
-
- printTableAddFooter(&cont, buf.data);
- }
- }
- PQclear(result);
- }
-
- /* print incoming foreign-key references */
- if (tableinfo.hastriggers ||
- tableinfo.relkind == RELKIND_PARTITIONED_TABLE)
- {
- if (pset.sversion >= 120000)
- {
- printfPQExpBuffer(&buf,
- "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
- " FROM pg_catalog.pg_constraint c\n"
- " WHERE confrelid IN (SELECT pg_catalog.pg_partition_ancestors('%s')\n"
- " UNION ALL VALUES ('%s'::pg_catalog.regclass))\n"
- " AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
- "ORDER BY conname;",
- oid, oid);
- }
- else
- {
- printfPQExpBuffer(&buf,
- "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
- " FROM pg_catalog.pg_constraint\n"
- " WHERE confrelid = %s AND contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n"
- "ORDER BY conname;",
- oid);
- }
-
- result = PSQLexec(buf.data);
- if (!result)
- goto error_return;
- else
- tuples = PQntuples(result);
-
- if (tuples > 0)
- {
- int i_conname = PQfnumber(result, "conname"),
- i_ontable = PQfnumber(result, "ontable"),
- i_condef = PQfnumber(result, "condef");
-
- printTableAddFooter(&cont, _("Referenced by:"));
- for (i = 0; i < tuples; i++)
- {
+ if (strcmp(PQgetvalue(result, i, i_sametable), "f") == 0)
printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
PQgetvalue(result, i, i_ontable),
PQgetvalue(result, i, i_conname),
PQgetvalue(result, i, i_condef));
+ else
+ printfPQExpBuffer(&buf, " \"%s\" %s",
+ PQgetvalue(result, i, i_conname),
+ PQgetvalue(result, i, i_condef));
- printTableAddFooter(&cont, buf.data);
- }
+ printTableAddFooter(&cont, buf.data);
}
- PQclear(result);
}
+ PQclear(result);
+
+ /* print incoming foreign-key references */
+ if (pset.sversion >= 120000)
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
+ " FROM pg_catalog.pg_constraint c\n"
+ " WHERE confrelid IN (SELECT pg_catalog.pg_partition_ancestors('%s')\n"
+ " UNION ALL VALUES ('%s'::pg_catalog.regclass))\n"
+ " AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
+ "ORDER BY conname;",
+ oid, oid);
+ }
+ else
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
+ " FROM pg_catalog.pg_constraint\n"
+ " WHERE confrelid = %s AND contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n"
+ "ORDER BY conname;",
+ oid);
+ }
+
+ result = PSQLexec(buf.data);
+ if (!result)
+ goto error_return;
+ else
+ tuples = PQntuples(result);
+
+ if (tuples > 0)
+ {
+ int i_conname = PQfnumber(result, "conname"),
+ i_ontable = PQfnumber(result, "ontable"),
+ i_condef = PQfnumber(result, "condef");
+
+ printTableAddFooter(&cont, _("Referenced by:"));
+ for (i = 0; i < tuples; i++)
+ {
+ printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
+ PQgetvalue(result, i, i_ontable),
+ PQgetvalue(result, i, i_conname),
+ PQgetvalue(result, i, i_condef));
+
+ printTableAddFooter(&cont, buf.data);
+ }
+ }
+ PQclear(result);
/* print any row-level policies */
if (pset.sversion >= 90500)
--
2.43.5
v7-0005-WIP-Add-support-for-NOT-ENFORCED-in-foreign-key-c.patchapplication/octet-stream; name=v7-0005-WIP-Add-support-for-NOT-ENFORCED-in-foreign-key-c.patchDownload
From 52d2592809b1f2a9fba3de1f5dca03be7cbb2c39 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Wed, 4 Dec 2024 15:23:38 +0530
Subject: [PATCH v7 5/5] WIP-Add support for NOT ENFORCED in foreign key
constraints.
Typically, when a foreign key (FK) constraint is created on a table,
action and check triggers are added to maintain data integrity. With
this patch, if a constraint is marked as NOT ENFORCED, integrity
checks are no longer required, making these triggers unnecessary.
Consequently, when creating a NOT ENFORCED FK constraint, triggers
will not be created, and the constraint will be marked as NOT VALID.
Similarly, if an existing FK constraint is changed to NOT ENFORCED,
the associated triggers will be dropped, and the constraint will also
be marked as NOT VALID. Conversely, if a NOT ENFORCED FK constraint is
changed to ENFORCED, the necessary triggers will be created, and the
constraint will remain in the NOT VALID state.
== WIP ==
The restriction preventing NOT VALID foreign key constraints on
partitioned tables has now been bypassed for NOT ENFORCED constraints,
but it could potentially be removed entirely. As per Alvaro Herrera,
this feature has not yet been implemented for partitioned tables.
Further investigation is needed to determine what additional changes,
aside from removing the error check, are necessary to fully support
NOT VALID foreign key constraints on partitioned tables. As a result,
this patch remains marked as WIP.
=========
---
doc/src/sgml/advanced.sgml | 13 +
doc/src/sgml/catalogs.sgml | 2 +-
doc/src/sgml/ddl.sgml | 12 +-
doc/src/sgml/ref/alter_table.sgml | 12 +-
doc/src/sgml/ref/create_table.sgml | 10 +-
src/backend/catalog/pg_constraint.c | 5 +-
src/backend/commands/tablecmds.c | 439 ++++++++++++++++------
src/backend/parser/gram.y | 5 +-
src/backend/parser/parse_utilcmd.c | 14 +-
src/backend/utils/cache/relcache.c | 1 +
src/include/utils/rel.h | 3 +
src/test/regress/expected/alter_table.out | 2 +-
src/test/regress/expected/constraints.out | 8 +-
src/test/regress/expected/foreign_key.out | 126 ++++++-
src/test/regress/sql/foreign_key.sql | 89 ++++-
15 files changed, 589 insertions(+), 152 deletions(-)
diff --git a/doc/src/sgml/advanced.sgml b/doc/src/sgml/advanced.sgml
index 755c9f14850..b7d90de69d7 100644
--- a/doc/src/sgml/advanced.sgml
+++ b/doc/src/sgml/advanced.sgml
@@ -125,6 +125,19 @@ ERROR: insert or update on table "weather" violates foreign key constraint "wea
DETAIL: Key (city)=(Berkeley) is not present in table "cities".
</screen>
</para>
+ <para>
+ If, for some reason, you do not want to enforce this constraint and wish to
+ avoid the error, you can modify the foreign key constraint to <literal>NOT ENFORCED</literal>,
+ or specify it in the <link linkend="sql-createtable"><command>CREATE TABLE</command></link>.
+ </para>
+ <para>
+ Let's change the constraint to <literal>NOT ENFORCED</literal>, and when you
+ attempt to reinsert the same record, it will not result in any error.
+<programlisting>
+ALTER TABLE weather ALTER CONSTRAINT weather_city_fkey NOT ENFORCED;
+INSERT INTO weather VALUES ('Berkeley', 45, 53, 0.0, '1994-11-28');
+</programlisting>
+ </para>
<para>
The behavior of foreign keys can be finely tuned to your
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index a56ef4df562..3bb04574481 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2608,7 +2608,7 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l
</para>
<para>
Is the constraint enforceable?
- Currently, can be false only for CHECK constraints
+ Currently, can be false only for foreign keys and CHECK constraints
</para></entry>
</row>
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index 9a20396dae4..cda36576670 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -1058,11 +1058,13 @@ CREATE TABLE example (
</indexterm>
<para>
- A foreign key constraint specifies that the values in a column (or
- a group of columns) must match the values appearing in some row
- of another table.
- We say this maintains the <firstterm>referential
- integrity</firstterm> between two related tables.
+ A foreign key constraint with <literal>ENFORCED</literal>, the default
+ setting, specifies that the values in a column (or a group of columns)
+ must match the values appearing in some row of another table. We say this
+ maintains the <firstterm>referential integrity</firstterm> between two
+ related tables. If a foreign key constraint is created with <literal>NOT ENFORCED</literal>,
+ the check for aforementioned <firstterm>referential integrity</firstterm>
+ will not be performed.
</para>
<para>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 938450fba18..fa807cb9f3a 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -58,7 +58,7 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable>
ALTER [ COLUMN ] <replaceable class="parameter">column_name</replaceable> SET COMPRESSION <replaceable class="parameter">compression_method</replaceable>
ADD <replaceable class="parameter">table_constraint</replaceable> [ NOT VALID ]
ADD <replaceable class="parameter">table_constraint_using_index</replaceable>
- ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+ ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]
VALIDATE CONSTRAINT <replaceable class="parameter">constraint_name</replaceable>
DROP CONSTRAINT [ IF EXISTS ] <replaceable class="parameter">constraint_name</replaceable> [ RESTRICT | CASCADE ]
DISABLE TRIGGER [ <replaceable class="parameter">trigger_name</replaceable> | ALL | USER ]
@@ -555,6 +555,13 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
This form alters the attributes of a constraint that was previously
created. Currently only foreign key constraints may be altered.
</para>
+ <para>
+ Note that when a constraint is changed to <literal>NOT ENFORCED</literal>,
+ it is also marked as <literal>NOT VALID</literal>. While this might seem
+ unnecessary, it is done for consistency and should be set accordingly.
+ This ensures that if the constraint is later changed to <literal>ENFORCED</literal>,
+ it will automatically be in the correct <literal>NOT VALID</literal> state.
+ </para>
</listitem>
</varlistentry>
@@ -565,7 +572,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
This form validates a foreign key or check constraint that was
previously created as <literal>NOT VALID</literal>, by scanning the
table to ensure there are no rows for which the constraint is not
- satisfied. Nothing happens if the constraint is already marked valid.
+ satisfied. If the constraint is not enforced, an error is thrown.
+ Nothing happens if the constraint is already marked valid.
(See <xref linkend="sql-altertable-notes"/> below for an explanation
of the usefulness of this command.)
</para>
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index cfd01c4ef2b..89fc7354c00 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -1382,11 +1382,11 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
<term><literal>NOT ENFORCED</literal></term>
<listitem>
<para>
- This is currently only allowed for <literal>CHECK</literal> constraints.
- If the constraint is <literal>NOT ENFORCED</literal>, this clause
- specifies that the constraint check will be skipped. When the constraint
- is <literal>ENFORCED</literal>, check is performed after each statement;
- the latter is the default.
+ This is currently only allowed for foreign key and <literal>CHECK</literal>
+ constraints. If the constraint is <literal>NOT ENFORCED</literal>, this
+ clause specifies that the constraint check will be skipped. When the
+ constraint is <literal>ENFORCED</literal>, check is performed after each
+ statement; the latter is the default.
</para>
<para>
Note that if the constraint is <literal>NOT ENFORCED</literal>, it is also
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index f9e4c3d3cfa..3e8a6eb94f3 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -100,8 +100,9 @@ CreateConstraintEntry(const char *constraintName,
ObjectAddresses *addrs_auto;
ObjectAddresses *addrs_normal;
- /* Only CHECK constraint can be not enforced */
- Assert(isEnforced || constraintType == CONSTRAINT_CHECK);
+ /* Only CHECK or FOREIGN KEY constraint can be not enforced */
+ Assert(isEnforced || constraintType == CONSTRAINT_CHECK ||
+ constraintType == CONSTRAINT_FOREIGN);
/* NOT ENFORCED constraint must be NOT VALID */
Assert(isEnforced || !isValidated);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index cba3f1714c0..2f704e6d777 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -389,19 +389,29 @@ static void AlterIndexNamespaces(Relation classRel, Relation rel,
static void AlterSeqNamespaces(Relation classRel, Relation rel,
Oid oldNspOid, Oid newNspOid, ObjectAddresses *objsMoved,
LOCKMODE lockmode);
-static ObjectAddress ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd,
+static ObjectAddress ATExecAlterConstraint(List **wqueue, Relation rel, AlterTableCmd *cmd,
bool recurse, bool recursing, LOCKMODE lockmode);
-static bool ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
+static bool ATExecAlterConstrRecurse(List **wqueue, Constraint *cmdcon,
+ Relation conrel, Relation tgrel,
const Oid fkrelid, const Oid pkrelid,
HeapTuple contuple, List **otherrelids,
LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
Oid ReferencedParentUpdTrigger,
Oid ReferencingParentInsTrigger,
Oid ReferencingParentUpdTrigger);
-static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
+static void ATExecAlterConstrEnforceability(List **wqueue, Constraint *cmdcon,
+ Relation conrel, Relation tgrel,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger);
+static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Oid fkrel,
bool deferrable, bool initdeferred,
List **otherrelids);
-static void ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
+static void ATExecAlterChildConstr(List **wqueue, Constraint *cmdcon,
+ Relation conrel, Relation tgrel,
const Oid fkrelid, const Oid pkrelid,
HeapTuple contuple, List **otherrelids,
LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
@@ -5428,7 +5438,7 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
lockmode);
break;
case AT_AlterConstraint: /* ALTER CONSTRAINT */
- address = ATExecAlterConstraint(rel, cmd, false, false, lockmode);
+ address = ATExecAlterConstraint(wqueue, rel, cmd, false, false, lockmode);
break;
case AT_ValidateConstraint: /* VALIDATE CONSTRAINT */
address = ATExecValidateConstraint(wqueue, rel, cmd->name, cmd->recurse,
@@ -9772,7 +9782,24 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
errmsg("cannot use ONLY for foreign key on partitioned table \"%s\" referencing relation \"%s\"",
RelationGetRelationName(rel),
RelationGetRelationName(pkrel))));
- if (fkconstraint->skip_validation && !fkconstraint->initially_valid)
+
+
+ /*
+ * TODO: I'm not sure why this restriction exists; NOT VALID foreign
+ * keys seem to work fine on partitioned tables, with behavior matching
+ * that of NOT VALID foreign keys on regular tables. I still need to
+ * confirm the necessity of this restriction, and it might be something
+ * that can be removed in the future. For now, I have bypassed this
+ * error for NOT ENFORCED foreign key constraints.
+ *
+ * TODO: UPDATE: As per Alvaro Herrera, this feature has not been
+ * implemented for partitioned tables. I still need to investigate what
+ * additional changes, beyond removing the following error check, are
+ * required to support NOT VALID foreign key constraints on partitioned
+ * tables.
+ */
+ if (fkconstraint->is_enforced && fkconstraint->skip_validation &&
+ !fkconstraint->initially_valid)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot add NOT VALID foreign key on partitioned table \"%s\" referencing relation \"%s\"",
@@ -10446,7 +10473,7 @@ addFkConstraint(addFkConstraintSides fkside,
CONSTRAINT_FOREIGN,
fkconstraint->deferrable,
fkconstraint->initdeferred,
- true, /* Is Enforced */
+ fkconstraint->is_enforced,
fkconstraint->initially_valid,
parentConstr,
RelationGetRelid(rel),
@@ -10564,20 +10591,22 @@ addFkRecurseReferenced(Constraint *fkconstraint, Relation rel,
Oid parentDelTrigger, Oid parentUpdTrigger,
bool with_period)
{
- Oid deleteTriggerOid,
- updateTriggerOid;
+ Oid deleteTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
Assert(CheckRelationLockedByMe(pkrel, ShareRowExclusiveLock, true));
Assert(CheckRelationLockedByMe(rel, ShareRowExclusiveLock, true));
/*
- * Create the action triggers that enforce the constraint.
+ * Create action triggers to enforce the constraint, or skip them if the
+ * constraint is not enforced.
*/
- createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
- fkconstraint,
- parentConstr, indexOid,
- parentDelTrigger, parentUpdTrigger,
- &deleteTriggerOid, &updateTriggerOid);
+ if (fkconstraint->is_enforced)
+ createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
+ fkconstraint,
+ parentConstr, indexOid,
+ parentDelTrigger, parentUpdTrigger,
+ &deleteTriggerOid, &updateTriggerOid);
/*
* If the referenced table is partitioned, recurse on ourselves to handle
@@ -10698,8 +10727,8 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
Oid parentInsTrigger, Oid parentUpdTrigger,
bool with_period)
{
- Oid insertTriggerOid,
- updateTriggerOid;
+ Oid insertTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
Assert(OidIsValid(parentConstr));
Assert(CheckRelationLockedByMe(rel, ShareRowExclusiveLock, true));
@@ -10711,29 +10740,32 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
errmsg("foreign key constraints are not supported on foreign tables")));
/*
- * Add the check triggers to it and, if necessary, schedule it to be
- * checked in Phase 3.
+ * Add check triggers if the constraint is enforced, and if needed,
+ * schedule them to be checked in Phase 3.
*
* If the relation is partitioned, drill down to do it to its partitions.
*/
- createForeignKeyCheckTriggers(RelationGetRelid(rel),
- RelationGetRelid(pkrel),
- fkconstraint,
- parentConstr,
- indexOid,
- parentInsTrigger, parentUpdTrigger,
- &insertTriggerOid, &updateTriggerOid);
+ if (fkconstraint->is_enforced)
+ createForeignKeyCheckTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
+ fkconstraint,
+ parentConstr,
+ indexOid,
+ parentInsTrigger, parentUpdTrigger,
+ &insertTriggerOid, &updateTriggerOid);
if (rel->rd_rel->relkind == RELKIND_RELATION)
{
/*
* Tell Phase 3 to check that the constraint is satisfied by existing
- * rows. We can skip this during table creation, when requested
- * explicitly by specifying NOT VALID in an ADD FOREIGN KEY command,
- * and when we're recreating a constraint following a SET DATA TYPE
- * operation that did not impugn its validity.
+ * rows. We can skip this during table creation, when constraint is
+ * specified as NOT ENFORCED, when requested explicitly by specifying
+ * NOT VALID in an ADD FOREIGN KEY command, and when we're recreating
+ * a constraint following a SET DATA TYPE operation that did not
+ * impugn its validity.
*/
- if (wqueue && !old_check_ok && !fkconstraint->skip_validation)
+ if (wqueue && !old_check_ok && !fkconstraint->skip_validation &&
+ fkconstraint->is_enforced)
{
NewConstraint *newcon;
AlteredTableInfo *tab;
@@ -10968,8 +11000,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
AttrNumber confdelsetcols[INDEX_MAX_KEYS];
Constraint *fkconstraint;
ObjectAddress address;
- Oid deleteTriggerOid,
- updateTriggerOid;
+ Oid deleteTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(constrOid));
if (!HeapTupleIsValid(tuple))
@@ -11029,8 +11061,9 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
fkconstraint->fk_del_set_cols = NIL;
fkconstraint->old_conpfeqop = NIL;
fkconstraint->old_pktable_oid = InvalidOid;
+ fkconstraint->is_enforced = constrForm->conenforced;
fkconstraint->skip_validation = false;
- fkconstraint->initially_valid = true;
+ fkconstraint->initially_valid = constrForm->convalidated;
/* set up colnames that are used to generate the constraint name */
for (int i = 0; i < numfks; i++)
@@ -11058,9 +11091,10 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
* parent OIDs for similar triggers that will be created on the
* partition in addFkRecurseReferenced().
*/
- GetForeignKeyActionTriggers(trigrel, constrOid,
- constrForm->confrelid, constrForm->conrelid,
- &deleteTriggerOid, &updateTriggerOid);
+ if (constrForm->conenforced)
+ GetForeignKeyActionTriggers(trigrel, constrOid,
+ constrForm->confrelid, constrForm->conrelid,
+ &deleteTriggerOid, &updateTriggerOid);
/* Add this constraint ... */
address = addFkConstraint(addFkReferencedSide,
@@ -11226,17 +11260,18 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
mapped_conkey[i] = attmap->attnums[conkey[i] - 1];
/*
- * Get the "check" triggers belonging to the constraint to pass as
- * parent OIDs for similar triggers that will be created on the
- * partition in addFkRecurseReferencing(). They are also passed to
- * tryAttachPartitionForeignKey() below to simply assign as parents to
- * the partition's existing "check" triggers, that is, if the
- * corresponding constraints is deemed attachable to the parent
- * constraint.
+ * Get the "check" triggers belonging to the constraint, if it is
+ * enforced, to pass as parent OIDs for similar triggers that will be
+ * created on the partition in addFkRecurseReferencing(). They are
+ * also passed to tryAttachPartitionForeignKey() below to simply
+ * assign as parents to the partition's existing "check" triggers,
+ * that is, if the corresponding constraints is deemed attachable to
+ * the parent constraint.
*/
- GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
- constrForm->confrelid, constrForm->conrelid,
- &insertTriggerOid, &updateTriggerOid);
+ if (constrForm->conenforced)
+ GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
+ constrForm->confrelid, constrForm->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
/*
* Before creating a new constraint, see whether any existing FKs are
@@ -11288,6 +11323,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
fkconstraint->fk_del_set_cols = NIL;
fkconstraint->old_conpfeqop = NIL;
fkconstraint->old_pktable_oid = InvalidOid;
+ fkconstraint->is_enforced = constrForm->conenforced;
fkconstraint->skip_validation = false;
fkconstraint->initially_valid = constrForm->convalidated;
for (int i = 0; i < numfks; i++)
@@ -11372,8 +11408,6 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
ScanKeyData key;
SysScanDesc scan;
HeapTuple trigtup;
- Oid insertTriggerOid,
- updateTriggerOid;
parentConstrTup = SearchSysCache1(CONSTROID,
ObjectIdGetDatum(parentConstrOid));
@@ -11415,6 +11449,7 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
!partConstr->convalidated ||
partConstr->condeferrable != parentConstr->condeferrable ||
partConstr->condeferred != parentConstr->condeferred ||
+ partConstr->conenforced != parentConstr->conenforced ||
partConstr->confupdtype != parentConstr->confupdtype ||
partConstr->confdeltype != parentConstr->confdeltype ||
partConstr->confmatchtype != parentConstr->confmatchtype)
@@ -11474,18 +11509,25 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
ConstraintSetParentConstraint(fk->conoid, parentConstrOid, partRelid);
/*
- * Like the constraint, attach partition's "check" triggers to the
- * corresponding parent triggers.
+ * Similar to the constraint, attach the partition's "check" triggers to
+ * the corresponding parent triggers if the constraint is enforced.
+ * Non-enforced constraints do not have these triggers.
*/
- GetForeignKeyCheckTriggers(trigrel,
- fk->conoid, fk->confrelid, fk->conrelid,
- &insertTriggerOid, &updateTriggerOid);
- Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
- TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
- partRelid);
- Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
- TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
- partRelid);
+ if (fk->conenforced)
+ {
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
+ partRelid);
+ Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
+ partRelid);
+ }
/*
* If the referenced table is partitioned, then the partition we're
@@ -11699,8 +11741,8 @@ GetForeignKeyCheckTriggers(Relation trigrel,
* InvalidObjectAddress.
*/
static ObjectAddress
-ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
- bool recursing, LOCKMODE lockmode)
+ATExecAlterConstraint(List **wqueue, Relation rel, AlterTableCmd *cmd,
+ bool recurse, bool recursing, LOCKMODE lockmode)
{
Constraint *cmdcon;
Relation conrel;
@@ -11802,12 +11844,14 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
address = InvalidObjectAddress;
if (currcon->condeferrable != cmdcon->deferrable ||
currcon->condeferred != cmdcon->initdeferred ||
+ currcon->conenforced != cmdcon->is_enforced ||
rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
- if (ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, currcon->conrelid,
- currcon->confrelid, contuple, &otherrelids,
- lockmode, InvalidOid, InvalidOid,
- InvalidOid, InvalidOid))
+ if (ATExecAlterConstrRecurse(wqueue, cmdcon, conrel, tgrel,
+ currcon->conrelid, currcon->confrelid,
+ contuple, &otherrelids, lockmode,
+ InvalidOid, InvalidOid, InvalidOid,
+ InvalidOid))
ObjectAddressSet(address, ConstraintRelationId, currcon->oid);
}
@@ -11839,8 +11883,8 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
* but existing releases don't do that.)
*/
static bool
-ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
- const Oid fkrelid, const Oid pkrelid,
+ATExecAlterConstrRecurse(List **wqueue, Constraint *cmdcon, Relation conrel,
+ Relation tgrel, const Oid fkrelid, const Oid pkrelid,
HeapTuple contuple, List **otherrelids,
LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
Oid ReferencedParentUpdTrigger,
@@ -11849,19 +11893,18 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
{
Form_pg_constraint currcon;
Oid conoid;
+ Oid conrelid;
Oid refrelid;
bool changed = false;
- Relation rel;
/* since this function recurses, it could be driven to stack overflow */
check_stack_depth();
currcon = (Form_pg_constraint) GETSTRUCT(contuple);
conoid = currcon->oid;
+ conrelid = currcon->conrelid;
refrelid = currcon->confrelid;
- rel = table_open(currcon->conrelid, lockmode);
-
/*
* Update pg_constraint with the flags from cmdcon.
*
@@ -11869,7 +11912,8 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
* silently do nothing.
*/
if (currcon->condeferrable != cmdcon->deferrable ||
- currcon->condeferred != cmdcon->initdeferred)
+ currcon->condeferred != cmdcon->initdeferred ||
+ currcon->conenforced != cmdcon->is_enforced)
{
HeapTuple copyTuple;
Form_pg_constraint copy_con;
@@ -11878,6 +11922,17 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
copy_con->condeferrable = cmdcon->deferrable;
copy_con->condeferred = cmdcon->initdeferred;
+ copy_con->conenforced = cmdcon->is_enforced;
+
+ /*
+ * NB: The convalidated status is irrelevant when the constraint is set to
+ * NOT ENFORCED, but for consistency, it should be set accordingly.
+ * This ensures that if the constraint is later changed to ENFORCED, it
+ * will automatically be in the correct NOT VALID state.
+ */
+ if (!cmdcon->is_enforced)
+ copy_con->convalidated = false;
+
CatalogTupleUpdate(conrel, ©Tuple->t_self, copyTuple);
InvokeObjectPostAlterHook(ConstraintRelationId,
@@ -11887,38 +11942,187 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
changed = true;
/* Make new constraint flags visible to others */
- CacheInvalidateRelcache(rel);
+ CacheInvalidateRelcacheByRelid(conrelid);
+ }
+ if (currcon->conenforced != cmdcon->is_enforced)
+ {
+ ATExecAlterConstrEnforceability(wqueue, cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, contuple, otherrelids, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
+ }
+ else
+ {
/*
* Now we need to update the multiple entries in pg_trigger that
* implement the constraint.
*/
- AlterConstrTriggerDeferrability(conoid, tgrel, rel, cmdcon->deferrable,
- cmdcon->initdeferred, otherrelids);
+ AlterConstrTriggerDeferrability(conoid, tgrel, conrelid,
+ cmdcon->deferrable,
+ cmdcon->initdeferred,
+ otherrelids);
+
+ /*
+ * If the table at either end of the constraint is partitioned, we
+ * need to recurse and handle every constraint that is a child of this
+ * one.
+ *
+ * (This assumes that the recurse flag is forcibly set for partitioned
+ * tables, and not set for legacy inheritance, though we don't check
+ * for that here.)
+ */
+ if (get_rel_relkind(conrelid) == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)
+ ATExecAlterChildConstr(wqueue, cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, contuple, otherrelids, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
}
- /*
- * If the table at either end of the constraint is partitioned, we need to
- * recurse and handle every constraint that is a child of this one.
- *
- * (This assumes that the recurse flag is forcibly set for partitioned
- * tables, and not set for legacy inheritance, though we don't check for
- * that here.)
- */
- if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
- get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)
- ATExecAlterChildConstr(cmdcon, conrel, tgrel, fkrelid, pkrelid,
- contuple, otherrelids, lockmode,
- ReferencedParentDelTrigger,
- ReferencedParentUpdTrigger,
- ReferencingParentInsTrigger,
- ReferencingParentUpdTrigger);
-
- table_close(rel, NoLock);
-
return changed;
}
+/*
+ * A subroutine of ATExecAlterConstrRecurse that updates the enforceability of
+ * a foreign key constraint. Depending on whether the constraint is being set
+ * to enforced or not enforced, it creates or drops the trigger accordingly.
+ *
+ * The arguments to this function have the same meaning as the arguments to
+ * ATExecAlterConstrRecurse.
+ */
+static void
+ATExecAlterConstrEnforceability(List **wqueue, Constraint *cmdcon,
+ Relation conrel, Relation tgrel,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger)
+{
+ Form_pg_constraint currcon;
+ Oid conoid;
+
+ currcon = (Form_pg_constraint) GETSTRUCT(contuple);
+ conoid = currcon->oid;
+
+ /* Drop triggers */
+ if (!cmdcon->is_enforced)
+ {
+ HeapTuple tgtuple;
+ ScanKeyData tgkey;
+ SysScanDesc tgscan;
+
+ /*
+ * When setting a constraint to NOT ENFORCED, the constraint triggers
+ * need to be dropped. Therefore, we must process the child relations
+ * first, followed by the parent, to account for dependencies.
+ */
+ if (get_rel_relkind(currcon->conrelid) == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE)
+ ATExecAlterChildConstr(wqueue, cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, contuple, otherrelids, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
+
+ ScanKeyInit(&tgkey,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+ tgscan = systable_beginscan(tgrel, TriggerConstraintIndexId, true,
+ NULL, 1, &tgkey);
+ while (HeapTupleIsValid(tgtuple = systable_getnext(tgscan)))
+ {
+ Form_pg_trigger tgform = (Form_pg_trigger) GETSTRUCT(tgtuple);
+ ObjectAddress trigger;
+
+ /*
+ * Delete only RI_FKey_noaction_del, RI_FKey_noaction_upd,
+ * RI_FKey_check_ins and RI_FKey_check_upd triggers, but not
+ * others; see createForeignKeyActionTriggers and
+ * CreateFKCheckTrigger.
+ */
+ if (tgform->tgfoid != F_RI_FKEY_NOACTION_DEL &&
+ tgform->tgfoid != F_RI_FKEY_NOACTION_UPD &&
+ tgform->tgfoid != F_RI_FKEY_CHECK_INS &&
+ tgform->tgfoid != F_RI_FKEY_CHECK_UPD)
+ continue;
+
+ deleteDependencyRecordsFor(TriggerRelationId, tgform->oid,
+ false);
+ /* make dependency deletion visible to performDeletion */
+ CommandCounterIncrement();
+ ObjectAddressSet(trigger, TriggerRelationId, tgform->oid);
+ performDeletion(&trigger, DROP_RESTRICT, 0);
+ /* make trigger drop visible, in case the loop iterates */
+ CommandCounterIncrement();
+ }
+
+ systable_endscan(tgscan);
+ }
+ else /* Create triggers */
+ {
+ Relation rel;
+ Oid ReferencedDelTriggerOid = InvalidOid,
+ ReferencedUpdTriggerOid = InvalidOid,
+ ReferencingInsTriggerOid = InvalidOid,
+ ReferencingUpdTriggerOid = InvalidOid;
+
+ /* Update the foreign key action required for trigger creation. */
+ cmdcon->fk_matchtype = currcon->confmatchtype;
+ cmdcon->fk_upd_action = currcon->confupdtype;
+ cmdcon->fk_del_action = currcon->confdeltype;
+
+ rel = table_open(currcon->conrelid, lockmode);
+
+ /* Create referenced triggers */
+ if (currcon->conrelid == fkrelid)
+ createForeignKeyActionTriggers(rel,
+ currcon->confrelid,
+ cmdcon,
+ conoid,
+ currcon->conindid,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ &ReferencedDelTriggerOid,
+ &ReferencedUpdTriggerOid);
+
+ /* Create referencing triggers */
+ if (currcon->confrelid == pkrelid)
+ createForeignKeyCheckTriggers(currcon->conrelid,
+ pkrelid,
+ cmdcon,
+ conoid,
+ currcon->conindid,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger,
+ &ReferencingInsTriggerOid,
+ &ReferencingUpdTriggerOid);
+
+ /*
+ * If the table at either end of the constraint is partitioned, we
+ * need to recurse and create triggers for each constraint that is a
+ * child of this one.
+ */
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE)
+ ATExecAlterChildConstr(wqueue, cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, contuple, otherrelids, lockmode,
+ ReferencedDelTriggerOid,
+ ReferencedUpdTriggerOid,
+ ReferencingInsTriggerOid,
+ ReferencingUpdTriggerOid);
+ table_close(rel, NoLock);
+ }
+}
+
/*
* A subroutine of ATExecAlterConstrRecurse that updated constraint trigger's
* deferrability.
@@ -11927,7 +12131,7 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
* ATExecAlterConstrRecurse.
*/
static void
-AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
+AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Oid fkrel,
bool deferrable, bool initdeferred,
List **otherrelids)
{
@@ -11953,7 +12157,7 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
* other rels that don't have a trigger whose properties change, but
* let's be conservative.)
*/
- if (tgform->tgrelid != RelationGetRelid(rel))
+ if (tgform->tgrelid != fkrel)
*otherrelids = list_append_unique_oid(*otherrelids,
tgform->tgrelid);
@@ -11992,8 +12196,8 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
* ATExecAlterConstrRecurse.
*/
static void
-ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
- const Oid fkrelid, const Oid pkrelid,
+ATExecAlterChildConstr(List **wqueue, Constraint *cmdcon, Relation conrel,
+ Relation tgrel, const Oid fkrelid, const Oid pkrelid,
HeapTuple contuple, List **otherrelids,
LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
Oid ReferencedParentUpdTrigger,
@@ -12018,9 +12222,9 @@ ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
true, NULL, 1, &pkey);
while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
- ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, fkrelid, pkrelid,
- childtup, otherrelids, lockmode,
- ReferencedParentDelTrigger,
+ ATExecAlterConstrRecurse(wqueue, cmdcon, conrel, tgrel,
+ fkrelid, pkrelid, childtup, otherrelids,
+ lockmode, ReferencedParentDelTrigger,
ReferencedParentUpdTrigger,
ReferencingParentInsTrigger,
ReferencingParentUpdTrigger);
@@ -19964,8 +20168,6 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
Form_pg_constraint conform;
- Oid insertTriggerOid,
- updateTriggerOid;
contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
if (!HeapTupleIsValid(contup))
@@ -19988,17 +20190,25 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
/*
* Also, look up the partition's "check" triggers corresponding to the
- * constraint being detached and detach them from the parent triggers.
+ * enforced constraint being detached and detach them from the parent
+ * triggers. Non-enforced constraints do not have these triggers;
+ * therefore, this step is not needed.
*/
- GetForeignKeyCheckTriggers(trigrel,
- fk->conoid, fk->confrelid, fk->conrelid,
- &insertTriggerOid, &updateTriggerOid);
- Assert(OidIsValid(insertTriggerOid));
- TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
- RelationGetRelid(partRel));
- Assert(OidIsValid(updateTriggerOid));
- TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
- RelationGetRelid(partRel));
+ if (fk->conenforced)
+ {
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ Assert(OidIsValid(updateTriggerOid));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ }
/*
* Lastly, create the action triggers on the referenced table, using
@@ -20038,8 +20248,9 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
fkconstraint->conname = pstrdup(NameStr(conform->conname));
fkconstraint->deferrable = conform->condeferrable;
fkconstraint->initdeferred = conform->condeferred;
+ fkconstraint->is_enforced = conform->conenforced;
fkconstraint->skip_validation = true;
- fkconstraint->initially_valid = true;
+ fkconstraint->initially_valid = conform->convalidated;
/* a few irrelevant fields omitted here */
fkconstraint->pktable = NULL;
fkconstraint->fk_attrs = NIL;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 833b3be02be..e8119d90914 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -2660,7 +2660,7 @@ alter_table_cmd:
processCASbits($4, @4, "ALTER CONSTRAINT statement",
&c->deferrable,
&c->initdeferred,
- NULL, NULL, NULL, yyscanner);
+ &c->is_enforced, NULL, NULL, yyscanner);
$$ = (Node *) n;
}
/* ALTER TABLE <name> VALIDATE CONSTRAINT ... */
@@ -4027,6 +4027,7 @@ ColConstraintElem:
n->fk_del_set_cols = ($5)->deleteAction->cols;
n->skip_validation = false;
n->initially_valid = true;
+ n->is_enforced = true;
$$ = (Node *) n;
}
;
@@ -4302,7 +4303,7 @@ ConstraintElem:
n->fk_del_set_cols = ($11)->deleteAction->cols;
processCASbits($12, @12, "FOREIGN KEY",
&n->deferrable, &n->initdeferred,
- NULL, &n->skip_validation,
+ &n->is_enforced, &n->skip_validation,
NULL,
yyscanner);
n->initially_valid = !n->skip_validation;
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index fe1116c0922..6060cf619dd 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -2961,8 +2961,10 @@ transformFKConstraints(CreateStmtContext *cxt,
/*
* If CREATE TABLE or adding a column with NULL default, we can safely
- * skip validation of FK constraints, and nonetheless mark them valid.
- * (This will override any user-supplied NOT VALID flag.)
+ * skip validation of FK constraints, and mark them as valid based on the
+ * constraint enforcement flag, since NOT ENFORCED constraints must always
+ * be marked as NOT VALID. (This will override any user-supplied NOT VALID
+ * flag.)
*/
if (skipValidation)
{
@@ -2971,7 +2973,7 @@ transformFKConstraints(CreateStmtContext *cxt,
Constraint *constraint = (Constraint *) lfirst(fkclist);
constraint->skip_validation = true;
- constraint->initially_valid = true;
+ constraint->initially_valid = constraint->is_enforced;
}
}
@@ -3966,7 +3968,8 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
case CONSTR_ATTR_ENFORCED:
if (lastprimarycon == NULL ||
- lastprimarycon->contype != CONSTR_CHECK)
+ (lastprimarycon->contype != CONSTR_CHECK &&
+ lastprimarycon->contype != CONSTR_FOREIGN))
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("misplaced ENFORCED clause"),
@@ -3982,7 +3985,8 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
case CONSTR_ATTR_NOT_ENFORCED:
if (lastprimarycon == NULL ||
- lastprimarycon->contype != CONSTR_CHECK)
+ (lastprimarycon->contype != CONSTR_CHECK &&
+ lastprimarycon->contype != CONSTR_FOREIGN))
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("misplaced NOT ENFORCED clause"),
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 0809f03aca2..0efbcca190d 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4700,6 +4700,7 @@ RelationGetFKeyList(Relation relation)
info->conoid = constraint->oid;
info->conrelid = constraint->conrelid;
info->confrelid = constraint->confrelid;
+ info->conenforced = constraint->conenforced;
DeconstructFkConstraintRow(htup, &info->nkeys,
info->conkey,
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 87002049538..2db011ddfb7 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -284,6 +284,9 @@ typedef struct ForeignKeyCacheInfo
/* number of columns in the foreign key */
int nkeys;
+ /* Is enforced ? */
+ bool conenforced;
+
/*
* these arrays each have nkeys valid entries:
*/
diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out
index b68ac47a3cd..c0b6f204223 100644
--- a/src/test/regress/expected/alter_table.out
+++ b/src/test/regress/expected/alter_table.out
@@ -514,7 +514,7 @@ DELETE FROM attmp3 WHERE NOT b > 10;
ALTER TABLE attmp3 VALIDATE CONSTRAINT b_greater_than_ten; -- succeeds
ALTER TABLE attmp3 VALIDATE CONSTRAINT b_greater_than_ten; -- succeeds
ALTER TABLE attmp3 VALIDATE CONSTRAINT b_greater_than_ten_not_enforced; -- fail
-ERROR: cannot validated NOT ENFORCED constraint
+ERROR: cannot validate NOT ENFORCED constraint
-- Test inherited NOT VALID CHECK constraints
select * from attmp3;
a | b
diff --git a/src/test/regress/expected/constraints.out b/src/test/regress/expected/constraints.out
index 692a69fe457..f8767c5cacf 100644
--- a/src/test/regress/expected/constraints.out
+++ b/src/test/regress/expected/constraints.out
@@ -746,13 +746,9 @@ LINE 1: CREATE TABLE UNIQUE_NOTEN_TBL(i int UNIQUE NOT ENFORCED);
^
-- XXX: error message is misleading here
ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
-ERROR: ALTER CONSTRAINT statement constraints cannot be marked ENFORCED
-LINE 1: ...TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
- ^
+ERROR: constraint "unique_tbl_i_key" of relation "unique_tbl" is not a foreign key constraint
ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORCED;
-ERROR: ALTER CONSTRAINT statement constraints cannot be marked NOT ENFORCED
-LINE 1: ...ABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORC...
- ^
+ERROR: constraint "unique_tbl_i_key" of relation "unique_tbl" is not a foreign key constraint
DROP TABLE unique_tbl;
--
-- EXCLUDE constraints
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 3f459f70ac1..d448af38454 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -1,12 +1,44 @@
--
-- FOREIGN KEY
--
--- MATCH FULL
+-- NOT ENFORCED
--
-- First test, check and cascade
--
CREATE TABLE PKTABLE ( ptest1 int PRIMARY KEY, ptest2 text );
-CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE, ftest2 int );
+CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE NOT ENFORCED,
+ ftest2 int );
+-- Inserting into the foreign key table will not result in an error, even if
+-- there is no matching key in the referenced table.
+INSERT INTO FKTABLE VALUES (1, 2);
+INSERT INTO FKTABLE VALUES (2, 3);
+-- Check FKTABLE
+SELECT * FROM FKTABLE;
+ ftest1 | ftest2
+--------+--------
+ 1 | 2
+ 2 | 3
+(2 rows)
+
+-- Changing it back to ENFORCED will leave the constraint in the NOT VALID state
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+-- Which needs to be explicitly validated.
+ALTER TABLE FKTABLE VALIDATE CONSTRAINT fktable_ftest1_fkey;
+ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_fkey"
+DETAIL: Key (ftest1)=(1) is not present in table "pktable".
+-- Remove any existing rows that violate the constraint, then attempt to change
+-- it.
+TRUNCATE FKTABLE;
+ALTER TABLE FKTABLE VALIDATE CONSTRAINT fktable_ftest1_fkey;
+-- Any further inserts will fail due to the enforcement.
+INSERT INTO FKTABLE VALUES (3, 4);
+ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_fkey"
+DETAIL: Key (ftest1)=(3) is not present in table "pktable".
+--
+-- MATCH FULL
+--
+-- First test, check and cascade
+--
-- Insert test data into PKTABLE
INSERT INTO PKTABLE VALUES (1, 'Test1');
INSERT INTO PKTABLE VALUES (2, 'Test2');
@@ -351,6 +383,8 @@ INSERT INTO FKTABLE VALUES (1, NULL);
ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL;
ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_ftest2_fkey"
DETAIL: MATCH FULL does not allow mixing of null and nonnull key values.
+-- NOT ENFORCED will bypass the initial check.
+ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL NOT ENFORCED;
DROP TABLE FKTABLE;
DROP TABLE PKTABLE;
-- MATCH SIMPLE
@@ -1276,13 +1310,28 @@ INSERT INTO fktable VALUES (0, 20);
ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_fk_fkey"
DETAIL: Key (fk)=(20) is not present in table "pktable".
COMMIT;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT ENFORCED;
+BEGIN;
+-- doesn't match FK, no error.
+UPDATE pktable SET id = 10 WHERE id = 5;
+-- doesn't match PK, no error.
+INSERT INTO fktable VALUES (0, 20);
+ROLLBACK;
-- try additional syntax
-ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE ENFORCED;
-- illegal option
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE INITIALLY DEFERRED;
ERROR: constraint declared INITIALLY DEFERRED must be DEFERRABLE
LINE 1: ...e ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE INITIALLY ...
^
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORCED;
+ERROR: conflicting constraint properties
+LINE 1: ...fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORC...
+ ^
+CREATE TEMP TABLE fktable2 (fk int references pktable ENFORCED NOT ENFORCED);
+ERROR: multiple ENFORCED/NOT ENFORCED clauses not allowed
+LINE 1: ...ABLE fktable2 (fk int references pktable ENFORCED NOT ENFORC...
+ ^
-- test order of firing of FK triggers when several RI-induced changes need to
-- be made to the same row. This was broken by subtransaction-related
-- changes in 8.0.
@@ -1580,7 +1629,8 @@ ALTER TABLE fk_partitioned_fk DROP COLUMN fdrop1;
CREATE TABLE fk_partitioned_fk_1 (fdrop1 int, fdrop2 int, a int, fdrop3 int, b int);
ALTER TABLE fk_partitioned_fk_1 DROP COLUMN fdrop1, DROP COLUMN fdrop2, DROP COLUMN fdrop3;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FROM (0,0) TO (1000,1000);
-ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk;
+ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
@@ -1663,7 +1713,38 @@ DETAIL: Key (a, b)=(2500, 2502) is still referenced from table "fk_partitioned_
Indexes:
"fk_notpartitioned_pk_pkey" PRIMARY KEY, btree (a, b)
Referenced by:
- TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b)
+ TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) NOT VALID
+
+-- Check the exsting FK trigger
+CREATE TEMP TABLE tmp_trg_info AS SELECT tgtype, tgrelid::regclass, tgconstraint
+FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+SELECT count(1) FROM tmp_trg_info;
+ count
+-------
+ 14
+(1 row)
+
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+-- No triggers
+SELECT count(1) FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+ count
+-------
+ 0
+(1 row)
+
+-- Changing it back to ENFORCED will recreate the necessary triggers.
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+-- Should be exactly the same number of triggers found as before
+SELECT COUNT(1) FROM pg_trigger pgt JOIN tmp_trg_info tt
+ON (pgt.tgtype = tt.tgtype AND pgt.tgrelid = tt.tgrelid AND pgt.tgconstraint = tt.tgconstraint);
+ count
+-------
+ 14
+(1 row)
ALTER TABLE fk_partitioned_fk DROP CONSTRAINT fk_partitioned_fk_a_b_fkey;
-- done.
@@ -1868,6 +1949,41 @@ Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
Foreign-key constraints:
TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
+DROP TABLE fk_partitioned_fk_2;
+CREATE TABLE fk_partitioned_fk_2 (b int, a int,
+ FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
+BEGIN;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have two constraints
+\d fk_partitioned_fk_2
+ Table "public.fk_partitioned_fk_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ b | integer | | |
+ a | integer | | |
+Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
+Foreign-key constraints:
+ "fk_partitioned_fk_2_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED
+ TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
+
+DROP TABLE fk_partitioned_fk_2;
+ROLLBACK;
+BEGIN;
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have only one constraint
+\d fk_partitioned_fk_2
+ Table "public.fk_partitioned_fk_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ b | integer | | |
+ a | integer | | |
+Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
+Foreign-key constraints:
+ "fk_partitioned_fk_2_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED
+ TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED
+
+ROLLBACK;
DROP TABLE fk_partitioned_fk_2;
CREATE TABLE fk_partitioned_fk_4 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE) PARTITION BY RANGE (b, a);
CREATE TABLE fk_partitioned_fk_4_1 PARTITION OF fk_partitioned_fk_4 FOR VALUES FROM (1,1) TO (100,100);
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index 2e710e419c2..9b23fe8cf8f 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -2,13 +2,40 @@
-- FOREIGN KEY
--
--- MATCH FULL
+-- NOT ENFORCED
--
-- First test, check and cascade
--
CREATE TABLE PKTABLE ( ptest1 int PRIMARY KEY, ptest2 text );
-CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE, ftest2 int );
+CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE NOT ENFORCED,
+ ftest2 int );
+-- Inserting into the foreign key table will not result in an error, even if
+-- there is no matching key in the referenced table.
+INSERT INTO FKTABLE VALUES (1, 2);
+INSERT INTO FKTABLE VALUES (2, 3);
+
+-- Check FKTABLE
+SELECT * FROM FKTABLE;
+
+-- Changing it back to ENFORCED will leave the constraint in the NOT VALID state
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+-- Which needs to be explicitly validated.
+ALTER TABLE FKTABLE VALIDATE CONSTRAINT fktable_ftest1_fkey;
+
+-- Remove any existing rows that violate the constraint, then attempt to change
+-- it.
+TRUNCATE FKTABLE;
+ALTER TABLE FKTABLE VALIDATE CONSTRAINT fktable_ftest1_fkey;
+
+-- Any further inserts will fail due to the enforcement.
+INSERT INTO FKTABLE VALUES (3, 4);
+
+--
+-- MATCH FULL
+--
+-- First test, check and cascade
+--
-- Insert test data into PKTABLE
INSERT INTO PKTABLE VALUES (1, 'Test1');
INSERT INTO PKTABLE VALUES (2, 'Test2');
@@ -230,6 +257,9 @@ INSERT INTO FKTABLE VALUES (1, NULL);
ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL;
+-- NOT ENFORCED will bypass the initial check.
+ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL NOT ENFORCED;
+
DROP TABLE FKTABLE;
DROP TABLE PKTABLE;
@@ -968,10 +998,23 @@ INSERT INTO fktable VALUES (0, 20);
COMMIT;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT ENFORCED;
+
+BEGIN;
+
+-- doesn't match FK, no error.
+UPDATE pktable SET id = 10 WHERE id = 5;
+-- doesn't match PK, no error.
+INSERT INTO fktable VALUES (0, 20);
+
+ROLLBACK;
+
-- try additional syntax
-ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE ENFORCED;
-- illegal option
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORCED;
+CREATE TEMP TABLE fktable2 (fk int references pktable ENFORCED NOT ENFORCED);
-- test order of firing of FK triggers when several RI-induced changes need to
-- be made to the same row. This was broken by subtransaction-related
@@ -1182,7 +1225,8 @@ ALTER TABLE fk_partitioned_fk DROP COLUMN fdrop1;
CREATE TABLE fk_partitioned_fk_1 (fdrop1 int, fdrop2 int, a int, fdrop3 int, b int);
ALTER TABLE fk_partitioned_fk_1 DROP COLUMN fdrop1, DROP COLUMN fdrop2, DROP COLUMN fdrop3;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FROM (0,0) TO (1000,1000);
-ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk;
+ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
@@ -1236,6 +1280,27 @@ UPDATE fk_notpartitioned_pk SET b = 1502 WHERE a = 1500;
UPDATE fk_notpartitioned_pk SET b = 2504 WHERE a = 2500;
-- check psql behavior
\d fk_notpartitioned_pk
+
+-- Check the exsting FK trigger
+CREATE TEMP TABLE tmp_trg_info AS SELECT tgtype, tgrelid::regclass, tgconstraint
+FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+SELECT count(1) FROM tmp_trg_info;
+
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+-- No triggers
+SELECT count(1) FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+
+-- Changing it back to ENFORCED will recreate the necessary triggers.
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+
+-- Should be exactly the same number of triggers found as before
+SELECT COUNT(1) FROM pg_trigger pgt JOIN tmp_trg_info tt
+ON (pgt.tgtype = tt.tgtype AND pgt.tgrelid = tt.tgrelid AND pgt.tgconstraint = tt.tgconstraint);
+
ALTER TABLE fk_partitioned_fk DROP CONSTRAINT fk_partitioned_fk_a_b_fkey;
-- done.
DROP TABLE fk_notpartitioned_pk, fk_partitioned_fk;
@@ -1372,6 +1437,22 @@ ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN
\d fk_partitioned_fk_2
DROP TABLE fk_partitioned_fk_2;
+CREATE TABLE fk_partitioned_fk_2 (b int, a int,
+ FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
+BEGIN;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have two constraints
+\d fk_partitioned_fk_2
+DROP TABLE fk_partitioned_fk_2;
+ROLLBACK;
+BEGIN;
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have only one constraint
+\d fk_partitioned_fk_2
+ROLLBACK;
+DROP TABLE fk_partitioned_fk_2;
+
CREATE TABLE fk_partitioned_fk_4 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE) PARTITION BY RANGE (b, a);
CREATE TABLE fk_partitioned_fk_4_1 PARTITION OF fk_partitioned_fk_4 FOR VALUES FROM (1,1) TO (100,100);
CREATE TABLE fk_partitioned_fk_4_2 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE SET NULL);
--
2.43.5
hi.
only applied v7-0001.
alter_table.sgml says we can specify enforceability
for ALTER TABLE ADD column_constraint
and ALTER TABLE ADD column_constraint table_constraint.
but we didn't have a test for column_constraint in alter_table.sql
so segmental fault happened again:
create table tx(a int);
alter table tx add column b text collate "C" constraint cc check (a >
1) not enforced;
alter table tx add column b text collate "C" constraint cc check (b <>
'h') not enforced;
------------------------------------------------------------------------
errmsg("multiple ENFORCED/NOT ENFORCED clauses not allowed"),
never tested.
here are the tests:
CREATE TABLE t5(x int CHECK (x > 3) NOT ENFORCED enforced , b int);
CREATE TABLE t5(x int CHECK (x > 3) ENFORCED not enforced , b int);
------------------------------------------------------------------------
create foreign table with column_constraint, segmental fault also
reproduce:
DO $d$
BEGIN
EXECUTE $$CREATE SERVER loopback FOREIGN DATA WRAPPER postgres_fdw
OPTIONS (dbname '$$||current_database()||$$',
port '$$||current_setting('port')||$$'
)$$;
EXECUTE $$CREATE SERVER loopback2 FOREIGN DATA WRAPPER postgres_fdw
OPTIONS (dbname '$$||current_database()||$$',
port '$$||current_setting('port')||$$'
)$$;
EXECUTE $$CREATE SERVER loopback3 FOREIGN DATA WRAPPER postgres_fdw
OPTIONS (dbname '$$||current_database()||$$',
port '$$||current_setting('port')||$$'
)$$;
END;
$d$;
CREATE USER MAPPING FOR CURRENT_USER SERVER loopback;
CREATE FOREIGN TABLE ft1 (c0 int constraint cc check (c0 > 1) not
enforced) SERVER loopback;
On Mon, Dec 9, 2024 at 9:40 PM jian he <jian.universality@gmail.com> wrote:
hi.
only applied v7-0001.alter_table.sgml says we can specify enforceability
for ALTER TABLE ADD column_constraint
and ALTER TABLE ADD column_constraint table_constraint.
but we didn't have a test for column_constraint in alter_table.sqlso segmental fault happened again:
This is an assertion failure introduced by the patch to ensure that a
NOT ENFORCED constraint is marked as invalid. The failure occurs
because skip_validation and initially_valid were not set inside
transformConstraintAttrs(). I will post an updated version of the
patch tomorrow after conducting some additional testing. Thanks for
the test.
Regards,
Amul
hi. some minor issue about v7-0001.
there are 5 appearances of "sizeof(CookedConstraint)"
to make it safe, it would be nice to manual do
`
cooked->is_enforced = true;
`
for other kinds of constraints.
static bool
MergeWithExistingConstraint(Relation rel, const char *ccname, Node *expr,
bool allow_merge, bool is_local,
+ bool is_enforced,
bool is_initially_valid,
bool is_no_inherit)
{
@@ -2729,12 +2738,24 @@ MergeWithExistingConstraint(Relation rel,
const char *ccname, Node *expr,
* If the child constraint is "not valid" then cannot merge with a
* valid parent constraint.
*/
- if (is_initially_valid && !con->convalidated)
+ if (is_initially_valid && con->conenforced && !con->convalidated)
ereport(ERROR,
(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
errmsg("constraint \"%s\" conflicts with NOT VALID constraint on
relation \"%s\"",
ccname, RelationGetRelationName(rel))));
There are no tests for this change. I think this change is not necessary.
- a/src/test/regress/expected/alter_table.out
+++ b/src/test/regress/expected/alter_table.out
...
+ALTER TABLE attmp3 VALIDATE CONSTRAINT b_greater_than_ten_not_enforced; -- fail
+ERROR: cannot validated NOT ENFORCED constraint
there should be
ERROR: cannot validate NOT ENFORCED constraint
?
Do we need to update create_foreign_table.sgml
and alter_foreign_table.sgml?
On Tue, Dec 10, 2024 at 1:21 PM jian he <jian.universality@gmail.com> wrote:
hi. some minor issue about v7-0001.
there are 5 appearances of "sizeof(CookedConstraint)"
to make it safe, it would be nice to manual do
`
cooked->is_enforced = true;
`
for other kinds of constraints.
I am not sure if it's necessary, but it doesn't seem like a bad idea,
did the same in the attached version.
static bool MergeWithExistingConstraint(Relation rel, const char *ccname, Node *expr, bool allow_merge, bool is_local, + bool is_enforced, bool is_initially_valid, bool is_no_inherit) { @@ -2729,12 +2738,24 @@ MergeWithExistingConstraint(Relation rel, const char *ccname, Node *expr, * If the child constraint is "not valid" then cannot merge with a * valid parent constraint. */ - if (is_initially_valid && !con->convalidated) + if (is_initially_valid && con->conenforced && !con->convalidated) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), errmsg("constraint \"%s\" conflicts with NOT VALID constraint on relation \"%s\"", ccname, RelationGetRelationName(rel))));There are no tests for this change. I think this change is not necessary.
It is necessary; otherwise, it would raise an error for a NOT ENFORCED
constraint, which is NOT VALID by default.
- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out ... +ALTER TABLE attmp3 VALIDATE CONSTRAINT b_greater_than_ten_not_enforced; -- fail +ERROR: cannot validated NOT ENFORCED constraintthere should be
ERROR: cannot validate NOT ENFORCED constraint
?
Are you sure you're looking at the latest patch? The error string is
already the same as you suggested.
Do we need to update create_foreign_table.sgml
and alter_foreign_table.sgml?
Yes, I think we just need to add the syntax; a description isn't
necessary, imo, since constraints on foreign constraints are never
enforced.
Regards,
Amul
Attachments:
v8-0001-Add-support-for-NOT-ENFORCED-in-CHECK-constraints.patchapplication/octet-stream; name=v8-0001-Add-support-for-NOT-ENFORCED-in-CHECK-constraints.patchDownload
From 7e94a7550a8289e6e29b15e7c44b00f3985d2b66 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Wed, 4 Dec 2024 10:24:40 +0530
Subject: [PATCH v8 1/5] Add support for NOT ENFORCED in CHECK constraints.
This added basic infrastructure for the NOT ENFORCED/ENFORCED
constraint supports where incluing grammar and catalog flags.
Note that CHECK constraints do not currently support ALTER operations,
so changing the enforceability of an existing constraint isn't
possible without dropping and recreating it. We could consider
adding support for altering CHECK constraints either in this
patch series or as a separatly.
---
doc/src/sgml/catalogs.sgml | 10 ++
doc/src/sgml/ddl.sgml | 8 +-
doc/src/sgml/information_schema.sgml | 4 +-
doc/src/sgml/ref/alter_table.sgml | 12 +-
doc/src/sgml/ref/create_foreign_table.sgml | 6 +-
doc/src/sgml/ref/create_table.sgml | 25 ++++-
src/backend/access/common/tupdesc.c | 2 +
src/backend/catalog/heap.c | 57 ++++++++--
src/backend/catalog/index.c | 1 +
src/backend/catalog/information_schema.sql | 2 +-
src/backend/catalog/pg_constraint.c | 9 ++
src/backend/commands/tablecmds.c | 51 ++++++++-
src/backend/commands/trigger.c | 1 +
src/backend/commands/typecmds.c | 16 +++
src/backend/executor/execMain.c | 8 +-
src/backend/nodes/makefuncs.c | 1 +
src/backend/optimizer/util/plancat.c | 12 ++
src/backend/parser/gram.y | 106 ++++++++++++++----
src/backend/parser/parse_utilcmd.c | 57 +++++++++-
src/backend/utils/adt/ruleutils.c | 6 +-
src/backend/utils/cache/relcache.c | 1 +
src/include/access/tupdesc.h | 1 +
src/include/catalog/catversion.h | 2 +-
src/include/catalog/heap.h | 1 +
src/include/catalog/pg_constraint.h | 2 +
src/include/nodes/parsenodes.h | 3 +
src/include/parser/kwlist.h | 1 +
src/test/regress/expected/alter_table.out | 10 ++
src/test/regress/expected/constraints.out | 40 ++++++-
.../regress/expected/create_table_like.out | 10 +-
src/test/regress/expected/domain.out | 15 +++
src/test/regress/expected/inherit.out | 94 ++++++++++++++--
src/test/regress/sql/alter_table.sql | 7 ++
src/test/regress/sql/constraints.sql | 22 +++-
src/test/regress/sql/create_table_like.sql | 3 +-
src/test/regress/sql/domain.sql | 8 ++
src/test/regress/sql/inherit.sql | 47 +++++++-
37 files changed, 589 insertions(+), 72 deletions(-)
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index bf3cee08a93..a56ef4df562 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2602,6 +2602,16 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l
</para></entry>
</row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>conenforced</structfield> <type>bool</type>
+ </para>
+ <para>
+ Is the constraint enforceable?
+ Currently, can be false only for CHECK constraints
+ </para></entry>
+ </row>
+
<row>
<entry role="catalog_table_entry"><para role="column_definition">
<structfield>convalidated</structfield> <type>bool</type>
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index dea04d64db6..9a20396dae4 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -584,7 +584,10 @@ CREATE TABLE products (
consists of the key word <literal>CHECK</literal> followed by an
expression in parentheses. The check constraint expression should
involve the column thus constrained, otherwise the constraint
- would not make too much sense.
+ would not make too much sense. The check constraint expression can be
+ followed by the <literal>ENFORCED</literal>, which is the default, or
+ <literal>NOT ENFORCED</literal> keyword, determining whether the
+ constraint check is performed or skipped after each statement.
</para>
<indexterm>
@@ -720,7 +723,8 @@ CREATE TABLE products (
to implement that. (This approach avoids the dump/restore problem because
<application>pg_dump</application> does not reinstall triggers until after
restoring data, so that the check will not be enforced during a
- dump/restore.)
+ dump/restore.) You can add that CHECK constraint with <literal>NOT ENFORCED</literal>,
+ allowing the underlying constraint rule to be represented without being enforced.
</para>
</note>
diff --git a/doc/src/sgml/information_schema.sgml b/doc/src/sgml/information_schema.sgml
index 9442b0718c0..19dffe7be6a 100644
--- a/doc/src/sgml/information_schema.sgml
+++ b/doc/src/sgml/information_schema.sgml
@@ -6896,9 +6896,7 @@ ORDER BY c.ordinal_position;
<structfield>enforced</structfield> <type>yes_or_no</type>
</para>
<para>
- Applies to a feature not available in
- <productname>PostgreSQL</productname> (currently always
- <literal>YES</literal>)
+ <literal>YES</literal> if the constraint is enforced, <literal>NO</literal> if not
</para></entry>
</row>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index c8f7ab7d956..938450fba18 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -108,7 +108,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
PRIMARY KEY <replaceable class="parameter">index_parameters</replaceable> |
REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> ) ] [ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ]
[ ON DELETE <replaceable class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ] }
-[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]
<phrase>and <replaceable class="parameter">table_constraint</replaceable> is:</phrase>
@@ -120,7 +120,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
EXCLUDE [ USING <replaceable class="parameter">index_method</replaceable> ] ( <replaceable class="parameter">exclude_element</replaceable> WITH <replaceable class="parameter">operator</replaceable> [, ... ] ) <replaceable class="parameter">index_parameters</replaceable> [ WHERE ( <replaceable class="parameter">predicate</replaceable> ) ] |
FOREIGN KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">column_name</replaceable> ] ) REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">refcolumn</replaceable> ] ) ]
[ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ] [ ON DELETE <replaceable class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ] }
-[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]
<phrase>and <replaceable class="parameter">table_constraint_using_index</replaceable> is:</phrase>
@@ -1423,9 +1423,11 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</para>
<para>
- Adding a <literal>CHECK</literal> or <literal>NOT NULL</literal> constraint requires
- scanning the table to verify that existing rows meet the constraint,
- but does not require a table rewrite.
+ Adding an enforced <literal>CHECK</literal> or <literal>NOT NULL</literal>
+ constraint requires scanning the table to verify that existing rows meet the
+ constraint, but does not require a table rewrite. If a <literal>CHECK</literal>
+ constraint is added as <literal>NOT ENFORCED</literal>, the validation will
+ not be performed.
</para>
<para>
diff --git a/doc/src/sgml/ref/create_foreign_table.sgml b/doc/src/sgml/ref/create_foreign_table.sgml
index fc81ba3c498..0dcd9ca6f87 100644
--- a/doc/src/sgml/ref/create_foreign_table.sgml
+++ b/doc/src/sgml/ref/create_foreign_table.sgml
@@ -48,12 +48,14 @@ CREATE FOREIGN TABLE [ IF NOT EXISTS ] <replaceable class="parameter">table_name
CHECK ( <replaceable class="parameter">expression</replaceable> ) [ NO INHERIT ] |
DEFAULT <replaceable>default_expr</replaceable> |
GENERATED ALWAYS AS ( <replaceable>generation_expr</replaceable> ) STORED }
+[ ENFORCED | NOT ENFORCED ]
<phrase>and <replaceable class="parameter">table_constraint</replaceable> is:</phrase>
[ CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> ]
- NOT NULL <replaceable class="parameter">column_name</replaceable> [ NO INHERIT ] |
-CHECK ( <replaceable class="parameter">expression</replaceable> ) [ NO INHERIT ]
+{ NOT NULL <replaceable class="parameter">column_name</replaceable> [ NO INHERIT ] |
+ CHECK ( <replaceable class="parameter">expression</replaceable> ) [ NO INHERIT ] }
+[ ENFORCED | NOT ENFORCED ]
<phrase>and <replaceable class="parameter">partition_bound_spec</replaceable> is:</phrase>
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index 70fa929caa4..cfd01c4ef2b 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -71,7 +71,7 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI
PRIMARY KEY <replaceable class="parameter">index_parameters</replaceable> |
REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> ) ] [ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ]
[ ON DELETE <replaceable class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ] }
-[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]
<phrase>and <replaceable class="parameter">table_constraint</replaceable> is:</phrase>
@@ -84,7 +84,7 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI
FOREIGN KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">column_name</replaceable> ] ) REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">refcolumn</replaceable> ] ) ]
[ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ] [ ON DELETE <replaceable
class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ] }
-[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]
<phrase>and <replaceable class="parameter">like_option</replaceable> is:</phrase>
@@ -1377,6 +1377,27 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</listitem>
</varlistentry>
+ <varlistentry id="sql-createtable-parms-enforce">
+ <term><literal>ENFORCED</literal></term>
+ <term><literal>NOT ENFORCED</literal></term>
+ <listitem>
+ <para>
+ This is currently only allowed for <literal>CHECK</literal> constraints.
+ If the constraint is <literal>NOT ENFORCED</literal>, this clause
+ specifies that the constraint check will be skipped. When the constraint
+ is <literal>ENFORCED</literal>, check is performed after each statement;
+ the latter is the default.
+ </para>
+ <para>
+ Note that if the constraint is <literal>NOT ENFORCED</literal>, it is also
+ marked as <literal>NOT VALID</literal>. While this might seem unnecessary,
+ it is done for consistency and should be set accordingly. This ensures
+ that if the constraint is later changed to <literal>ENFORCED</literal>, it
+ will automatically be in the correct <literal>NOT VALID</literal> state.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="sql-createtable-method">
<term><literal>USING <replaceable class="parameter">method</replaceable></literal></term>
<listitem>
diff --git a/src/backend/access/common/tupdesc.c b/src/backend/access/common/tupdesc.c
index 47379fef220..6c64e5b9e71 100644
--- a/src/backend/access/common/tupdesc.c
+++ b/src/backend/access/common/tupdesc.c
@@ -224,6 +224,7 @@ CreateTupleDescCopyConstr(TupleDesc tupdesc)
{
cpy->check[i].ccname = pstrdup(constr->check[i].ccname);
cpy->check[i].ccbin = pstrdup(constr->check[i].ccbin);
+ cpy->check[i].ccenforced = constr->check[i].ccenforced;
cpy->check[i].ccvalid = constr->check[i].ccvalid;
cpy->check[i].ccnoinherit = constr->check[i].ccnoinherit;
}
@@ -547,6 +548,7 @@ equalTupleDescs(TupleDesc tupdesc1, TupleDesc tupdesc2)
if (!(strcmp(check1->ccname, check2->ccname) == 0 &&
strcmp(check1->ccbin, check2->ccbin) == 0 &&
+ check1->ccenforced == check2->ccenforced &&
check1->ccvalid == check2->ccvalid &&
check1->ccnoinherit == check2->ccnoinherit))
return false;
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index d7b88b61dcc..b386ae03eb1 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -101,13 +101,14 @@ static ObjectAddress AddNewRelationType(const char *typeName,
Oid new_row_type,
Oid new_array_type);
static void RelationRemoveInheritance(Oid relid);
-static Oid StoreRelCheck(Relation rel, const char *ccname, Node *expr,
- bool is_validated, bool is_local, int16 inhcount,
- bool is_no_inherit, bool is_internal);
+static Oid StoreRelCheck(Relation rel, const char *ccname, Node *expr,
+ bool is_enforced, bool is_validated, bool is_local,
+ int16 inhcount, bool is_no_inherit, bool is_internal);
static void StoreConstraints(Relation rel, List *cooked_constraints,
bool is_internal);
static bool MergeWithExistingConstraint(Relation rel, const char *ccname, Node *expr,
bool allow_merge, bool is_local,
+ bool is_enforced,
bool is_initially_valid,
bool is_no_inherit);
static void SetRelationNumChecks(Relation rel, int numchecks);
@@ -2074,8 +2075,8 @@ SetAttrMissing(Oid relid, char *attname, char *value)
*/
static Oid
StoreRelCheck(Relation rel, const char *ccname, Node *expr,
- bool is_validated, bool is_local, int16 inhcount,
- bool is_no_inherit, bool is_internal)
+ bool is_enforced, bool is_validated, bool is_local,
+ int16 inhcount, bool is_no_inherit, bool is_internal)
{
char *ccbin;
List *varList;
@@ -2140,6 +2141,7 @@ StoreRelCheck(Relation rel, const char *ccname, Node *expr,
CONSTRAINT_CHECK, /* Constraint Type */
false, /* Is Deferrable */
false, /* Is Deferred */
+ is_enforced, /* Is Enforced */
is_validated,
InvalidOid, /* no parent constraint */
RelationGetRelid(rel), /* relation */
@@ -2193,6 +2195,7 @@ StoreRelNotNull(Relation rel, const char *nnname, AttrNumber attnum,
CONSTRAINT_NOTNULL,
false,
false,
+ true, /* Is Enforced */
is_validated,
InvalidOid,
RelationGetRelid(rel),
@@ -2262,9 +2265,9 @@ StoreConstraints(Relation rel, List *cooked_constraints, bool is_internal)
case CONSTR_CHECK:
con->conoid =
StoreRelCheck(rel, con->name, con->expr,
- !con->skip_validation, con->is_local,
- con->inhcount, con->is_no_inherit,
- is_internal);
+ con->is_enforced, !con->skip_validation,
+ con->is_local, con->inhcount,
+ con->is_no_inherit, is_internal);
numchecks++;
break;
@@ -2405,6 +2408,7 @@ AddRelationNewConstraints(Relation rel,
cooked->name = NULL;
cooked->attnum = colDef->attnum;
cooked->expr = expr;
+ cooked->is_enforced = true;
cooked->skip_validation = false;
cooked->is_local = is_local;
cooked->inhcount = is_local ? 0 : 1;
@@ -2476,6 +2480,7 @@ AddRelationNewConstraints(Relation rel,
*/
if (MergeWithExistingConstraint(rel, ccname, expr,
allow_merge, is_local,
+ cdef->is_enforced,
cdef->initially_valid,
cdef->is_no_inherit))
continue;
@@ -2524,8 +2529,10 @@ AddRelationNewConstraints(Relation rel,
* OK, store it.
*/
constrOid =
- StoreRelCheck(rel, ccname, expr, cdef->initially_valid, is_local,
- is_local ? 0 : 1, cdef->is_no_inherit, is_internal);
+ StoreRelCheck(rel, ccname, expr, cdef->is_enforced,
+ cdef->initially_valid, is_local,
+ is_local ? 0 : 1, cdef->is_no_inherit,
+ is_internal);
numchecks++;
@@ -2535,6 +2542,7 @@ AddRelationNewConstraints(Relation rel,
cooked->name = ccname;
cooked->attnum = 0;
cooked->expr = expr;
+ cooked->is_enforced = cdef->is_enforced;
cooked->skip_validation = cdef->skip_validation;
cooked->is_local = is_local;
cooked->inhcount = is_local ? 0 : 1;
@@ -2605,6 +2613,7 @@ AddRelationNewConstraints(Relation rel,
nncooked->name = nnname;
nncooked->attnum = colnum;
nncooked->expr = NULL;
+ nncooked->is_enforced = true;
nncooked->skip_validation = cdef->skip_validation;
nncooked->is_local = is_local;
nncooked->inhcount = inhcount;
@@ -2639,6 +2648,7 @@ AddRelationNewConstraints(Relation rel,
static bool
MergeWithExistingConstraint(Relation rel, const char *ccname, Node *expr,
bool allow_merge, bool is_local,
+ bool is_enforced,
bool is_initially_valid,
bool is_no_inherit)
{
@@ -2729,12 +2739,24 @@ MergeWithExistingConstraint(Relation rel, const char *ccname, Node *expr,
* If the child constraint is "not valid" then cannot merge with a
* valid parent constraint.
*/
- if (is_initially_valid && !con->convalidated)
+ if (is_initially_valid && con->conenforced && !con->convalidated)
ereport(ERROR,
(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
errmsg("constraint \"%s\" conflicts with NOT VALID constraint on relation \"%s\"",
ccname, RelationGetRelationName(rel))));
+ /*
+ * A non-enforced child constraint cannot be merged with an enforced
+ * parent constraint. However, the reverse is allowed, where the child
+ * constraint is enforced.
+ */
+ if ((!is_local && is_enforced && !con->conenforced) ||
+ (is_local && !is_enforced && con->conenforced))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("constraint \"%s\" conflicts with NOT ENFORCED constraint on relation \"%s\"",
+ ccname, RelationGetRelationName(rel))));
+
/* OK to update the tuple */
ereport(NOTICE,
(errmsg("merging constraint \"%s\" with inherited definition",
@@ -2770,6 +2792,19 @@ MergeWithExistingConstraint(Relation rel, const char *ccname, Node *expr,
con->connoinherit = true;
}
+ /*
+ * If the child constraint is required to be enforced while the parent
+ * 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.
+ */
+ if (is_enforced && !con->conenforced)
+ {
+ Assert(is_local);
+ con->conenforced = true;
+ con->convalidated = true;
+ }
+
CatalogTupleUpdate(conDesc, &tup->t_self, tup);
}
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 1c3a9e06d37..f4cbaf8953a 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -1957,6 +1957,7 @@ index_constraint_create(Relation heapRelation,
constraintType,
deferrable,
initdeferred,
+ true, /* Is Enforced */
true,
parentConstraintId,
RelationGetRelid(heapRelation),
diff --git a/src/backend/catalog/information_schema.sql b/src/backend/catalog/information_schema.sql
index 76c78c0d184..bd241130381 100644
--- a/src/backend/catalog/information_schema.sql
+++ b/src/backend/catalog/information_schema.sql
@@ -1844,7 +1844,7 @@ CREATE VIEW table_constraints AS
AS is_deferrable,
CAST(CASE WHEN c.condeferred THEN 'YES' ELSE 'NO' END AS yes_or_no)
AS initially_deferred,
- CAST('YES' AS yes_or_no) AS enforced,
+ CAST(CASE WHEN c.conenforced THEN 'YES' ELSE 'NO' END AS yes_or_no) AS enforced,
CAST(CASE WHEN c.contype = 'u'
THEN CASE WHEN (SELECT NOT indnullsnotdistinct FROM pg_index WHERE indexrelid = conindid) THEN 'YES' ELSE 'NO' END
END
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index 9c05a98d28c..1a201ebeadb 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -53,6 +53,7 @@ CreateConstraintEntry(const char *constraintName,
char constraintType,
bool isDeferrable,
bool isDeferred,
+ bool isEnforced,
bool isValidated,
Oid parentConstrId,
Oid relId,
@@ -99,6 +100,11 @@ CreateConstraintEntry(const char *constraintName,
ObjectAddresses *addrs_auto;
ObjectAddresses *addrs_normal;
+ /* Only CHECK constraint can be not enforced */
+ Assert(isEnforced || constraintType == CONSTRAINT_CHECK);
+ /* NOT ENFORCED constraint must be NOT VALID */
+ Assert(isEnforced || !isValidated);
+
conDesc = table_open(ConstraintRelationId, RowExclusiveLock);
Assert(constraintName);
@@ -182,6 +188,7 @@ CreateConstraintEntry(const char *constraintName,
values[Anum_pg_constraint_contype - 1] = CharGetDatum(constraintType);
values[Anum_pg_constraint_condeferrable - 1] = BoolGetDatum(isDeferrable);
values[Anum_pg_constraint_condeferred - 1] = BoolGetDatum(isDeferred);
+ values[Anum_pg_constraint_conenforced - 1] = BoolGetDatum(isEnforced);
values[Anum_pg_constraint_convalidated - 1] = BoolGetDatum(isValidated);
values[Anum_pg_constraint_conrelid - 1] = ObjectIdGetDatum(relId);
values[Anum_pg_constraint_contypid - 1] = ObjectIdGetDatum(domainId);
@@ -822,6 +829,7 @@ RelationGetNotNullConstraints(Oid relid, bool cooked, bool include_noinh)
cooked->name = pstrdup(NameStr(conForm->conname));
cooked->attnum = colnum;
cooked->expr = NULL;
+ cooked->is_enforced = true;
cooked->skip_validation = false;
cooked->is_local = true;
cooked->inhcount = 0;
@@ -841,6 +849,7 @@ RelationGetNotNullConstraints(Oid relid, bool cooked, bool include_noinh)
constr->location = -1;
constr->keys = list_make1(makeString(get_attname(relid, colnum,
false)));
+ constr->is_enforced = true;
constr->skip_validation = false;
constr->initially_valid = true;
constr->is_no_inherit = conForm->connoinherit;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 6ccae4cb4a8..e3bb4176642 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -373,7 +373,7 @@ static void RangeVarCallbackForTruncate(const RangeVar *relation,
static List *MergeAttributes(List *columns, const List *supers, char relpersistence,
bool is_partition, List **supconstr,
List **supnotnulls);
-static List *MergeCheckConstraint(List *constraints, const char *name, Node *expr);
+static List *MergeCheckConstraint(List *constraints, const char *name, Node *expr, bool is_enforced);
static void MergeChildAttribute(List *inh_columns, int exist_attno, int newcol_attno, const ColumnDef *newdef);
static ColumnDef *MergeInheritedAttribute(List *inh_columns, int exist_attno, const ColumnDef *newdef);
static void MergeAttributesIntoExisting(Relation child_rel, Relation parent_rel, bool ispartition);
@@ -973,6 +973,7 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
cooked->name = NULL;
cooked->attnum = attnum;
cooked->expr = colDef->cooked_default;
+ cooked->is_enforced = true;
cooked->skip_validation = false;
cooked->is_local = true; /* not used for defaults */
cooked->inhcount = 0; /* ditto */
@@ -2885,7 +2886,8 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
name,
RelationGetRelationName(relation))));
- constraints = MergeCheckConstraint(constraints, name, expr);
+ constraints = MergeCheckConstraint(constraints, name, expr,
+ check[i].ccenforced);
}
}
@@ -3099,7 +3101,7 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
* the list.
*/
static List *
-MergeCheckConstraint(List *constraints, const char *name, Node *expr)
+MergeCheckConstraint(List *constraints, const char *name, Node *expr, bool is_enforced)
{
ListCell *lc;
CookedConstraint *newcon;
@@ -3122,6 +3124,17 @@ MergeCheckConstraint(List *constraints, const char *name, Node *expr)
ereport(ERROR,
errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
errmsg("too many inheritance parents"));
+
+ /*
+ * When enforceability differs, the merged constraint should be
+ * marked as ENFORCED because one of the parents is ENFORCED.
+ */
+ if (!ccon->is_enforced && is_enforced)
+ {
+ ccon->is_enforced = true;
+ ccon->skip_validation = false;
+ }
+
return constraints;
}
@@ -3140,6 +3153,8 @@ MergeCheckConstraint(List *constraints, const char *name, Node *expr)
newcon->name = pstrdup(name);
newcon->expr = expr;
newcon->inhcount = 1;
+ newcon->is_enforced = is_enforced;
+ newcon->skip_validation = !is_enforced;
return lappend(constraints, newcon);
}
@@ -10418,6 +10433,7 @@ addFkConstraint(addFkConstraintSides fkside,
CONSTRAINT_FOREIGN,
fkconstraint->deferrable,
fkconstraint->initdeferred,
+ true, /* Is Enforced */
fkconstraint->initially_valid,
parentConstr,
RelationGetRelid(rel),
@@ -12004,6 +12020,11 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
errmsg("constraint \"%s\" of relation \"%s\" is not a foreign key or check constraint",
constrName, RelationGetRelationName(rel))));
+ if (!con->conenforced)
+ ereport(ERROR,
+ (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("cannot validate NOT ENFORCED constraint")));
+
if (!con->convalidated)
{
AlteredTableInfo *tab;
@@ -16249,6 +16270,9 @@ decompile_conbin(HeapTuple contup, TupleDesc tupdesc)
* The test we apply is to see whether they reverse-compile to the same
* source string. This insulates us from issues like whether attributes
* have the same physical column numbers in parent and child relations.
+ *
+ * Note that we ignore enforceability as there are cases where constraints
+ * with differing enforceability are allowed.
*/
static bool
constraints_equivalent(HeapTuple a, HeapTuple b, TupleDesc tupleDesc)
@@ -16518,12 +16542,24 @@ MergeConstraintsIntoExisting(Relation child_rel, Relation parent_rel)
* If the child constraint is "not valid" then cannot merge with a
* valid parent constraint
*/
- if (parent_con->convalidated && !child_con->convalidated)
+ if (parent_con->convalidated && child_con->conenforced &&
+ !child_con->convalidated)
ereport(ERROR,
(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
errmsg("constraint \"%s\" conflicts with NOT VALID constraint on child table \"%s\"",
NameStr(child_con->conname), RelationGetRelationName(child_rel))));
+ /*
+ * A non-enforced child constraint cannot be merged with an
+ * enforced parent constraint. However, the reverse is allowed,
+ * where the child constraint is enforced.
+ */
+ if (parent_con->conenforced && !child_con->conenforced)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("constraint \"%s\" conflicts with NOT ENFORCED constraint on child table \"%s\"",
+ NameStr(child_con->conname), RelationGetRelationName(child_rel))));
+
/*
* OK, bump the child constraint's inheritance count. (If we fail
* later on, this change will just roll back.)
@@ -18875,6 +18911,12 @@ ConstraintImpliedByRelConstraint(Relation scanrel, List *testConstraint, List *p
if (!constr->check[i].ccvalid)
continue;
+ /*
+ * NOT ENFORCED constraints are always marked as invalid, which
+ * should have been ignored.
+ */
+ Assert(constr->check[i].ccenforced);
+
cexpr = stringToNode(constr->check[i].ccbin);
/*
@@ -20185,6 +20227,7 @@ DetachAddConstraintIfNeeded(List **wqueue, Relation partRel)
n->is_no_inherit = false;
n->raw_expr = NULL;
n->cooked_expr = nodeToString(make_ands_explicit(constraintExpr));
+ n->is_enforced = true;
n->initially_valid = true;
n->skip_validation = true;
/* It's a re-add, since it nominally already exists */
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 09356e46d16..49d3fc22983 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -809,6 +809,7 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
CONSTRAINT_TRIGGER,
stmt->deferrable,
stmt->initdeferred,
+ true, /* Is Enforced */
true,
InvalidOid, /* no parent */
RelationGetRelid(rel),
diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c
index da591c0922b..59f3cd9a5a3 100644
--- a/src/backend/commands/typecmds.c
+++ b/src/backend/commands/typecmds.c
@@ -1018,6 +1018,13 @@ DefineDomain(CreateDomainStmt *stmt)
errmsg("specifying GENERATED not supported for domains")));
break;
+ case CONSTR_ATTR_ENFORCED:
+ case CONSTR_ATTR_NOT_ENFORCED:
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("specifying constraint enforceability not supported for domains")));
+ break;
+
/* no default, to let compiler warn about missing case */
}
}
@@ -2975,6 +2982,13 @@ AlterDomainAddConstraint(List *names, Node *newConstraint,
errmsg("specifying constraint deferrability not supported for domains")));
break;
+ case CONSTR_ATTR_ENFORCED:
+ case CONSTR_ATTR_NOT_ENFORCED:
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("specifying constraint enforceability not supported for domains")));
+ break;
+
default:
elog(ERROR, "unrecognized constraint subtype: %d",
(int) constr->contype);
@@ -3604,6 +3618,7 @@ domainAddCheckConstraint(Oid domainOid, Oid domainNamespace, Oid baseTypeOid,
CONSTRAINT_CHECK, /* Constraint Type */
false, /* Is Deferrable */
false, /* Is Deferred */
+ true, /* Is Enforced */
!constr->skip_validation, /* Is Validated */
InvalidOid, /* no parent constraint */
InvalidOid, /* not a relation constraint */
@@ -3711,6 +3726,7 @@ domainAddNotNullConstraint(Oid domainOid, Oid domainNamespace, Oid baseTypeOid,
CONSTRAINT_NOTNULL, /* Constraint Type */
false, /* Is Deferrable */
false, /* Is Deferred */
+ true, /* Is Enforced */
!constr->skip_validation, /* Is Validated */
InvalidOid, /* no parent constraint */
InvalidOid, /* not a relation constraint */
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 13f3fcdaee9..c4707abde65 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1742,11 +1742,15 @@ ExecRelCheck(ResultRelInfo *resultRelInfo,
{
oldContext = MemoryContextSwitchTo(estate->es_query_cxt);
resultRelInfo->ri_ConstraintExprs =
- (ExprState **) palloc(ncheck * sizeof(ExprState *));
+ (ExprState **) palloc0(ncheck * sizeof(ExprState *));
for (i = 0; i < ncheck; i++)
{
Expr *checkconstr;
+ /* Skip not enforced constraint */
+ if (!check[i].ccenforced)
+ continue;
+
checkconstr = stringToNode(check[i].ccbin);
resultRelInfo->ri_ConstraintExprs[i] =
ExecPrepareExpr(checkconstr, estate);
@@ -1773,7 +1777,7 @@ ExecRelCheck(ResultRelInfo *resultRelInfo,
* is not to be treated as a failure. Therefore, use ExecCheck not
* ExecQual.
*/
- if (!ExecCheck(checkconstr, econtext))
+ if (checkconstr && !ExecCheck(checkconstr, econtext))
return check[i].ccname;
}
diff --git a/src/backend/nodes/makefuncs.c b/src/backend/nodes/makefuncs.c
index 7e5df7bea4d..07ad4f5fc1a 100644
--- a/src/backend/nodes/makefuncs.c
+++ b/src/backend/nodes/makefuncs.c
@@ -453,6 +453,7 @@ makeNotNullConstraint(String *colname)
notnull->initdeferred = false;
notnull->location = -1;
notnull->keys = list_make1(colname);
+ notnull->is_enforced = true;
notnull->skip_validation = false;
notnull->initially_valid = true;
diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c
index 37b0ca2e439..155453e1ba0 100644
--- a/src/backend/optimizer/util/plancat.c
+++ b/src/backend/optimizer/util/plancat.c
@@ -1303,9 +1303,21 @@ get_relation_constraints(PlannerInfo *root,
*/
if (!constr->check[i].ccvalid)
continue;
+
+ /*
+ * NOT ENFORCED constraints are always marked as invalid, which
+ * should have been ignored.
+ */
+ Assert(constr->check[i].ccenforced);
+
+ /*
+ * Also ignore if NO INHERIT and we weren't told
+ * that that's safe.
+ */
if (constr->check[i].ccnoinherit && !include_noinherit)
continue;
+
cexpr = stringToNode(constr->check[i].ccbin);
/*
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 67eb96396af..26766e2892f 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -143,6 +143,8 @@ typedef struct KeyActions
#define CAS_INITIALLY_DEFERRED 0x08
#define CAS_NOT_VALID 0x10
#define CAS_NO_INHERIT 0x20
+#define CAS_NOT_ENFORCED 0x40
+#define CAS_ENFORCED 0x80
#define parser_yyerror(msg) scanner_yyerror(msg, yyscanner)
@@ -196,8 +198,8 @@ static void SplitColQualList(List *qualList,
List **constraintList, CollateClause **collClause,
core_yyscan_t yyscanner);
static void processCASbits(int cas_bits, int location, const char *constrType,
- bool *deferrable, bool *initdeferred, bool *not_valid,
- bool *no_inherit, core_yyscan_t yyscanner);
+ bool *deferrable, bool *initdeferred, bool *is_enforced,
+ bool *not_valid, bool *no_inherit, core_yyscan_t yyscanner);
static PartitionStrategy parsePartitionStrategy(char *strategy, int location,
core_yyscan_t yyscanner);
static void preprocess_pubobj_list(List *pubobjspec_list,
@@ -711,9 +713,9 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
DETACH DICTIONARY DISABLE_P DISCARD DISTINCT DO DOCUMENT_P DOMAIN_P
DOUBLE_P DROP
- EACH ELSE EMPTY_P ENABLE_P ENCODING ENCRYPTED END_P ENUM_P ERROR_P ESCAPE
- EVENT EXCEPT EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION
- EXTENSION EXTERNAL EXTRACT
+ EACH ELSE EMPTY_P ENABLE_P ENCODING ENCRYPTED END_P ENFORCED ENUM_P ERROR_P
+ ESCAPE EVENT EXCEPT EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN
+ EXPRESSION EXTENSION EXTERNAL EXTRACT
FALSE_P FAMILY FETCH FILTER FINALIZE FIRST_P FLOAT_P FOLLOWING FOR
FORCE FOREIGN FORMAT FORWARD FREEZE FROM FULL FUNCTION FUNCTIONS
@@ -2658,7 +2660,7 @@ alter_table_cmd:
processCASbits($4, @4, "ALTER CONSTRAINT statement",
&c->deferrable,
&c->initdeferred,
- NULL, NULL, yyscanner);
+ NULL, NULL, NULL, yyscanner);
$$ = (Node *) n;
}
/* ALTER TABLE <name> VALIDATE CONSTRAINT ... */
@@ -3915,6 +3917,7 @@ ColConstraintElem:
n->contype = CONSTR_NOTNULL;
n->location = @1;
n->is_no_inherit = $3;
+ n->is_enforced = true;
n->skip_validation = false;
n->initially_valid = true;
$$ = (Node *) n;
@@ -3961,6 +3964,7 @@ ColConstraintElem:
n->is_no_inherit = $5;
n->raw_expr = $3;
n->cooked_expr = NULL;
+ n->is_enforced = true;
n->skip_validation = false;
n->initially_valid = true;
$$ = (Node *) n;
@@ -4022,6 +4026,7 @@ ColConstraintElem:
n->fk_upd_action = ($5)->updateAction->action;
n->fk_del_action = ($5)->deleteAction->action;
n->fk_del_set_cols = ($5)->deleteAction->cols;
+ n->is_enforced = true;
n->skip_validation = false;
n->initially_valid = true;
$$ = (Node *) n;
@@ -4087,6 +4092,22 @@ ConstraintAttr:
n->location = @1;
$$ = (Node *) n;
}
+ | ENFORCED
+ {
+ Constraint *n = makeNode(Constraint);
+
+ n->contype = CONSTR_ATTR_ENFORCED;
+ n->location = @1;
+ $$ = (Node *) n;
+ }
+ | NOT ENFORCED
+ {
+ Constraint *n = makeNode(Constraint);
+
+ n->contype = CONSTR_ATTR_NOT_ENFORCED;
+ n->location = @1;
+ $$ = (Node *) n;
+ }
;
@@ -4148,7 +4169,8 @@ ConstraintElem:
n->raw_expr = $3;
n->cooked_expr = NULL;
processCASbits($5, @5, "CHECK",
- NULL, NULL, &n->skip_validation,
+ NULL, NULL, &n->is_enforced,
+ &n->skip_validation,
&n->is_no_inherit, yyscanner);
n->initially_valid = !n->skip_validation;
$$ = (Node *) n;
@@ -4162,7 +4184,7 @@ ConstraintElem:
n->keys = list_make1(makeString($3));
/* no NOT VALID support yet */
processCASbits($4, @4, "NOT NULL",
- NULL, NULL, NULL,
+ NULL, NULL, NULL, NULL,
&n->is_no_inherit, yyscanner);
n->initially_valid = true;
$$ = (Node *) n;
@@ -4183,7 +4205,7 @@ ConstraintElem:
n->indexspace = $9;
processCASbits($10, @10, "UNIQUE",
&n->deferrable, &n->initdeferred, NULL,
- NULL, yyscanner);
+ NULL, NULL, yyscanner);
$$ = (Node *) n;
}
| UNIQUE ExistingIndex ConstraintAttributeSpec
@@ -4199,7 +4221,7 @@ ConstraintElem:
n->indexspace = NULL;
processCASbits($3, @3, "UNIQUE",
&n->deferrable, &n->initdeferred, NULL,
- NULL, yyscanner);
+ NULL, NULL, yyscanner);
$$ = (Node *) n;
}
| PRIMARY KEY '(' columnList opt_without_overlaps ')' opt_c_include opt_definition OptConsTableSpace
@@ -4217,7 +4239,7 @@ ConstraintElem:
n->indexspace = $9;
processCASbits($10, @10, "PRIMARY KEY",
&n->deferrable, &n->initdeferred, NULL,
- NULL, yyscanner);
+ NULL, NULL, yyscanner);
$$ = (Node *) n;
}
| PRIMARY KEY ExistingIndex ConstraintAttributeSpec
@@ -4233,7 +4255,7 @@ ConstraintElem:
n->indexspace = NULL;
processCASbits($4, @4, "PRIMARY KEY",
&n->deferrable, &n->initdeferred, NULL,
- NULL, yyscanner);
+ NULL, NULL, yyscanner);
$$ = (Node *) n;
}
| EXCLUDE access_method_clause '(' ExclusionConstraintList ')'
@@ -4253,7 +4275,7 @@ ConstraintElem:
n->where_clause = $9;
processCASbits($10, @10, "EXCLUDE",
&n->deferrable, &n->initdeferred, NULL,
- NULL, yyscanner);
+ NULL, NULL, yyscanner);
$$ = (Node *) n;
}
| FOREIGN KEY '(' columnList optionalPeriodName ')' REFERENCES qualified_name
@@ -4282,7 +4304,8 @@ ConstraintElem:
n->fk_del_set_cols = ($11)->deleteAction->cols;
processCASbits($12, @12, "FOREIGN KEY",
&n->deferrable, &n->initdeferred,
- &n->skip_validation, NULL,
+ NULL, &n->skip_validation,
+ NULL,
yyscanner);
n->initially_valid = !n->skip_validation;
$$ = (Node *) n;
@@ -4322,8 +4345,9 @@ DomainConstraintElem:
n->raw_expr = $3;
n->cooked_expr = NULL;
processCASbits($5, @5, "CHECK",
- NULL, NULL, &n->skip_validation,
+ NULL, NULL, NULL, &n->skip_validation,
&n->is_no_inherit, yyscanner);
+ n->is_enforced = true;
n->initially_valid = !n->skip_validation;
$$ = (Node *) n;
}
@@ -4337,7 +4361,7 @@ DomainConstraintElem:
/* no NOT VALID, NO INHERIT support */
processCASbits($3, @3, "NOT NULL",
NULL, NULL, NULL,
- NULL, yyscanner);
+ NULL, NULL, yyscanner);
n->initially_valid = true;
$$ = (Node *) n;
}
@@ -6000,7 +6024,7 @@ CreateTrigStmt:
n->transitionRels = NIL;
processCASbits($11, @11, "TRIGGER",
&n->deferrable, &n->initdeferred, NULL,
- NULL, yyscanner);
+ NULL, NULL, yyscanner);
n->constrrel = $10;
$$ = (Node *) n;
}
@@ -6169,7 +6193,8 @@ ConstraintAttributeSpec:
parser_errposition(@2)));
/* generic message for other conflicts */
if ((newspec & (CAS_NOT_DEFERRABLE | CAS_DEFERRABLE)) == (CAS_NOT_DEFERRABLE | CAS_DEFERRABLE) ||
- (newspec & (CAS_INITIALLY_IMMEDIATE | CAS_INITIALLY_DEFERRED)) == (CAS_INITIALLY_IMMEDIATE | CAS_INITIALLY_DEFERRED))
+ (newspec & (CAS_INITIALLY_IMMEDIATE | CAS_INITIALLY_DEFERRED)) == (CAS_INITIALLY_IMMEDIATE | CAS_INITIALLY_DEFERRED) ||
+ (newspec & (CAS_NOT_ENFORCED | CAS_ENFORCED)) == (CAS_NOT_ENFORCED | CAS_ENFORCED))
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("conflicting constraint properties"),
@@ -6185,6 +6210,8 @@ ConstraintAttributeElem:
| INITIALLY DEFERRED { $$ = CAS_INITIALLY_DEFERRED; }
| NOT VALID { $$ = CAS_NOT_VALID; }
| NO INHERIT { $$ = CAS_NO_INHERIT; }
+ | NOT ENFORCED { $$ = CAS_NOT_ENFORCED; }
+ | ENFORCED { $$ = CAS_ENFORCED; }
;
@@ -17688,6 +17715,7 @@ unreserved_keyword:
| ENABLE_P
| ENCODING
| ENCRYPTED
+ | ENFORCED
| ENUM_P
| ERROR_P
| ESCAPE
@@ -18265,6 +18293,7 @@ bare_label_keyword:
| ENCODING
| ENCRYPTED
| END_P
+ | ENFORCED
| ENUM_P
| ERROR_P
| ESCAPE
@@ -19403,8 +19432,8 @@ SplitColQualList(List *qualList,
*/
static void
processCASbits(int cas_bits, int location, const char *constrType,
- bool *deferrable, bool *initdeferred, bool *not_valid,
- bool *no_inherit, core_yyscan_t yyscanner)
+ bool *deferrable, bool *initdeferred, bool *is_enforced,
+ bool *not_valid, bool *no_inherit, core_yyscan_t yyscanner)
{
/* defaults */
if (deferrable)
@@ -19413,6 +19442,8 @@ processCASbits(int cas_bits, int location, const char *constrType,
*initdeferred = false;
if (not_valid)
*not_valid = false;
+ if (is_enforced)
+ *is_enforced = true;
if (cas_bits & (CAS_DEFERRABLE | CAS_INITIALLY_DEFERRED))
{
@@ -19465,6 +19496,41 @@ processCASbits(int cas_bits, int location, const char *constrType,
constrType),
parser_errposition(location)));
}
+
+ if (cas_bits & CAS_NOT_ENFORCED)
+ {
+ if (is_enforced)
+ *is_enforced = false;
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ /* translator: %s is CHECK, UNIQUE, or similar */
+ errmsg("%s constraints cannot be marked NOT ENFORCED",
+ constrType),
+ parser_errposition(location)));
+
+ /*
+ * NB: The validated status is irrelevant when the constraint is set to
+ * NOT ENFORCED, but for consistency, it should be set accordingly.
+ * This ensures that if the constraint is later changed to ENFORCED, it
+ * will automatically be in the correct NOT VALIDATED state.
+ */
+ if (not_valid)
+ *not_valid = true;
+ }
+
+ if (cas_bits & CAS_ENFORCED)
+ {
+ if (is_enforced)
+ *is_enforced = true;
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ /* translator: %s is CHECK, UNIQUE, or similar */
+ errmsg("%s constraints cannot be marked ENFORCED",
+ constrType),
+ parser_errposition(location)));
+ }
}
/*
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 0f324ee4e31..815ade9eeec 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -954,6 +954,8 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column)
case CONSTR_ATTR_NOT_DEFERRABLE:
case CONSTR_ATTR_DEFERRED:
case CONSTR_ATTR_IMMEDIATE:
+ case CONSTR_ATTR_ENFORCED:
+ case CONSTR_ATTR_NOT_ENFORCED:
/* transformConstraintAttrs took care of these */
break;
@@ -1093,6 +1095,8 @@ transformTableConstraint(CreateStmtContext *cxt, Constraint *constraint)
case CONSTR_ATTR_NOT_DEFERRABLE:
case CONSTR_ATTR_DEFERRED:
case CONSTR_ATTR_IMMEDIATE:
+ case CONSTR_ATTR_ENFORCED:
+ case CONSTR_ATTR_NOT_ENFORCED:
elog(ERROR, "invalid context for constraint type %d",
constraint->contype);
break;
@@ -1433,6 +1437,8 @@ expandTableLikeClause(RangeVar *heapRel, TableLikeClause *table_like_clause)
{
char *ccname = constr->check[ccnum].ccname;
char *ccbin = constr->check[ccnum].ccbin;
+ bool ccenforced = constr->check[ccnum].ccenforced;
+ bool ccvalid = constr->check[ccnum].ccvalid;
bool ccnoinherit = constr->check[ccnum].ccnoinherit;
Node *ccbin_node;
bool found_whole_row;
@@ -1462,13 +1468,14 @@ expandTableLikeClause(RangeVar *heapRel, TableLikeClause *table_like_clause)
n->contype = CONSTR_CHECK;
n->conname = pstrdup(ccname);
n->location = -1;
+ n->is_enforced = ccenforced;
+ n->initially_valid = ccvalid;
n->is_no_inherit = ccnoinherit;
n->raw_expr = NULL;
n->cooked_expr = nodeToString(ccbin_node);
/* We can skip validation, since the new table should be empty. */
n->skip_validation = true;
- n->initially_valid = true;
atsubcmd = makeNode(AlterTableCmd);
atsubcmd->subtype = AT_AddConstraint;
@@ -2921,9 +2928,11 @@ transformCheckConstraints(CreateStmtContext *cxt, bool skipValidation)
return;
/*
- * If creating a new table (but not a foreign table), we can safely skip
- * validation of check constraints, and nonetheless mark them valid. (This
- * will override any user-supplied NOT VALID flag.)
+ * When creating a new table (but not a foreign table), we can safely skip
+ * the validation of check constraints and mark them as valid based on the
+ * constraint enforcement flag, since NOT ENFORCED constraints must always
+ * be marked as NOT VALID. (This will override any user-supplied
+ * NOT VALID flag.)
*/
if (skipValidation)
{
@@ -2932,7 +2941,7 @@ transformCheckConstraints(CreateStmtContext *cxt, bool skipValidation)
Constraint *constraint = (Constraint *) lfirst(ckclist);
constraint->skip_validation = true;
- constraint->initially_valid = true;
+ constraint->initially_valid = constraint->is_enforced;
}
}
}
@@ -3859,6 +3868,7 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
Constraint *lastprimarycon = NULL;
bool saw_deferrability = false;
bool saw_initially = false;
+ bool saw_enforced = false;
ListCell *clist;
#define SUPPORTS_ATTRS(node) \
@@ -3954,12 +3964,49 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
lastprimarycon->initdeferred = false;
break;
+ case CONSTR_ATTR_ENFORCED:
+ if (lastprimarycon == NULL ||
+ lastprimarycon->contype != CONSTR_CHECK)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("misplaced ENFORCED clause"),
+ parser_errposition(cxt->pstate, con->location)));
+ if (saw_enforced)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("multiple ENFORCED/NOT ENFORCED clauses not allowed"),
+ parser_errposition(cxt->pstate, con->location)));
+ saw_enforced = true;
+ lastprimarycon->is_enforced = true;
+ break;
+
+ case CONSTR_ATTR_NOT_ENFORCED:
+ if (lastprimarycon == NULL ||
+ lastprimarycon->contype != CONSTR_CHECK)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("misplaced NOT ENFORCED clause"),
+ parser_errposition(cxt->pstate, con->location)));
+ if (saw_enforced)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("multiple ENFORCED/NOT ENFORCED clauses not allowed"),
+ parser_errposition(cxt->pstate, con->location)));
+ saw_enforced = true;
+ lastprimarycon->is_enforced = false;
+
+ /* A NOT ENFORCED constraint must be marked as invalid. */
+ lastprimarycon->skip_validation = true;
+ lastprimarycon->initially_valid = false;
+ break;
+
default:
/* Otherwise it's not an attribute */
lastprimarycon = con;
/* reset flags for new primary node */
saw_deferrability = false;
saw_initially = false;
+ saw_enforced = false;
break;
}
}
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 2194ab3dfa5..7332f37c736 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -2591,7 +2591,11 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
appendStringInfoString(&buf, " DEFERRABLE");
if (conForm->condeferred)
appendStringInfoString(&buf, " INITIALLY DEFERRED");
- if (!conForm->convalidated)
+
+ /* Validated status is irrelevant when the constraint is NOT ENFORCED. */
+ if (!conForm->conenforced)
+ appendStringInfoString(&buf, " NOT ENFORCED");
+ else if (!conForm->convalidated)
appendStringInfoString(&buf, " NOT VALID");
/* Cleanup */
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 422509f18d7..25db64dc440 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4585,6 +4585,7 @@ CheckConstraintFetch(Relation relation)
break;
}
+ check[found].ccenforced = conform->conenforced;
check[found].ccvalid = conform->convalidated;
check[found].ccnoinherit = conform->connoinherit;
check[found].ccname = MemoryContextStrdup(CacheMemoryContext,
diff --git a/src/include/access/tupdesc.h b/src/include/access/tupdesc.h
index 8930a28d660..908f8e7a57c 100644
--- a/src/include/access/tupdesc.h
+++ b/src/include/access/tupdesc.h
@@ -29,6 +29,7 @@ typedef struct ConstrCheck
{
char *ccname;
char *ccbin; /* nodeToString representation of expr */
+ bool ccenforced;
bool ccvalid;
bool ccnoinherit; /* this is a non-inheritable constraint */
} ConstrCheck;
diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h
index 3ca5dbf9e83..17250cc0393 100644
--- a/src/include/catalog/catversion.h
+++ b/src/include/catalog/catversion.h
@@ -57,6 +57,6 @@
*/
/* yyyymmddN */
-#define CATALOG_VERSION_NO 202412021
+#define CATALOG_VERSION_NO 202412041
#endif
diff --git a/src/include/catalog/heap.h b/src/include/catalog/heap.h
index 8c278f202b4..19d25244b89 100644
--- a/src/include/catalog/heap.h
+++ b/src/include/catalog/heap.h
@@ -40,6 +40,7 @@ typedef struct CookedConstraint
char *name; /* name, or NULL if none */
AttrNumber attnum; /* which attr (only for NOTNULL, DEFAULT) */
Node *expr; /* transformed default or check expr */
+ bool is_enforced; /* is enforced? (only for CHECK) */
bool skip_validation; /* skip validation? (only for CHECK) */
bool is_local; /* constraint has local (non-inherited) def */
int16 inhcount; /* number of times constraint is inherited */
diff --git a/src/include/catalog/pg_constraint.h b/src/include/catalog/pg_constraint.h
index 4b4476738a2..8e13f5ed710 100644
--- a/src/include/catalog/pg_constraint.h
+++ b/src/include/catalog/pg_constraint.h
@@ -51,6 +51,7 @@ CATALOG(pg_constraint,2606,ConstraintRelationId)
char contype; /* constraint type; see codes below */
bool condeferrable; /* deferrable constraint? */
bool condeferred; /* deferred by default? */
+ bool conenforced; /* enforced constraint? */
bool convalidated; /* constraint has been validated? */
/*
@@ -222,6 +223,7 @@ extern Oid CreateConstraintEntry(const char *constraintName,
char constraintType,
bool isDeferrable,
bool isDeferred,
+ bool isEnforced,
bool isValidated,
Oid parentConstrId,
Oid relId,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 0f9462493e3..912aabfa4ca 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2736,6 +2736,8 @@ typedef enum ConstrType /* types of constraints */
CONSTR_ATTR_NOT_DEFERRABLE,
CONSTR_ATTR_DEFERRED,
CONSTR_ATTR_IMMEDIATE,
+ CONSTR_ATTR_ENFORCED,
+ CONSTR_ATTR_NOT_ENFORCED,
} ConstrType;
/* Foreign key action codes */
@@ -2757,6 +2759,7 @@ typedef struct Constraint
char *conname; /* Constraint name, or NULL if unnamed */
bool deferrable; /* DEFERRABLE? */
bool initdeferred; /* INITIALLY DEFERRED? */
+ bool is_enforced; /* enforced constraint? */
bool skip_validation; /* skip validation of existing rows? */
bool initially_valid; /* mark the new constraint as valid? */
bool is_no_inherit; /* is constraint non-inheritable? */
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 899d64ad55f..f2bc378394e 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -153,6 +153,7 @@ PG_KEYWORD("enable", ENABLE_P, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("encoding", ENCODING, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("encrypted", ENCRYPTED, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("end", END_P, RESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("enforced", ENFORCED, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("enum", ENUM_P, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("error", ERROR_P, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("escape", ESCAPE, UNRESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out
index 2212c8dbb59..24eaebb46ac 100644
--- a/src/test/regress/expected/alter_table.out
+++ b/src/test/regress/expected/alter_table.out
@@ -507,11 +507,14 @@ ALTER TABLE attmp3 validate constraint attmpconstr;
ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten CHECK (b > 10); -- fail
ERROR: check constraint "b_greater_than_ten" of relation "attmp3" is violated by some row
ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten CHECK (b > 10) NOT VALID; -- succeeds
+ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten_not_enforced CHECK (b > 10) NOT ENFORCED; -- succeeds
ALTER TABLE attmp3 VALIDATE CONSTRAINT b_greater_than_ten; -- fails
ERROR: check constraint "b_greater_than_ten" of relation "attmp3" is violated by some row
DELETE FROM attmp3 WHERE NOT b > 10;
ALTER TABLE attmp3 VALIDATE CONSTRAINT b_greater_than_ten; -- succeeds
ALTER TABLE attmp3 VALIDATE CONSTRAINT b_greater_than_ten; -- succeeds
+ALTER TABLE attmp3 VALIDATE CONSTRAINT b_greater_than_ten_not_enforced; -- fail
+ERROR: cannot validated NOT ENFORCED constraint
-- Test inherited NOT VALID CHECK constraints
select * from attmp3;
a | b
@@ -1689,6 +1692,13 @@ alter table renameColumn add column w int;
-- this should fail
alter table only renameColumn add column x int;
ERROR: column must be added to child tables too
+-- this should work
+alter table renameColumn add column x int check (x > 0) not enforced;
+-- this should fail
+alter table renameColumn add column y int check (x > 0) not enforced enforced;
+ERROR: multiple ENFORCED/NOT ENFORCED clauses not allowed
+LINE 1: ...Column add column y int check (x > 0) not enforced enforced;
+ ^
-- Test corner cases in dropping of inherited columns
create table p1 (f1 int, f2 int);
create table c1 (f1 int not null) inherits(p1);
diff --git a/src/test/regress/expected/constraints.out b/src/test/regress/expected/constraints.out
index 71200c90ed3..692a69fe457 100644
--- a/src/test/regress/expected/constraints.out
+++ b/src/test/regress/expected/constraints.out
@@ -87,6 +87,25 @@ SELECT * FROM CHECK_TBL;
6
(3 rows)
+CREATE TABLE NE_CHECK_TBL (x int,
+ CONSTRAINT CHECK_CON CHECK (x > 3) NOT ENFORCED);
+INSERT INTO NE_CHECK_TBL VALUES (5);
+INSERT INTO NE_CHECK_TBL VALUES (4);
+INSERT INTO NE_CHECK_TBL VALUES (3);
+INSERT INTO NE_CHECK_TBL VALUES (2);
+INSERT INTO NE_CHECK_TBL VALUES (6);
+INSERT INTO NE_CHECK_TBL VALUES (1);
+SELECT * FROM NE_CHECK_TBL;
+ x
+---
+ 5
+ 4
+ 3
+ 2
+ 6
+ 1
+(6 rows)
+
CREATE SEQUENCE CHECK_SEQ;
CREATE TABLE CHECK2_TBL (x int, y text, z int,
CONSTRAINT SEQUENCE_CON
@@ -120,7 +139,8 @@ CREATE TABLE INSERT_TBL (x INT DEFAULT nextval('insert_seq'),
y TEXT DEFAULT '-NULL-',
z INT DEFAULT -1 * currval('insert_seq'),
CONSTRAINT INSERT_TBL_CON CHECK (x >= 3 AND y <> 'check failed' AND x < 8),
- CHECK (x + z = 0));
+ CHECK (x + z = 0) ENFORCED, /* no change it is a default */
+ CONSTRAINT NE_INSERT_TBL_CON CHECK (x + z = 1) NOT ENFORCED);
INSERT INTO INSERT_TBL(x,z) VALUES (2, -2);
ERROR: new row for relation "insert_tbl" violates check constraint "insert_tbl_con"
DETAIL: Failing row contains (2, -NULL-, -2).
@@ -715,6 +735,24 @@ SELECT * FROM unique_tbl;
3 | threex
(5 rows)
+-- enforcibility cannot be specified or set for unique constrain
+CREATE TABLE UNIQUE_EN_TBL(i int UNIQUE ENFORCED);
+ERROR: misplaced ENFORCED clause
+LINE 1: CREATE TABLE UNIQUE_EN_TBL(i int UNIQUE ENFORCED);
+ ^
+CREATE TABLE UNIQUE_NOTEN_TBL(i int UNIQUE NOT ENFORCED);
+ERROR: misplaced NOT ENFORCED clause
+LINE 1: CREATE TABLE UNIQUE_NOTEN_TBL(i int UNIQUE NOT ENFORCED);
+ ^
+-- XXX: error message is misleading here
+ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
+ERROR: ALTER CONSTRAINT statement constraints cannot be marked ENFORCED
+LINE 1: ...TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
+ ^
+ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORCED;
+ERROR: ALTER CONSTRAINT statement constraints cannot be marked NOT ENFORCED
+LINE 1: ...ABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORC...
+ ^
DROP TABLE unique_tbl;
--
-- EXCLUDE constraints
diff --git a/src/test/regress/expected/create_table_like.out b/src/test/regress/expected/create_table_like.out
index d091da5a1ef..e0613891351 100644
--- a/src/test/regress/expected/create_table_like.out
+++ b/src/test/regress/expected/create_table_like.out
@@ -315,7 +315,8 @@ Referenced by:
DROP TABLE inhz;
-- including storage and comments
-CREATE TABLE ctlt1 (a text CHECK (length(a) > 2) PRIMARY KEY, b text);
+CREATE TABLE ctlt1 (a text CHECK (length(a) > 2) ENFORCED PRIMARY KEY,
+ b text CHECK (length(b) > 100) NOT ENFORCED);
CREATE INDEX ctlt1_b_key ON ctlt1 (b);
CREATE INDEX ctlt1_fnidx ON ctlt1 ((a || b));
CREATE STATISTICS ctlt1_a_b_stat ON a,b FROM ctlt1;
@@ -366,6 +367,7 @@ CREATE TABLE ctlt1_inh (LIKE ctlt1 INCLUDING CONSTRAINTS INCLUDING COMMENTS) INH
NOTICE: merging column "a" with inherited definition
NOTICE: merging column "b" with inherited definition
NOTICE: merging constraint "ctlt1_a_check" with inherited definition
+NOTICE: merging constraint "ctlt1_b_check" with inherited definition
\d+ ctlt1_inh
Table "public.ctlt1_inh"
Column | Type | Collation | Nullable | Default | Storage | Stats target | Description
@@ -374,6 +376,7 @@ NOTICE: merging constraint "ctlt1_a_check" with inherited definition
b | text | | | | extended | | B
Check constraints:
"ctlt1_a_check" CHECK (length(a) > 2)
+ "ctlt1_b_check" CHECK (length(b) > 100) NOT ENFORCED
Not-null constraints:
"ctlt1_a_not_null" NOT NULL "a" (local, inherited)
Inherits: ctlt1
@@ -395,6 +398,7 @@ NOTICE: merging multiple inherited definitions of column "a"
c | text | | | | external | |
Check constraints:
"ctlt1_a_check" CHECK (length(a) > 2)
+ "ctlt1_b_check" CHECK (length(b) > 100) NOT ENFORCED
"ctlt3_a_check" CHECK (length(a) < 5)
"ctlt3_c_check" CHECK (length(c) < 7)
Not-null constraints:
@@ -415,6 +419,7 @@ Indexes:
"ctlt13_like_expr_idx" btree ((a || c))
Check constraints:
"ctlt1_a_check" CHECK (length(a) > 2)
+ "ctlt1_b_check" CHECK (length(b) > 100) NOT ENFORCED
"ctlt3_a_check" CHECK (length(a) < 5)
"ctlt3_c_check" CHECK (length(c) < 7)
Not-null constraints:
@@ -440,6 +445,7 @@ Indexes:
"ctlt_all_expr_idx" btree ((a || b))
Check constraints:
"ctlt1_a_check" CHECK (length(a) > 2)
+ "ctlt1_b_check" CHECK (length(b) > 100) NOT ENFORCED
Statistics objects:
"public.ctlt_all_a_b_stat" ON a, b FROM ctlt_all
"public.ctlt_all_expr_stat" ON (a || b) FROM ctlt_all
@@ -482,6 +488,7 @@ Indexes:
"pg_attrdef_expr_idx" btree ((a || b))
Check constraints:
"ctlt1_a_check" CHECK (length(a) > 2)
+ "ctlt1_b_check" CHECK (length(b) > 100) NOT ENFORCED
Statistics objects:
"public.pg_attrdef_a_b_stat" ON a, b FROM public.pg_attrdef
"public.pg_attrdef_expr_stat" ON (a || b) FROM public.pg_attrdef
@@ -506,6 +513,7 @@ Indexes:
"ctlt1_expr_idx" btree ((a || b))
Check constraints:
"ctlt1_a_check" CHECK (length(a) > 2)
+ "ctlt1_b_check" CHECK (length(b) > 100) NOT ENFORCED
Statistics objects:
"ctl_schema.ctlt1_a_b_stat" ON a, b FROM ctlt1
"ctl_schema.ctlt1_expr_stat" ON (a || b) FROM ctlt1
diff --git a/src/test/regress/expected/domain.out b/src/test/regress/expected/domain.out
index 42b6559f9c8..d894122ce61 100644
--- a/src/test/regress/expected/domain.out
+++ b/src/test/regress/expected/domain.out
@@ -1297,6 +1297,21 @@ select pg_basetype(1); -- expect NULL not error
drop domain mytext cascade;
NOTICE: drop cascades to type mytext_child_1
--
+-- Explicit enforceability specification not allowed
+---
+CREATE DOMAIN constraint_enforced_dom AS int CONSTRAINT the_constraint CHECK (value > 0) ENFORCED;
+ERROR: specifying constraint enforceability not supported for domains
+CREATE DOMAIN constraint_not_enforced_dom AS int CONSTRAINT the_constraint CHECK (value > 0) NOT ENFORCED;
+ERROR: specifying constraint enforceability not supported for domains
+ALTER DOMAIN constraint_comments_dom ADD CONSTRAINT the_constraint CHECK (value > 0) ENFORCED;
+ERROR: CHECK constraints cannot be marked ENFORCED
+LINE 1: ...om ADD CONSTRAINT the_constraint CHECK (value > 0) ENFORCED;
+ ^
+ALTER DOMAIN constraint_comments_dom ADD CONSTRAINT the_constraint CHECK (value > 0) NOT ENFORCED;
+ERROR: CHECK constraints cannot be marked NOT ENFORCED
+LINE 1: ...m ADD CONSTRAINT the_constraint CHECK (value > 0) NOT ENFORC...
+ ^
+--
-- Information schema
--
SELECT * FROM information_schema.column_domain_usage
diff --git a/src/test/regress/expected/inherit.out b/src/test/regress/expected/inherit.out
index bb81f6d2b4d..dbf3835cb14 100644
--- a/src/test/regress/expected/inherit.out
+++ b/src/test/regress/expected/inherit.out
@@ -1319,19 +1319,97 @@ NOTICE: merging constraint "inh_check_constraint1" with inherited definition
alter table p1_c1 add constraint inh_check_constraint2 check (f1 < 10);
alter table p1 add constraint inh_check_constraint2 check (f1 < 10);
NOTICE: merging constraint "inh_check_constraint2" with inherited definition
-select conrelid::regclass::text as relname, conname, conislocal, coninhcount
+alter table p1 add constraint inh_check_constraint3 check (f1 > 0) not enforced;
+alter table p1_c1 add constraint inh_check_constraint3 check (f1 > 0) not enforced;
+NOTICE: merging constraint "inh_check_constraint3" with inherited definition
+alter table p1_c1 add constraint inh_check_constraint4 check (f1 < 10) not enforced;
+alter table p1 add constraint inh_check_constraint4 check (f1 < 10) not enforced;
+NOTICE: merging constraint "inh_check_constraint4" with inherited definition
+-- allowed to merge enforced constraint with parent's not enforced constraint
+alter table p1_c1 add constraint inh_check_constraint5 check (f1 < 10) enforced;
+alter table p1 add constraint inh_check_constraint5 check (f1 < 10) not enforced;
+NOTICE: merging constraint "inh_check_constraint5" with inherited definition
+alter table p1 add constraint inh_check_constraint6 check (f1 < 10) not enforced;
+alter table p1_c1 add constraint inh_check_constraint6 check (f1 < 10) enforced;
+NOTICE: merging constraint "inh_check_constraint6" with inherited definition
+create table p1_c2(f1 int constraint inh_check_constraint4 check (f1 < 10)) inherits(p1);
+NOTICE: merging column "f1" with inherited definition
+NOTICE: merging constraint "inh_check_constraint4" with inherited definition
+-- but reverse is not allowed
+alter table p1_c1 add constraint inh_check_constraint7 check (f1 < 10) not enforced;
+alter table p1 add constraint inh_check_constraint7 check (f1 < 10) enforced;
+ERROR: constraint "inh_check_constraint7" conflicts with NOT ENFORCED constraint on relation "p1_c1"
+alter table p1 add constraint inh_check_constraint8 check (f1 < 10) enforced;
+alter table p1_c1 add constraint inh_check_constraint8 check (f1 < 10) not enforced;
+ERROR: constraint "inh_check_constraint8" conflicts with NOT ENFORCED constraint on relation "p1_c1"
+create table p1_fail(f1 int constraint inh_check_constraint2 check (f1 < 10) not enforced) inherits(p1);
+NOTICE: merging column "f1" with inherited definition
+ERROR: constraint "inh_check_constraint2" conflicts with NOT ENFORCED constraint on relation "p1_fail"
+-- constraints with different enforceability can be merged by marking them as ENFORCED
+create table p1_c3() inherits(p1, p1_c1);
+NOTICE: merging multiple inherited definitions of column "f1"
+-- but not allowed if the child constraint is explicitly asked to be NOT ENFORCED
+create table p1_fail(f1 int constraint inh_check_constraint6 check (f1 < 10) not enforced) inherits(p1, p1_c1);
+NOTICE: merging multiple inherited definitions of column "f1"
+NOTICE: merging column "f1" with inherited definition
+ERROR: constraint "inh_check_constraint6" conflicts with NOT ENFORCED constraint on relation "p1_fail"
+select conrelid::regclass::text as relname, conname, conislocal, coninhcount, conenforced
from pg_constraint where conname like 'inh\_check\_constraint%'
order by 1, 2;
- relname | conname | conislocal | coninhcount
----------+-----------------------+------------+-------------
- p1 | inh_check_constraint1 | t | 0
- p1 | inh_check_constraint2 | t | 0
- p1_c1 | inh_check_constraint1 | t | 1
- p1_c1 | inh_check_constraint2 | t | 1
-(4 rows)
+ relname | conname | conislocal | coninhcount | conenforced
+---------+-----------------------+------------+-------------+-------------
+ p1 | inh_check_constraint1 | t | 0 | t
+ p1 | inh_check_constraint2 | t | 0 | t
+ p1 | inh_check_constraint3 | t | 0 | f
+ p1 | inh_check_constraint4 | t | 0 | f
+ p1 | inh_check_constraint5 | t | 0 | f
+ p1 | inh_check_constraint6 | t | 0 | f
+ p1 | inh_check_constraint8 | t | 0 | t
+ p1_c1 | inh_check_constraint1 | t | 1 | t
+ p1_c1 | inh_check_constraint2 | t | 1 | t
+ p1_c1 | inh_check_constraint3 | t | 1 | f
+ p1_c1 | inh_check_constraint4 | t | 1 | f
+ p1_c1 | inh_check_constraint5 | t | 1 | t
+ p1_c1 | inh_check_constraint6 | t | 1 | t
+ p1_c1 | inh_check_constraint7 | t | 0 | f
+ p1_c1 | inh_check_constraint8 | f | 1 | t
+ p1_c2 | inh_check_constraint1 | f | 1 | t
+ p1_c2 | inh_check_constraint2 | f | 1 | t
+ p1_c2 | inh_check_constraint3 | f | 1 | f
+ p1_c2 | inh_check_constraint4 | t | 1 | t
+ p1_c2 | inh_check_constraint5 | f | 1 | f
+ p1_c2 | inh_check_constraint6 | f | 1 | f
+ p1_c2 | inh_check_constraint8 | f | 1 | t
+ p1_c3 | inh_check_constraint1 | f | 2 | t
+ p1_c3 | inh_check_constraint2 | f | 2 | t
+ p1_c3 | inh_check_constraint3 | f | 2 | f
+ p1_c3 | inh_check_constraint4 | f | 2 | f
+ p1_c3 | inh_check_constraint5 | f | 2 | t
+ p1_c3 | inh_check_constraint6 | f | 2 | t
+ p1_c3 | inh_check_constraint7 | f | 1 | f
+ p1_c3 | inh_check_constraint8 | f | 2 | t
+(30 rows)
+drop table p1 cascade;
+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
+--
+-- Similarly, check the merging of existing constraints; a parent constraint
+-- marked as NOT ENFORCED can merge with an ENFORCED child constraint, but the
+-- reverse is not allowed.
+--
+create table p1(f1 int constraint p1_a_check check (f1 > 0) not enforced);
+create table p1_c1(f1 int constraint p1_a_check check (f1 > 0) enforced);
+alter table p1_c1 inherit p1;
drop table p1 cascade;
NOTICE: drop cascades to table p1_c1
+create table p1(f1 int constraint p1_a_check check (f1 > 0) enforced);
+create table p1_c1(f1 int constraint p1_a_check check (f1 > 0) not enforced);
+alter table p1_c1 inherit p1;
+ERROR: constraint "p1_a_check" conflicts with NOT ENFORCED constraint on child table "p1_c1"
+drop table p1, p1_c1;
--
-- Test DROP behavior of multiply-defined CHECK constraints
--
diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql
index 637e3dac389..1035492988c 100644
--- a/src/test/regress/sql/alter_table.sql
+++ b/src/test/regress/sql/alter_table.sql
@@ -387,10 +387,12 @@ ALTER TABLE attmp3 validate constraint attmpconstr;
-- Try a non-verified CHECK constraint
ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten CHECK (b > 10); -- fail
ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten CHECK (b > 10) NOT VALID; -- succeeds
+ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten_not_enforced CHECK (b > 10) NOT ENFORCED; -- succeeds
ALTER TABLE attmp3 VALIDATE CONSTRAINT b_greater_than_ten; -- fails
DELETE FROM attmp3 WHERE NOT b > 10;
ALTER TABLE attmp3 VALIDATE CONSTRAINT b_greater_than_ten; -- succeeds
ALTER TABLE attmp3 VALIDATE CONSTRAINT b_greater_than_ten; -- succeeds
+ALTER TABLE attmp3 VALIDATE CONSTRAINT b_greater_than_ten_not_enforced; -- fail
-- Test inherited NOT VALID CHECK constraints
select * from attmp3;
@@ -1188,6 +1190,11 @@ alter table renameColumn add column w int;
-- this should fail
alter table only renameColumn add column x int;
+-- this should work
+alter table renameColumn add column x int check (x > 0) not enforced;
+
+-- this should fail
+alter table renameColumn add column y int check (x > 0) not enforced enforced;
-- Test corner cases in dropping of inherited columns
diff --git a/src/test/regress/sql/constraints.sql b/src/test/regress/sql/constraints.sql
index e607eb1fddb..d6742f83fb9 100644
--- a/src/test/regress/sql/constraints.sql
+++ b/src/test/regress/sql/constraints.sql
@@ -67,6 +67,18 @@ INSERT INTO CHECK_TBL VALUES (1);
SELECT * FROM CHECK_TBL;
+CREATE TABLE NE_CHECK_TBL (x int,
+ CONSTRAINT CHECK_CON CHECK (x > 3) NOT ENFORCED);
+
+INSERT INTO NE_CHECK_TBL VALUES (5);
+INSERT INTO NE_CHECK_TBL VALUES (4);
+INSERT INTO NE_CHECK_TBL VALUES (3);
+INSERT INTO NE_CHECK_TBL VALUES (2);
+INSERT INTO NE_CHECK_TBL VALUES (6);
+INSERT INTO NE_CHECK_TBL VALUES (1);
+
+SELECT * FROM NE_CHECK_TBL;
+
CREATE SEQUENCE CHECK_SEQ;
CREATE TABLE CHECK2_TBL (x int, y text, z int,
@@ -92,7 +104,8 @@ CREATE TABLE INSERT_TBL (x INT DEFAULT nextval('insert_seq'),
y TEXT DEFAULT '-NULL-',
z INT DEFAULT -1 * currval('insert_seq'),
CONSTRAINT INSERT_TBL_CON CHECK (x >= 3 AND y <> 'check failed' AND x < 8),
- CHECK (x + z = 0));
+ CHECK (x + z = 0) ENFORCED, /* no change it is a default */
+ CONSTRAINT NE_INSERT_TBL_CON CHECK (x + z = 1) NOT ENFORCED);
INSERT INTO INSERT_TBL(x,z) VALUES (2, -2);
@@ -518,6 +531,13 @@ COMMIT;
SELECT * FROM unique_tbl;
+-- enforcibility cannot be specified or set for unique constrain
+CREATE TABLE UNIQUE_EN_TBL(i int UNIQUE ENFORCED);
+CREATE TABLE UNIQUE_NOTEN_TBL(i int UNIQUE NOT ENFORCED);
+-- XXX: error message is misleading here
+ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
+ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORCED;
+
DROP TABLE unique_tbl;
--
diff --git a/src/test/regress/sql/create_table_like.sql b/src/test/regress/sql/create_table_like.sql
index dea8942c71f..a41f8b83d77 100644
--- a/src/test/regress/sql/create_table_like.sql
+++ b/src/test/regress/sql/create_table_like.sql
@@ -128,7 +128,8 @@ CREATE TABLE inhz (x text REFERENCES inhz, LIKE inhx INCLUDING INDEXES);
DROP TABLE inhz;
-- including storage and comments
-CREATE TABLE ctlt1 (a text CHECK (length(a) > 2) PRIMARY KEY, b text);
+CREATE TABLE ctlt1 (a text CHECK (length(a) > 2) ENFORCED PRIMARY KEY,
+ b text CHECK (length(b) > 100) NOT ENFORCED);
CREATE INDEX ctlt1_b_key ON ctlt1 (b);
CREATE INDEX ctlt1_fnidx ON ctlt1 ((a || b));
CREATE STATISTICS ctlt1_a_b_stat ON a,b FROM ctlt1;
diff --git a/src/test/regress/sql/domain.sql b/src/test/regress/sql/domain.sql
index ee07b03174e..518455c058a 100644
--- a/src/test/regress/sql/domain.sql
+++ b/src/test/regress/sql/domain.sql
@@ -866,6 +866,14 @@ select pg_basetype(1); -- expect NULL not error
drop domain mytext cascade;
+--
+-- Explicit enforceability specification not allowed
+---
+CREATE DOMAIN constraint_enforced_dom AS int CONSTRAINT the_constraint CHECK (value > 0) ENFORCED;
+CREATE DOMAIN constraint_not_enforced_dom AS int CONSTRAINT the_constraint CHECK (value > 0) NOT ENFORCED;
+ALTER DOMAIN constraint_comments_dom ADD CONSTRAINT the_constraint CHECK (value > 0) ENFORCED;
+ALTER DOMAIN constraint_comments_dom ADD CONSTRAINT the_constraint CHECK (value > 0) NOT ENFORCED;
+
--
-- Information schema
diff --git a/src/test/regress/sql/inherit.sql b/src/test/regress/sql/inherit.sql
index f51c70d6b03..49aae426f3c 100644
--- a/src/test/regress/sql/inherit.sql
+++ b/src/test/regress/sql/inherit.sql
@@ -468,12 +468,57 @@ alter table p1_c1 add constraint inh_check_constraint1 check (f1 > 0);
alter table p1_c1 add constraint inh_check_constraint2 check (f1 < 10);
alter table p1 add constraint inh_check_constraint2 check (f1 < 10);
-select conrelid::regclass::text as relname, conname, conislocal, coninhcount
+alter table p1 add constraint inh_check_constraint3 check (f1 > 0) not enforced;
+alter table p1_c1 add constraint inh_check_constraint3 check (f1 > 0) not enforced;
+
+alter table p1_c1 add constraint inh_check_constraint4 check (f1 < 10) not enforced;
+alter table p1 add constraint inh_check_constraint4 check (f1 < 10) not enforced;
+
+-- allowed to merge enforced constraint with parent's not enforced constraint
+alter table p1_c1 add constraint inh_check_constraint5 check (f1 < 10) enforced;
+alter table p1 add constraint inh_check_constraint5 check (f1 < 10) not enforced;
+
+alter table p1 add constraint inh_check_constraint6 check (f1 < 10) not enforced;
+alter table p1_c1 add constraint inh_check_constraint6 check (f1 < 10) enforced;
+
+create table p1_c2(f1 int constraint inh_check_constraint4 check (f1 < 10)) inherits(p1);
+
+-- but reverse is not allowed
+alter table p1_c1 add constraint inh_check_constraint7 check (f1 < 10) not enforced;
+alter table p1 add constraint inh_check_constraint7 check (f1 < 10) enforced;
+
+alter table p1 add constraint inh_check_constraint8 check (f1 < 10) enforced;
+alter table p1_c1 add constraint inh_check_constraint8 check (f1 < 10) not enforced;
+
+create table p1_fail(f1 int constraint inh_check_constraint2 check (f1 < 10) not enforced) inherits(p1);
+
+-- constraints with different enforceability can be merged by marking them as ENFORCED
+create table p1_c3() inherits(p1, p1_c1);
+
+-- but not allowed if the child constraint is explicitly asked to be NOT ENFORCED
+create table p1_fail(f1 int constraint inh_check_constraint6 check (f1 < 10) not enforced) inherits(p1, p1_c1);
+
+select conrelid::regclass::text as relname, conname, conislocal, coninhcount, conenforced
from pg_constraint where conname like 'inh\_check\_constraint%'
order by 1, 2;
drop table p1 cascade;
+--
+-- Similarly, check the merging of existing constraints; a parent constraint
+-- marked as NOT ENFORCED can merge with an ENFORCED child constraint, but the
+-- reverse is not allowed.
+--
+create table p1(f1 int constraint p1_a_check check (f1 > 0) not enforced);
+create table p1_c1(f1 int constraint p1_a_check check (f1 > 0) enforced);
+alter table p1_c1 inherit p1;
+drop table p1 cascade;
+
+create table p1(f1 int constraint p1_a_check check (f1 > 0) enforced);
+create table p1_c1(f1 int constraint p1_a_check check (f1 > 0) not enforced);
+alter table p1_c1 inherit p1;
+drop table p1, p1_c1;
+
--
-- Test DROP behavior of multiply-defined CHECK constraints
--
--
2.43.5
v8-0002-refactor-split-ATExecAlterConstrRecurse.patchapplication/octet-stream; name=v8-0002-refactor-split-ATExecAlterConstrRecurse.patchDownload
From 24a38c4a1d3e39a20d486c558da46eef28fd7fdc Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 23 Sep 2024 12:31:07 +0530
Subject: [PATCH v8 2/5] refactor: split ATExecAlterConstrRecurse()
When changing a NOT ENFORCED foreign key constraint to ENFORCED, we
need to create the necessary triggers for enforcing the constraint. We
can set the deferrability at the time of creation and skip the code
used for other constraint alteration operations.
Additionally, move the code that iterates over each child constraint
of the constraint being altered into a separate function.
---
src/backend/commands/tablecmds.c | 194 +++++++++++++++++++------------
1 file changed, 118 insertions(+), 76 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index e3bb4176642..7ee90d9a4a8 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -394,6 +394,12 @@ static ObjectAddress ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd,
static bool ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
Relation rel, HeapTuple contuple, List **otherrelids,
LOCKMODE lockmode);
+static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
+ bool deferrable, bool initdeferred,
+ List **otherrelids);
+static void ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
+ Relation rel, HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode);
static ObjectAddress ATExecValidateConstraint(List **wqueue,
Relation rel, char *constrName,
bool recurse, bool recursing, LOCKMODE lockmode);
@@ -11851,9 +11857,6 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
{
HeapTuple copyTuple;
Form_pg_constraint copy_con;
- HeapTuple tgtuple;
- ScanKeyData tgkey;
- SysScanDesc tgscan;
copyTuple = heap_copytuple(contuple);
copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
@@ -11874,53 +11877,8 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
* Now we need to update the multiple entries in pg_trigger that
* implement the constraint.
*/
- ScanKeyInit(&tgkey,
- Anum_pg_trigger_tgconstraint,
- BTEqualStrategyNumber, F_OIDEQ,
- ObjectIdGetDatum(conoid));
- tgscan = systable_beginscan(tgrel, TriggerConstraintIndexId, true,
- NULL, 1, &tgkey);
- while (HeapTupleIsValid(tgtuple = systable_getnext(tgscan)))
- {
- Form_pg_trigger tgform = (Form_pg_trigger) GETSTRUCT(tgtuple);
- Form_pg_trigger copy_tg;
- HeapTuple tgCopyTuple;
-
- /*
- * Remember OIDs of other relation(s) involved in FK constraint.
- * (Note: it's likely that we could skip forcing a relcache inval
- * for other rels that don't have a trigger whose properties
- * change, but let's be conservative.)
- */
- if (tgform->tgrelid != RelationGetRelid(rel))
- *otherrelids = list_append_unique_oid(*otherrelids,
- tgform->tgrelid);
-
- /*
- * Update deferrability of RI_FKey_noaction_del,
- * RI_FKey_noaction_upd, RI_FKey_check_ins and RI_FKey_check_upd
- * triggers, but not others; see createForeignKeyActionTriggers
- * and CreateFKCheckTrigger.
- */
- if (tgform->tgfoid != F_RI_FKEY_NOACTION_DEL &&
- tgform->tgfoid != F_RI_FKEY_NOACTION_UPD &&
- tgform->tgfoid != F_RI_FKEY_CHECK_INS &&
- tgform->tgfoid != F_RI_FKEY_CHECK_UPD)
- continue;
-
- tgCopyTuple = heap_copytuple(tgtuple);
- copy_tg = (Form_pg_trigger) GETSTRUCT(tgCopyTuple);
-
- copy_tg->tgdeferrable = cmdcon->deferrable;
- copy_tg->tginitdeferred = cmdcon->initdeferred;
- CatalogTupleUpdate(tgrel, &tgCopyTuple->t_self, tgCopyTuple);
-
- InvokeObjectPostAlterHook(TriggerRelationId, tgform->oid, 0);
-
- heap_freetuple(tgCopyTuple);
- }
-
- systable_endscan(tgscan);
+ AlterConstrTriggerDeferrability(conoid, tgrel, rel, cmdcon->deferrable,
+ cmdcon->initdeferred, otherrelids);
}
/*
@@ -11933,36 +11891,120 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
*/
if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)
- {
- ScanKeyData pkey;
- SysScanDesc pscan;
- HeapTuple childtup;
-
- ScanKeyInit(&pkey,
- Anum_pg_constraint_conparentid,
- BTEqualStrategyNumber, F_OIDEQ,
- ObjectIdGetDatum(conoid));
-
- pscan = systable_beginscan(conrel, ConstraintParentIndexId,
- true, NULL, 1, &pkey);
-
- while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
- {
- Form_pg_constraint childcon = (Form_pg_constraint) GETSTRUCT(childtup);
- Relation childrel;
-
- childrel = table_open(childcon->conrelid, lockmode);
- ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, childrel, childtup,
- otherrelids, lockmode);
- table_close(childrel, NoLock);
- }
-
- systable_endscan(pscan);
- }
+ ATExecAlterChildConstr(cmdcon, conrel, tgrel, rel, contuple,
+ otherrelids, lockmode);
return changed;
}
+/*
+ * A subroutine of ATExecAlterConstrRecurse that updated constraint trigger's
+ * deferrability.
+ *
+ * The arguments to this function have the same meaning as the arguments to
+ * ATExecAlterConstrRecurse.
+ */
+static void
+AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
+ bool deferrable, bool initdeferred,
+ List **otherrelids)
+{
+ HeapTuple tgtuple;
+ ScanKeyData tgkey;
+ SysScanDesc tgscan;
+
+ ScanKeyInit(&tgkey,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+ tgscan = systable_beginscan(tgrel, TriggerConstraintIndexId, true,
+ NULL, 1, &tgkey);
+ while (HeapTupleIsValid(tgtuple = systable_getnext(tgscan)))
+ {
+ Form_pg_trigger tgform = (Form_pg_trigger) GETSTRUCT(tgtuple);
+ Form_pg_trigger copy_tg;
+ HeapTuple tgCopyTuple;
+
+ /*
+ * Remember OIDs of other relation(s) involved in FK constraint.
+ * (Note: it's likely that we could skip forcing a relcache inval for
+ * other rels that don't have a trigger whose properties change, but
+ * let's be conservative.)
+ */
+ if (tgform->tgrelid != RelationGetRelid(rel))
+ *otherrelids = list_append_unique_oid(*otherrelids,
+ tgform->tgrelid);
+
+ /*
+ * Update enable status and deferrability of RI_FKey_noaction_del,
+ * RI_FKey_noaction_upd, RI_FKey_check_ins and RI_FKey_check_upd
+ * triggers, but not others; see createForeignKeyActionTriggers and
+ * CreateFKCheckTrigger.
+ */
+ if (tgform->tgfoid != F_RI_FKEY_NOACTION_DEL &&
+ tgform->tgfoid != F_RI_FKEY_NOACTION_UPD &&
+ tgform->tgfoid != F_RI_FKEY_CHECK_INS &&
+ tgform->tgfoid != F_RI_FKEY_CHECK_UPD)
+ continue;
+
+ tgCopyTuple = heap_copytuple(tgtuple);
+ copy_tg = (Form_pg_trigger) GETSTRUCT(tgCopyTuple);
+
+ copy_tg->tgdeferrable = deferrable;
+ copy_tg->tginitdeferred = initdeferred;
+ CatalogTupleUpdate(tgrel, &tgCopyTuple->t_self, tgCopyTuple);
+
+ InvokeObjectPostAlterHook(TriggerRelationId, tgform->oid, 0);
+
+ heap_freetuple(tgCopyTuple);
+ }
+
+ systable_endscan(tgscan);
+}
+
+/*
+ * Invokes ATExecAlterConstrRecurse for each constraint that is a child of the
+ * specified constraint.
+ *
+ * The arguments to this function have the same meaning as the arguments to
+ * ATExecAlterConstrRecurse.
+ */
+static void
+ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
+ Relation rel, HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode)
+{
+ Form_pg_constraint currcon;
+ Oid conoid;
+ ScanKeyData pkey;
+ SysScanDesc pscan;
+ HeapTuple childtup;
+
+ currcon = (Form_pg_constraint) GETSTRUCT(contuple);
+ conoid = currcon->oid;
+
+ ScanKeyInit(&pkey,
+ Anum_pg_constraint_conparentid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+
+ pscan = systable_beginscan(conrel, ConstraintParentIndexId,
+ true, NULL, 1, &pkey);
+
+ while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
+ {
+ Form_pg_constraint childcon = (Form_pg_constraint) GETSTRUCT(childtup);
+ Relation childrel;
+
+ childrel = table_open(childcon->conrelid, lockmode);
+ ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, childrel, childtup,
+ otherrelids, lockmode);
+ table_close(childrel, NoLock);
+ }
+
+ systable_endscan(pscan);
+}
+
/*
* ALTER TABLE VALIDATE CONSTRAINT
*
--
2.43.5
v8-0003-refactor-Change-ATExecAlterConstrRecurse-argument.patchapplication/octet-stream; name=v8-0003-refactor-Change-ATExecAlterConstrRecurse-argument.patchDownload
From 1ba785ac684266d7bc6c3b9969288b9dbc87d15b Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 23 Sep 2024 12:37:05 +0530
Subject: [PATCH v8 3/5] refactor: Change ATExecAlterConstrRecurse() argument.
Instead of passing the Relation of the referencing relation, we will
pass a relid. Opening the relation using the relid again should not
cause significant issues. However, this approach makes recursion more
efficient, especially in the later patch where we will be recreating
the referencing and referred triggers. In addition to that passing
relid of the referred relation too and Oids of triggers.
----
NOTE: This patch is not meant to be committed separately. It should
be squashed with the main patch that adds ENFORCED/NOT ENFORCED.
----
---
src/backend/commands/tablecmds.c | 66 ++++++++++++++++++++++----------
1 file changed, 45 insertions(+), 21 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 7ee90d9a4a8..441d3968f39 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -392,14 +392,22 @@ static void AlterSeqNamespaces(Relation classRel, Relation rel,
static ObjectAddress ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd,
bool recurse, bool recursing, LOCKMODE lockmode);
static bool ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode);
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger);
static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
bool deferrable, bool initdeferred,
List **otherrelids);
static void ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode);
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger);
static ObjectAddress ATExecValidateConstraint(List **wqueue,
Relation rel, char *constrName,
bool recurse, bool recursing, LOCKMODE lockmode);
@@ -11797,8 +11805,10 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
currcon->condeferred != cmdcon->initdeferred ||
rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
- if (ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, rel, contuple,
- &otherrelids, lockmode))
+ if (ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, currcon->conrelid,
+ currcon->confrelid, contuple, &otherrelids,
+ lockmode, InvalidOid, InvalidOid,
+ InvalidOid, InvalidOid))
ObjectAddressSet(address, ConstraintRelationId, currcon->oid);
}
@@ -11831,13 +11841,18 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
*/
static bool
ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode)
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger)
{
Form_pg_constraint currcon;
Oid conoid;
Oid refrelid;
bool changed = false;
+ Relation rel;
/* since this function recurses, it could be driven to stack overflow */
check_stack_depth();
@@ -11846,6 +11861,8 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
conoid = currcon->oid;
refrelid = currcon->confrelid;
+ rel = table_open(currcon->conrelid, lockmode);
+
/*
* Update pg_constraint with the flags from cmdcon.
*
@@ -11891,8 +11908,14 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
*/
if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)
- ATExecAlterChildConstr(cmdcon, conrel, tgrel, rel, contuple,
- otherrelids, lockmode);
+ ATExecAlterChildConstr(cmdcon, conrel, tgrel, fkrelid, pkrelid,
+ contuple, otherrelids, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
+
+ table_close(rel, NoLock);
return changed;
}
@@ -11971,8 +11994,12 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
*/
static void
ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode)
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger)
{
Form_pg_constraint currcon;
Oid conoid;
@@ -11992,15 +12019,12 @@ ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
true, NULL, 1, &pkey);
while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
- {
- Form_pg_constraint childcon = (Form_pg_constraint) GETSTRUCT(childtup);
- Relation childrel;
-
- childrel = table_open(childcon->conrelid, lockmode);
- ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, childrel, childtup,
- otherrelids, lockmode);
- table_close(childrel, NoLock);
- }
+ ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, fkrelid, pkrelid,
+ childtup, otherrelids, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
systable_endscan(pscan);
}
--
2.43.5
v8-0004-Remove-hastriggers-flag-check-before-fetching-FK-.patchapplication/octet-stream; name=v8-0004-Remove-hastriggers-flag-check-before-fetching-FK-.patchDownload
From 8a48ffce1b806e08297d65600337a61785f94fc0 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 7 Oct 2024 12:40:07 +0530
Subject: [PATCH v8 4/5] Remove hastriggers flag check before fetching FK
constraints.
With NOT ENFORCED, foreign key constraints will be created without
triggers. Therefore, the criteria for fetching foreign keys based on
the presence of triggers no longer apply.
----
NOTE: This patch is intended to reduce the diff noise from the main
patch and is not meant to be committed separately. It should be
squashed with the main patch that adds ENFORCED/NOT ENFORCED.
----
---
src/backend/utils/cache/relcache.c | 5 -
src/bin/pg_dump/pg_dump.c | 8 +-
src/bin/psql/describe.c | 226 ++++++++++++++---------------
3 files changed, 108 insertions(+), 131 deletions(-)
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 25db64dc440..0809f03aca2 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4669,11 +4669,6 @@ RelationGetFKeyList(Relation relation)
if (relation->rd_fkeyvalid)
return relation->rd_fkeylist;
- /* Fast path: non-partitioned tables without triggers can't have FKs */
- if (!relation->rd_rel->relhastriggers &&
- relation->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
- return NIL;
-
/*
* We build the list we intend to return (in the caller's context) while
* doing the scan. After successfully completing the scan, we copy that
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index ec0cdf4ed74..9ada4cdc182 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -7839,13 +7839,7 @@ getConstraints(Archive *fout, TableInfo tblinfo[], int numTables)
{
TableInfo *tinfo = &tblinfo[i];
- /*
- * For partitioned tables, foreign keys have no triggers so they must
- * be included anyway in case some foreign keys are defined.
- */
- if ((!tinfo->hastriggers &&
- tinfo->relkind != RELKIND_PARTITIONED_TABLE) ||
- !(tinfo->dobj.dump & DUMP_COMPONENT_DEFINITION))
+ if (!(tinfo->dobj.dump & DUMP_COMPONENT_DEFINITION))
continue;
/* OK, we need info for this table */
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 2657abdc72d..054e8a2d821 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2528,136 +2528,124 @@ describeOneTableDetails(const char *schemaname,
PQclear(result);
}
- /*
- * Print foreign-key constraints (there are none if no triggers,
- * except if the table is partitioned, in which case the triggers
- * appear in the partitions)
- */
- if (tableinfo.hastriggers ||
- tableinfo.relkind == RELKIND_PARTITIONED_TABLE)
+ /* Print foreign-key constraints */
+ if (pset.sversion >= 120000 &&
+ (tableinfo.ispartition || tableinfo.relkind == RELKIND_PARTITIONED_TABLE))
{
- if (pset.sversion >= 120000 &&
- (tableinfo.ispartition || tableinfo.relkind == RELKIND_PARTITIONED_TABLE))
+ /*
+ * Put the constraints defined in this table first, followed
+ * by the constraints defined in ancestor partitioned tables.
+ */
+ printfPQExpBuffer(&buf,
+ "SELECT conrelid = '%s'::pg_catalog.regclass AS sametable,\n"
+ " conname,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef,\n"
+ " conrelid::pg_catalog.regclass AS ontable\n"
+ " FROM pg_catalog.pg_constraint,\n"
+ " pg_catalog.pg_partition_ancestors('%s')\n"
+ " WHERE conrelid = relid AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
+ "ORDER BY sametable DESC, conname;",
+ oid, oid);
+ }
+ else
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT true as sametable, conname,\n"
+ " pg_catalog.pg_get_constraintdef(r.oid, true) as condef,\n"
+ " conrelid::pg_catalog.regclass AS ontable\n"
+ "FROM pg_catalog.pg_constraint r\n"
+ "WHERE r.conrelid = '%s' AND r.contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n",
+ oid);
+
+ if (pset.sversion >= 120000)
+ appendPQExpBufferStr(&buf, " AND conparentid = 0\n");
+ appendPQExpBufferStr(&buf, "ORDER BY conname");
+ }
+
+ result = PSQLexec(buf.data);
+ if (!result)
+ goto error_return;
+ else
+ tuples = PQntuples(result);
+
+ if (tuples > 0)
+ {
+ int i_sametable = PQfnumber(result, "sametable"),
+ i_conname = PQfnumber(result, "conname"),
+ i_condef = PQfnumber(result, "condef"),
+ i_ontable = PQfnumber(result, "ontable");
+
+ printTableAddFooter(&cont, _("Foreign-key constraints:"));
+ for (i = 0; i < tuples; i++)
{
/*
- * Put the constraints defined in this table first, followed
- * by the constraints defined in ancestor partitioned tables.
+ * Print untranslated constraint name and definition. Use
+ * a "TABLE tab" prefix when the constraint is defined in
+ * a parent partitioned table.
*/
- printfPQExpBuffer(&buf,
- "SELECT conrelid = '%s'::pg_catalog.regclass AS sametable,\n"
- " conname,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef,\n"
- " conrelid::pg_catalog.regclass AS ontable\n"
- " FROM pg_catalog.pg_constraint,\n"
- " pg_catalog.pg_partition_ancestors('%s')\n"
- " WHERE conrelid = relid AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
- "ORDER BY sametable DESC, conname;",
- oid, oid);
- }
- else
- {
- printfPQExpBuffer(&buf,
- "SELECT true as sametable, conname,\n"
- " pg_catalog.pg_get_constraintdef(r.oid, true) as condef,\n"
- " conrelid::pg_catalog.regclass AS ontable\n"
- "FROM pg_catalog.pg_constraint r\n"
- "WHERE r.conrelid = '%s' AND r.contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n",
- oid);
-
- if (pset.sversion >= 120000)
- appendPQExpBufferStr(&buf, " AND conparentid = 0\n");
- appendPQExpBufferStr(&buf, "ORDER BY conname");
- }
-
- result = PSQLexec(buf.data);
- if (!result)
- goto error_return;
- else
- tuples = PQntuples(result);
-
- if (tuples > 0)
- {
- int i_sametable = PQfnumber(result, "sametable"),
- i_conname = PQfnumber(result, "conname"),
- i_condef = PQfnumber(result, "condef"),
- i_ontable = PQfnumber(result, "ontable");
-
- printTableAddFooter(&cont, _("Foreign-key constraints:"));
- for (i = 0; i < tuples; i++)
- {
- /*
- * Print untranslated constraint name and definition. Use
- * a "TABLE tab" prefix when the constraint is defined in
- * a parent partitioned table.
- */
- if (strcmp(PQgetvalue(result, i, i_sametable), "f") == 0)
- printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
- PQgetvalue(result, i, i_ontable),
- PQgetvalue(result, i, i_conname),
- PQgetvalue(result, i, i_condef));
- else
- printfPQExpBuffer(&buf, " \"%s\" %s",
- PQgetvalue(result, i, i_conname),
- PQgetvalue(result, i, i_condef));
-
- printTableAddFooter(&cont, buf.data);
- }
- }
- PQclear(result);
- }
-
- /* print incoming foreign-key references */
- if (tableinfo.hastriggers ||
- tableinfo.relkind == RELKIND_PARTITIONED_TABLE)
- {
- if (pset.sversion >= 120000)
- {
- printfPQExpBuffer(&buf,
- "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
- " FROM pg_catalog.pg_constraint c\n"
- " WHERE confrelid IN (SELECT pg_catalog.pg_partition_ancestors('%s')\n"
- " UNION ALL VALUES ('%s'::pg_catalog.regclass))\n"
- " AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
- "ORDER BY conname;",
- oid, oid);
- }
- else
- {
- printfPQExpBuffer(&buf,
- "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
- " FROM pg_catalog.pg_constraint\n"
- " WHERE confrelid = %s AND contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n"
- "ORDER BY conname;",
- oid);
- }
-
- result = PSQLexec(buf.data);
- if (!result)
- goto error_return;
- else
- tuples = PQntuples(result);
-
- if (tuples > 0)
- {
- int i_conname = PQfnumber(result, "conname"),
- i_ontable = PQfnumber(result, "ontable"),
- i_condef = PQfnumber(result, "condef");
-
- printTableAddFooter(&cont, _("Referenced by:"));
- for (i = 0; i < tuples; i++)
- {
+ if (strcmp(PQgetvalue(result, i, i_sametable), "f") == 0)
printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
PQgetvalue(result, i, i_ontable),
PQgetvalue(result, i, i_conname),
PQgetvalue(result, i, i_condef));
+ else
+ printfPQExpBuffer(&buf, " \"%s\" %s",
+ PQgetvalue(result, i, i_conname),
+ PQgetvalue(result, i, i_condef));
- printTableAddFooter(&cont, buf.data);
- }
+ printTableAddFooter(&cont, buf.data);
}
- PQclear(result);
}
+ PQclear(result);
+
+ /* print incoming foreign-key references */
+ if (pset.sversion >= 120000)
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
+ " FROM pg_catalog.pg_constraint c\n"
+ " WHERE confrelid IN (SELECT pg_catalog.pg_partition_ancestors('%s')\n"
+ " UNION ALL VALUES ('%s'::pg_catalog.regclass))\n"
+ " AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
+ "ORDER BY conname;",
+ oid, oid);
+ }
+ else
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
+ " FROM pg_catalog.pg_constraint\n"
+ " WHERE confrelid = %s AND contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n"
+ "ORDER BY conname;",
+ oid);
+ }
+
+ result = PSQLexec(buf.data);
+ if (!result)
+ goto error_return;
+ else
+ tuples = PQntuples(result);
+
+ if (tuples > 0)
+ {
+ int i_conname = PQfnumber(result, "conname"),
+ i_ontable = PQfnumber(result, "ontable"),
+ i_condef = PQfnumber(result, "condef");
+
+ printTableAddFooter(&cont, _("Referenced by:"));
+ for (i = 0; i < tuples; i++)
+ {
+ printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
+ PQgetvalue(result, i, i_ontable),
+ PQgetvalue(result, i, i_conname),
+ PQgetvalue(result, i, i_condef));
+
+ printTableAddFooter(&cont, buf.data);
+ }
+ }
+ PQclear(result);
/* print any row-level policies */
if (pset.sversion >= 90500)
--
2.43.5
v8-0005-WIP-Add-support-for-NOT-ENFORCED-in-foreign-key-c.patchapplication/octet-stream; name=v8-0005-WIP-Add-support-for-NOT-ENFORCED-in-foreign-key-c.patchDownload
From 7d11479fc81227d22e1c6b3a5a3c6879cb9a343e Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Wed, 4 Dec 2024 15:23:38 +0530
Subject: [PATCH v8 5/5] WIP-Add support for NOT ENFORCED in foreign key
constraints.
Typically, when a foreign key (FK) constraint is created on a table,
action and check triggers are added to maintain data integrity. With
this patch, if a constraint is marked as NOT ENFORCED, integrity
checks are no longer required, making these triggers unnecessary.
Consequently, when creating a NOT ENFORCED FK constraint, triggers
will not be created, and the constraint will be marked as NOT VALID.
Similarly, if an existing FK constraint is changed to NOT ENFORCED,
the associated triggers will be dropped, and the constraint will also
be marked as NOT VALID. Conversely, if a NOT ENFORCED FK constraint is
changed to ENFORCED, the necessary triggers will be created, and the
constraint will remain in the NOT VALID state.
== WIP ==
The restriction preventing NOT VALID foreign key constraints on
partitioned tables has now been bypassed for NOT ENFORCED constraints,
but it could potentially be removed entirely. As per Alvaro Herrera,
this feature has not yet been implemented for partitioned tables.
Further investigation is needed to determine what additional changes,
aside from removing the error check, are necessary to fully support
NOT VALID foreign key constraints on partitioned tables. As a result,
this patch remains marked as WIP.
=========
---
doc/src/sgml/advanced.sgml | 13 +
doc/src/sgml/catalogs.sgml | 2 +-
doc/src/sgml/ddl.sgml | 12 +-
doc/src/sgml/ref/alter_table.sgml | 12 +-
doc/src/sgml/ref/create_table.sgml | 10 +-
src/backend/catalog/pg_constraint.c | 5 +-
src/backend/commands/tablecmds.c | 427 ++++++++++++++++------
src/backend/parser/gram.y | 5 +-
src/backend/parser/parse_utilcmd.c | 14 +-
src/backend/utils/cache/relcache.c | 1 +
src/include/utils/rel.h | 3 +
src/test/regress/expected/alter_table.out | 2 +-
src/test/regress/expected/constraints.out | 8 +-
src/test/regress/expected/foreign_key.out | 130 ++++++-
src/test/regress/sql/foreign_key.sql | 93 ++++-
15 files changed, 590 insertions(+), 147 deletions(-)
diff --git a/doc/src/sgml/advanced.sgml b/doc/src/sgml/advanced.sgml
index 755c9f14850..b7d90de69d7 100644
--- a/doc/src/sgml/advanced.sgml
+++ b/doc/src/sgml/advanced.sgml
@@ -125,6 +125,19 @@ ERROR: insert or update on table "weather" violates foreign key constraint "wea
DETAIL: Key (city)=(Berkeley) is not present in table "cities".
</screen>
</para>
+ <para>
+ If, for some reason, you do not want to enforce this constraint and wish to
+ avoid the error, you can modify the foreign key constraint to <literal>NOT ENFORCED</literal>,
+ or specify it in the <link linkend="sql-createtable"><command>CREATE TABLE</command></link>.
+ </para>
+ <para>
+ Let's change the constraint to <literal>NOT ENFORCED</literal>, and when you
+ attempt to reinsert the same record, it will not result in any error.
+<programlisting>
+ALTER TABLE weather ALTER CONSTRAINT weather_city_fkey NOT ENFORCED;
+INSERT INTO weather VALUES ('Berkeley', 45, 53, 0.0, '1994-11-28');
+</programlisting>
+ </para>
<para>
The behavior of foreign keys can be finely tuned to your
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index a56ef4df562..3bb04574481 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2608,7 +2608,7 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l
</para>
<para>
Is the constraint enforceable?
- Currently, can be false only for CHECK constraints
+ Currently, can be false only for foreign keys and CHECK constraints
</para></entry>
</row>
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index 9a20396dae4..cda36576670 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -1058,11 +1058,13 @@ CREATE TABLE example (
</indexterm>
<para>
- A foreign key constraint specifies that the values in a column (or
- a group of columns) must match the values appearing in some row
- of another table.
- We say this maintains the <firstterm>referential
- integrity</firstterm> between two related tables.
+ A foreign key constraint with <literal>ENFORCED</literal>, the default
+ setting, specifies that the values in a column (or a group of columns)
+ must match the values appearing in some row of another table. We say this
+ maintains the <firstterm>referential integrity</firstterm> between two
+ related tables. If a foreign key constraint is created with <literal>NOT ENFORCED</literal>,
+ the check for aforementioned <firstterm>referential integrity</firstterm>
+ will not be performed.
</para>
<para>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 938450fba18..fa807cb9f3a 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -58,7 +58,7 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable>
ALTER [ COLUMN ] <replaceable class="parameter">column_name</replaceable> SET COMPRESSION <replaceable class="parameter">compression_method</replaceable>
ADD <replaceable class="parameter">table_constraint</replaceable> [ NOT VALID ]
ADD <replaceable class="parameter">table_constraint_using_index</replaceable>
- ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+ ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]
VALIDATE CONSTRAINT <replaceable class="parameter">constraint_name</replaceable>
DROP CONSTRAINT [ IF EXISTS ] <replaceable class="parameter">constraint_name</replaceable> [ RESTRICT | CASCADE ]
DISABLE TRIGGER [ <replaceable class="parameter">trigger_name</replaceable> | ALL | USER ]
@@ -555,6 +555,13 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
This form alters the attributes of a constraint that was previously
created. Currently only foreign key constraints may be altered.
</para>
+ <para>
+ Note that when a constraint is changed to <literal>NOT ENFORCED</literal>,
+ it is also marked as <literal>NOT VALID</literal>. While this might seem
+ unnecessary, it is done for consistency and should be set accordingly.
+ This ensures that if the constraint is later changed to <literal>ENFORCED</literal>,
+ it will automatically be in the correct <literal>NOT VALID</literal> state.
+ </para>
</listitem>
</varlistentry>
@@ -565,7 +572,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
This form validates a foreign key or check constraint that was
previously created as <literal>NOT VALID</literal>, by scanning the
table to ensure there are no rows for which the constraint is not
- satisfied. Nothing happens if the constraint is already marked valid.
+ satisfied. If the constraint is not enforced, an error is thrown.
+ Nothing happens if the constraint is already marked valid.
(See <xref linkend="sql-altertable-notes"/> below for an explanation
of the usefulness of this command.)
</para>
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index cfd01c4ef2b..89fc7354c00 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -1382,11 +1382,11 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
<term><literal>NOT ENFORCED</literal></term>
<listitem>
<para>
- This is currently only allowed for <literal>CHECK</literal> constraints.
- If the constraint is <literal>NOT ENFORCED</literal>, this clause
- specifies that the constraint check will be skipped. When the constraint
- is <literal>ENFORCED</literal>, check is performed after each statement;
- the latter is the default.
+ This is currently only allowed for foreign key and <literal>CHECK</literal>
+ constraints. If the constraint is <literal>NOT ENFORCED</literal>, this
+ clause specifies that the constraint check will be skipped. When the
+ constraint is <literal>ENFORCED</literal>, check is performed after each
+ statement; the latter is the default.
</para>
<para>
Note that if the constraint is <literal>NOT ENFORCED</literal>, it is also
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index 1a201ebeadb..d9653dd9cb0 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -100,8 +100,9 @@ CreateConstraintEntry(const char *constraintName,
ObjectAddresses *addrs_auto;
ObjectAddresses *addrs_normal;
- /* Only CHECK constraint can be not enforced */
- Assert(isEnforced || constraintType == CONSTRAINT_CHECK);
+ /* Only CHECK or FOREIGN KEY constraint can be not enforced */
+ Assert(isEnforced || constraintType == CONSTRAINT_CHECK ||
+ constraintType == CONSTRAINT_FOREIGN);
/* NOT ENFORCED constraint must be NOT VALID */
Assert(isEnforced || !isValidated);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 441d3968f39..f392c5c4e97 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -391,20 +391,30 @@ static void AlterSeqNamespaces(Relation classRel, Relation rel,
LOCKMODE lockmode);
static ObjectAddress ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd,
bool recurse, bool recursing, LOCKMODE lockmode);
-static bool ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
- const Oid fkrelid, const Oid pkrelid,
- HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
+static bool ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel,
+ Relation tgrel, const Oid fkrelid,
+ const Oid pkrelid, HeapTuple contuple,
+ List **otherrelids, LOCKMODE lockmode,
+ Oid ReferencedParentDelTrigger,
Oid ReferencedParentUpdTrigger,
Oid ReferencingParentInsTrigger,
Oid ReferencingParentUpdTrigger);
-static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
+static void ATExecAlterConstrEnforceability(Constraint *cmdcon, Relation conrel,
+ Relation tgrel, const Oid fkrelid,
+ const Oid pkrelid, HeapTuple contuple,
+ List **otherrelids, LOCKMODE lockmode,
+ Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger);
+static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Oid fkrel,
bool deferrable, bool initdeferred,
List **otherrelids);
-static void ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
- const Oid fkrelid, const Oid pkrelid,
- HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
+static void ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel,
+ Relation tgrel, const Oid fkrelid,
+ const Oid pkrelid, HeapTuple contuple,
+ List **otherrelids, LOCKMODE lockmode,
+ Oid ReferencedParentDelTrigger,
Oid ReferencedParentUpdTrigger,
Oid ReferencingParentInsTrigger,
Oid ReferencingParentUpdTrigger);
@@ -9773,7 +9783,24 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
errmsg("cannot use ONLY for foreign key on partitioned table \"%s\" referencing relation \"%s\"",
RelationGetRelationName(rel),
RelationGetRelationName(pkrel))));
- if (fkconstraint->skip_validation && !fkconstraint->initially_valid)
+
+
+ /*
+ * TODO: I'm not sure why this restriction exists; NOT VALID foreign
+ * keys seem to work fine on partitioned tables, with behavior
+ * matching that of NOT VALID foreign keys on regular tables. I still
+ * need to confirm the necessity of this restriction, and it might be
+ * something that can be removed in the future. For now, I have
+ * bypassed this error for NOT ENFORCED foreign key constraints.
+ *
+ * TODO: UPDATE: As per Alvaro Herrera, this feature has not been
+ * implemented for partitioned tables. I still need to investigate
+ * what additional changes, beyond removing the following error check,
+ * are required to support NOT VALID foreign key constraints on
+ * partitioned tables.
+ */
+ if (fkconstraint->is_enforced && fkconstraint->skip_validation &&
+ !fkconstraint->initially_valid)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot add NOT VALID foreign key on partitioned table \"%s\" referencing relation \"%s\"",
@@ -10447,7 +10474,7 @@ addFkConstraint(addFkConstraintSides fkside,
CONSTRAINT_FOREIGN,
fkconstraint->deferrable,
fkconstraint->initdeferred,
- true, /* Is Enforced */
+ fkconstraint->is_enforced,
fkconstraint->initially_valid,
parentConstr,
RelationGetRelid(rel),
@@ -10565,20 +10592,22 @@ addFkRecurseReferenced(Constraint *fkconstraint, Relation rel,
Oid parentDelTrigger, Oid parentUpdTrigger,
bool with_period)
{
- Oid deleteTriggerOid,
- updateTriggerOid;
+ Oid deleteTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
Assert(CheckRelationLockedByMe(pkrel, ShareRowExclusiveLock, true));
Assert(CheckRelationLockedByMe(rel, ShareRowExclusiveLock, true));
/*
- * Create the action triggers that enforce the constraint.
+ * Create action triggers to enforce the constraint, or skip them if the
+ * constraint is not enforced.
*/
- createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
- fkconstraint,
- parentConstr, indexOid,
- parentDelTrigger, parentUpdTrigger,
- &deleteTriggerOid, &updateTriggerOid);
+ if (fkconstraint->is_enforced)
+ createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
+ fkconstraint,
+ parentConstr, indexOid,
+ parentDelTrigger, parentUpdTrigger,
+ &deleteTriggerOid, &updateTriggerOid);
/*
* If the referenced table is partitioned, recurse on ourselves to handle
@@ -10699,8 +10728,8 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
Oid parentInsTrigger, Oid parentUpdTrigger,
bool with_period)
{
- Oid insertTriggerOid,
- updateTriggerOid;
+ Oid insertTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
Assert(OidIsValid(parentConstr));
Assert(CheckRelationLockedByMe(rel, ShareRowExclusiveLock, true));
@@ -10712,29 +10741,32 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
errmsg("foreign key constraints are not supported on foreign tables")));
/*
- * Add the check triggers to it and, if necessary, schedule it to be
- * checked in Phase 3.
+ * Add check triggers if the constraint is enforced, and if needed,
+ * schedule them to be checked in Phase 3.
*
* If the relation is partitioned, drill down to do it to its partitions.
*/
- createForeignKeyCheckTriggers(RelationGetRelid(rel),
- RelationGetRelid(pkrel),
- fkconstraint,
- parentConstr,
- indexOid,
- parentInsTrigger, parentUpdTrigger,
- &insertTriggerOid, &updateTriggerOid);
+ if (fkconstraint->is_enforced)
+ createForeignKeyCheckTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
+ fkconstraint,
+ parentConstr,
+ indexOid,
+ parentInsTrigger, parentUpdTrigger,
+ &insertTriggerOid, &updateTriggerOid);
if (rel->rd_rel->relkind == RELKIND_RELATION)
{
/*
* Tell Phase 3 to check that the constraint is satisfied by existing
- * rows. We can skip this during table creation, when requested
- * explicitly by specifying NOT VALID in an ADD FOREIGN KEY command,
- * and when we're recreating a constraint following a SET DATA TYPE
- * operation that did not impugn its validity.
+ * rows. We can skip this during table creation, when constraint is
+ * specified as NOT ENFORCED, when requested explicitly by specifying
+ * NOT VALID in an ADD FOREIGN KEY command, and when we're recreating
+ * a constraint following a SET DATA TYPE operation that did not
+ * impugn its validity.
*/
- if (wqueue && !old_check_ok && !fkconstraint->skip_validation)
+ if (wqueue && !old_check_ok && !fkconstraint->skip_validation &&
+ fkconstraint->is_enforced)
{
NewConstraint *newcon;
AlteredTableInfo *tab;
@@ -10969,8 +11001,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
AttrNumber confdelsetcols[INDEX_MAX_KEYS];
Constraint *fkconstraint;
ObjectAddress address;
- Oid deleteTriggerOid,
- updateTriggerOid;
+ Oid deleteTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(constrOid));
if (!HeapTupleIsValid(tuple))
@@ -11030,8 +11062,9 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
fkconstraint->fk_del_set_cols = NIL;
fkconstraint->old_conpfeqop = NIL;
fkconstraint->old_pktable_oid = InvalidOid;
+ fkconstraint->is_enforced = constrForm->conenforced;
fkconstraint->skip_validation = false;
- fkconstraint->initially_valid = true;
+ fkconstraint->initially_valid = constrForm->convalidated;
/* set up colnames that are used to generate the constraint name */
for (int i = 0; i < numfks; i++)
@@ -11059,9 +11092,10 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
* parent OIDs for similar triggers that will be created on the
* partition in addFkRecurseReferenced().
*/
- GetForeignKeyActionTriggers(trigrel, constrOid,
- constrForm->confrelid, constrForm->conrelid,
- &deleteTriggerOid, &updateTriggerOid);
+ if (constrForm->conenforced)
+ GetForeignKeyActionTriggers(trigrel, constrOid,
+ constrForm->confrelid, constrForm->conrelid,
+ &deleteTriggerOid, &updateTriggerOid);
/* Add this constraint ... */
address = addFkConstraint(addFkReferencedSide,
@@ -11227,17 +11261,18 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
mapped_conkey[i] = attmap->attnums[conkey[i] - 1];
/*
- * Get the "check" triggers belonging to the constraint to pass as
- * parent OIDs for similar triggers that will be created on the
- * partition in addFkRecurseReferencing(). They are also passed to
- * tryAttachPartitionForeignKey() below to simply assign as parents to
- * the partition's existing "check" triggers, that is, if the
- * corresponding constraints is deemed attachable to the parent
- * constraint.
+ * Get the "check" triggers belonging to the constraint, if it is
+ * enforced, to pass as parent OIDs for similar triggers that will be
+ * created on the partition in addFkRecurseReferencing(). They are
+ * also passed to tryAttachPartitionForeignKey() below to simply
+ * assign as parents to the partition's existing "check" triggers,
+ * that is, if the corresponding constraints is deemed attachable to
+ * the parent constraint.
*/
- GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
- constrForm->confrelid, constrForm->conrelid,
- &insertTriggerOid, &updateTriggerOid);
+ if (constrForm->conenforced)
+ GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
+ constrForm->confrelid, constrForm->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
/*
* Before creating a new constraint, see whether any existing FKs are
@@ -11289,6 +11324,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
fkconstraint->fk_del_set_cols = NIL;
fkconstraint->old_conpfeqop = NIL;
fkconstraint->old_pktable_oid = InvalidOid;
+ fkconstraint->is_enforced = constrForm->conenforced;
fkconstraint->skip_validation = false;
fkconstraint->initially_valid = constrForm->convalidated;
for (int i = 0; i < numfks; i++)
@@ -11373,8 +11409,6 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
ScanKeyData key;
SysScanDesc scan;
HeapTuple trigtup;
- Oid insertTriggerOid,
- updateTriggerOid;
parentConstrTup = SearchSysCache1(CONSTROID,
ObjectIdGetDatum(parentConstrOid));
@@ -11413,9 +11447,10 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
elog(ERROR, "cache lookup failed for constraint %u", fk->conoid);
partConstr = (Form_pg_constraint) GETSTRUCT(partcontup);
if (OidIsValid(partConstr->conparentid) ||
- !partConstr->convalidated ||
+ (partConstr->conenforced && !partConstr->convalidated) ||
partConstr->condeferrable != parentConstr->condeferrable ||
partConstr->condeferred != parentConstr->condeferred ||
+ partConstr->conenforced != parentConstr->conenforced ||
partConstr->confupdtype != parentConstr->confupdtype ||
partConstr->confdeltype != parentConstr->confdeltype ||
partConstr->confmatchtype != parentConstr->confmatchtype)
@@ -11475,18 +11510,25 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
ConstraintSetParentConstraint(fk->conoid, parentConstrOid, partRelid);
/*
- * Like the constraint, attach partition's "check" triggers to the
- * corresponding parent triggers.
+ * Similar to the constraint, attach the partition's "check" triggers to
+ * the corresponding parent triggers if the constraint is enforced.
+ * Non-enforced constraints do not have these triggers.
*/
- GetForeignKeyCheckTriggers(trigrel,
- fk->conoid, fk->confrelid, fk->conrelid,
- &insertTriggerOid, &updateTriggerOid);
- Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
- TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
- partRelid);
- Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
- TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
- partRelid);
+ if (fk->conenforced)
+ {
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
+ partRelid);
+ Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
+ partRelid);
+ }
/*
* If the referenced table is partitioned, then the partition we're
@@ -11803,6 +11845,7 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
address = InvalidObjectAddress;
if (currcon->condeferrable != cmdcon->deferrable ||
currcon->condeferred != cmdcon->initdeferred ||
+ currcon->conenforced != cmdcon->is_enforced ||
rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
if (ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, currcon->conrelid,
@@ -11850,19 +11893,18 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
{
Form_pg_constraint currcon;
Oid conoid;
+ Oid conrelid;
Oid refrelid;
bool changed = false;
- Relation rel;
/* since this function recurses, it could be driven to stack overflow */
check_stack_depth();
currcon = (Form_pg_constraint) GETSTRUCT(contuple);
conoid = currcon->oid;
+ conrelid = currcon->conrelid;
refrelid = currcon->confrelid;
- rel = table_open(currcon->conrelid, lockmode);
-
/*
* Update pg_constraint with the flags from cmdcon.
*
@@ -11870,7 +11912,8 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
* silently do nothing.
*/
if (currcon->condeferrable != cmdcon->deferrable ||
- currcon->condeferred != cmdcon->initdeferred)
+ currcon->condeferred != cmdcon->initdeferred ||
+ currcon->conenforced != cmdcon->is_enforced)
{
HeapTuple copyTuple;
Form_pg_constraint copy_con;
@@ -11879,6 +11922,18 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
copy_con->condeferrable = cmdcon->deferrable;
copy_con->condeferred = cmdcon->initdeferred;
+ copy_con->conenforced = cmdcon->is_enforced;
+
+ /*
+ * NB: The convalidated status is irrelevant when the constraint is
+ * set to NOT ENFORCED, but for consistency, it should be set
+ * accordingly. This ensures that if the constraint is later changed
+ * to ENFORCED, it will automatically be in the correct NOT VALID
+ * state.
+ */
+ if (!cmdcon->is_enforced)
+ copy_con->convalidated = false;
+
CatalogTupleUpdate(conrel, ©Tuple->t_self, copyTuple);
InvokeObjectPostAlterHook(ConstraintRelationId,
@@ -11888,38 +11943,187 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
changed = true;
/* Make new constraint flags visible to others */
- CacheInvalidateRelcache(rel);
+ CacheInvalidateRelcacheByRelid(conrelid);
+ }
+ if (currcon->conenforced != cmdcon->is_enforced)
+ {
+ ATExecAlterConstrEnforceability(cmdcon, conrel, tgrel, fkrelid, pkrelid,
+ contuple, otherrelids, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
+ }
+ else
+ {
/*
* Now we need to update the multiple entries in pg_trigger that
* implement the constraint.
*/
- AlterConstrTriggerDeferrability(conoid, tgrel, rel, cmdcon->deferrable,
- cmdcon->initdeferred, otherrelids);
+ AlterConstrTriggerDeferrability(conoid, tgrel, conrelid,
+ cmdcon->deferrable,
+ cmdcon->initdeferred,
+ otherrelids);
+
+ /*
+ * If the table at either end of the constraint is partitioned, we
+ * need to recurse and handle every constraint that is a child of this
+ * one.
+ *
+ * (This assumes that the recurse flag is forcibly set for partitioned
+ * tables, and not set for legacy inheritance, though we don't check
+ * for that here.)
+ */
+ if (get_rel_relkind(conrelid) == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)
+ ATExecAlterChildConstr(cmdcon, conrel, tgrel, fkrelid, pkrelid,
+ contuple, otherrelids, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
}
- /*
- * If the table at either end of the constraint is partitioned, we need to
- * recurse and handle every constraint that is a child of this one.
- *
- * (This assumes that the recurse flag is forcibly set for partitioned
- * tables, and not set for legacy inheritance, though we don't check for
- * that here.)
- */
- if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
- get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)
- ATExecAlterChildConstr(cmdcon, conrel, tgrel, fkrelid, pkrelid,
- contuple, otherrelids, lockmode,
- ReferencedParentDelTrigger,
- ReferencedParentUpdTrigger,
- ReferencingParentInsTrigger,
- ReferencingParentUpdTrigger);
-
- table_close(rel, NoLock);
-
return changed;
}
+/*
+ * A subroutine of ATExecAlterConstrRecurse that updates the enforceability of
+ * a foreign key constraint. Depending on whether the constraint is being set
+ * to enforced or not enforced, it creates or drops the trigger accordingly.
+ *
+ * The arguments to this function have the same meaning as the arguments to
+ * ATExecAlterConstrRecurse.
+ */
+static void
+ATExecAlterConstrEnforceability(Constraint *cmdcon, Relation conrel,
+ Relation tgrel, const Oid fkrelid,
+ const Oid pkrelid, HeapTuple contuple,
+ List **otherrelids, LOCKMODE lockmode,
+ Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger)
+{
+ Form_pg_constraint currcon;
+ Oid conoid;
+
+ currcon = (Form_pg_constraint) GETSTRUCT(contuple);
+ conoid = currcon->oid;
+
+ /* Drop triggers */
+ if (!cmdcon->is_enforced)
+ {
+ HeapTuple tgtuple;
+ ScanKeyData tgkey;
+ SysScanDesc tgscan;
+
+ /*
+ * When setting a constraint to NOT ENFORCED, the constraint triggers
+ * need to be dropped. Therefore, we must process the child relations
+ * first, followed by the parent, to account for dependencies.
+ */
+ if (get_rel_relkind(currcon->conrelid) == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE)
+ ATExecAlterChildConstr(cmdcon, conrel, tgrel, fkrelid, pkrelid,
+ contuple, otherrelids, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
+
+ ScanKeyInit(&tgkey,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+ tgscan = systable_beginscan(tgrel, TriggerConstraintIndexId, true,
+ NULL, 1, &tgkey);
+ while (HeapTupleIsValid(tgtuple = systable_getnext(tgscan)))
+ {
+ Form_pg_trigger tgform = (Form_pg_trigger) GETSTRUCT(tgtuple);
+ ObjectAddress trigger;
+
+ /*
+ * Delete only RI_FKey_noaction_del, RI_FKey_noaction_upd,
+ * RI_FKey_check_ins and RI_FKey_check_upd triggers, but not
+ * others; see createForeignKeyActionTriggers and
+ * CreateFKCheckTrigger.
+ */
+ if (tgform->tgfoid != F_RI_FKEY_NOACTION_DEL &&
+ tgform->tgfoid != F_RI_FKEY_NOACTION_UPD &&
+ tgform->tgfoid != F_RI_FKEY_CHECK_INS &&
+ tgform->tgfoid != F_RI_FKEY_CHECK_UPD)
+ continue;
+
+ deleteDependencyRecordsFor(TriggerRelationId, tgform->oid,
+ false);
+ /* make dependency deletion visible to performDeletion */
+ CommandCounterIncrement();
+ ObjectAddressSet(trigger, TriggerRelationId, tgform->oid);
+ performDeletion(&trigger, DROP_RESTRICT, 0);
+ /* make trigger drop visible, in case the loop iterates */
+ CommandCounterIncrement();
+ }
+
+ systable_endscan(tgscan);
+ }
+ else /* Create triggers */
+ {
+ Relation rel;
+ Oid ReferencedDelTriggerOid = InvalidOid,
+ ReferencedUpdTriggerOid = InvalidOid,
+ ReferencingInsTriggerOid = InvalidOid,
+ ReferencingUpdTriggerOid = InvalidOid;
+
+ /* Update the foreign key action required for trigger creation. */
+ cmdcon->fk_matchtype = currcon->confmatchtype;
+ cmdcon->fk_upd_action = currcon->confupdtype;
+ cmdcon->fk_del_action = currcon->confdeltype;
+
+ rel = table_open(currcon->conrelid, lockmode);
+
+ /* Create referenced triggers */
+ if (currcon->conrelid == fkrelid)
+ createForeignKeyActionTriggers(rel,
+ currcon->confrelid,
+ cmdcon,
+ conoid,
+ currcon->conindid,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ &ReferencedDelTriggerOid,
+ &ReferencedUpdTriggerOid);
+
+ /* Create referencing triggers */
+ if (currcon->confrelid == pkrelid)
+ createForeignKeyCheckTriggers(currcon->conrelid,
+ pkrelid,
+ cmdcon,
+ conoid,
+ currcon->conindid,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger,
+ &ReferencingInsTriggerOid,
+ &ReferencingUpdTriggerOid);
+
+ /*
+ * If the table at either end of the constraint is partitioned, we
+ * need to recurse and create triggers for each constraint that is a
+ * child of this one.
+ */
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE)
+ ATExecAlterChildConstr(cmdcon, conrel, tgrel, fkrelid, pkrelid,
+ contuple, otherrelids, lockmode,
+ ReferencedDelTriggerOid,
+ ReferencedUpdTriggerOid,
+ ReferencingInsTriggerOid,
+ ReferencingUpdTriggerOid);
+ table_close(rel, NoLock);
+ }
+}
+
/*
* A subroutine of ATExecAlterConstrRecurse that updated constraint trigger's
* deferrability.
@@ -11928,7 +12132,7 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
* ATExecAlterConstrRecurse.
*/
static void
-AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
+AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Oid fkrel,
bool deferrable, bool initdeferred,
List **otherrelids)
{
@@ -11954,7 +12158,7 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
* other rels that don't have a trigger whose properties change, but
* let's be conservative.)
*/
- if (tgform->tgrelid != RelationGetRelid(rel))
+ if (tgform->tgrelid != fkrel)
*otherrelids = list_append_unique_oid(*otherrelids,
tgform->tgrelid);
@@ -18978,8 +19182,8 @@ ConstraintImpliedByRelConstraint(Relation scanrel, List *testConstraint, List *p
continue;
/*
- * NOT ENFORCED constraints are always marked as invalid, which
- * should have been ignored.
+ * NOT ENFORCED constraints are always marked as invalid, which should
+ * have been ignored.
*/
Assert(constr->check[i].ccenforced);
@@ -19965,8 +20169,6 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
Form_pg_constraint conform;
- Oid insertTriggerOid,
- updateTriggerOid;
contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
if (!HeapTupleIsValid(contup))
@@ -19989,17 +20191,25 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
/*
* Also, look up the partition's "check" triggers corresponding to the
- * constraint being detached and detach them from the parent triggers.
+ * enforced constraint being detached and detach them from the parent
+ * triggers. Non-enforced constraints do not have these triggers;
+ * therefore, this step is not needed.
*/
- GetForeignKeyCheckTriggers(trigrel,
- fk->conoid, fk->confrelid, fk->conrelid,
- &insertTriggerOid, &updateTriggerOid);
- Assert(OidIsValid(insertTriggerOid));
- TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
- RelationGetRelid(partRel));
- Assert(OidIsValid(updateTriggerOid));
- TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
- RelationGetRelid(partRel));
+ if (fk->conenforced)
+ {
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ Assert(OidIsValid(updateTriggerOid));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ }
/*
* Lastly, create the action triggers on the referenced table, using
@@ -20039,8 +20249,9 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
fkconstraint->conname = pstrdup(NameStr(conform->conname));
fkconstraint->deferrable = conform->condeferrable;
fkconstraint->initdeferred = conform->condeferred;
+ fkconstraint->is_enforced = conform->conenforced;
fkconstraint->skip_validation = true;
- fkconstraint->initially_valid = true;
+ fkconstraint->initially_valid = conform->convalidated;
/* a few irrelevant fields omitted here */
fkconstraint->pktable = NULL;
fkconstraint->fk_attrs = NIL;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 26766e2892f..600d6a571d1 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -2660,7 +2660,7 @@ alter_table_cmd:
processCASbits($4, @4, "ALTER CONSTRAINT statement",
&c->deferrable,
&c->initdeferred,
- NULL, NULL, NULL, yyscanner);
+ &c->is_enforced, NULL, NULL, yyscanner);
$$ = (Node *) n;
}
/* ALTER TABLE <name> VALIDATE CONSTRAINT ... */
@@ -4029,6 +4029,7 @@ ColConstraintElem:
n->is_enforced = true;
n->skip_validation = false;
n->initially_valid = true;
+ n->is_enforced = true;
$$ = (Node *) n;
}
;
@@ -4304,7 +4305,7 @@ ConstraintElem:
n->fk_del_set_cols = ($11)->deleteAction->cols;
processCASbits($12, @12, "FOREIGN KEY",
&n->deferrable, &n->initdeferred,
- NULL, &n->skip_validation,
+ &n->is_enforced, &n->skip_validation,
NULL,
yyscanner);
n->initially_valid = !n->skip_validation;
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 815ade9eeec..de4e68ed8df 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -2961,8 +2961,10 @@ transformFKConstraints(CreateStmtContext *cxt,
/*
* If CREATE TABLE or adding a column with NULL default, we can safely
- * skip validation of FK constraints, and nonetheless mark them valid.
- * (This will override any user-supplied NOT VALID flag.)
+ * skip validation of FK constraints, and mark them as valid based on the
+ * constraint enforcement flag, since NOT ENFORCED constraints must always
+ * be marked as NOT VALID. (This will override any user-supplied NOT VALID
+ * flag.)
*/
if (skipValidation)
{
@@ -2971,7 +2973,7 @@ transformFKConstraints(CreateStmtContext *cxt,
Constraint *constraint = (Constraint *) lfirst(fkclist);
constraint->skip_validation = true;
- constraint->initially_valid = true;
+ constraint->initially_valid = constraint->is_enforced;
}
}
@@ -3966,7 +3968,8 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
case CONSTR_ATTR_ENFORCED:
if (lastprimarycon == NULL ||
- lastprimarycon->contype != CONSTR_CHECK)
+ (lastprimarycon->contype != CONSTR_CHECK &&
+ lastprimarycon->contype != CONSTR_FOREIGN))
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("misplaced ENFORCED clause"),
@@ -3982,7 +3985,8 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
case CONSTR_ATTR_NOT_ENFORCED:
if (lastprimarycon == NULL ||
- lastprimarycon->contype != CONSTR_CHECK)
+ (lastprimarycon->contype != CONSTR_CHECK &&
+ lastprimarycon->contype != CONSTR_FOREIGN))
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("misplaced NOT ENFORCED clause"),
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 0809f03aca2..0efbcca190d 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4700,6 +4700,7 @@ RelationGetFKeyList(Relation relation)
info->conoid = constraint->oid;
info->conrelid = constraint->conrelid;
info->confrelid = constraint->confrelid;
+ info->conenforced = constraint->conenforced;
DeconstructFkConstraintRow(htup, &info->nkeys,
info->conkey,
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 87002049538..2db011ddfb7 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -284,6 +284,9 @@ typedef struct ForeignKeyCacheInfo
/* number of columns in the foreign key */
int nkeys;
+ /* Is enforced ? */
+ bool conenforced;
+
/*
* these arrays each have nkeys valid entries:
*/
diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out
index 24eaebb46ac..d37a336bc23 100644
--- a/src/test/regress/expected/alter_table.out
+++ b/src/test/regress/expected/alter_table.out
@@ -514,7 +514,7 @@ DELETE FROM attmp3 WHERE NOT b > 10;
ALTER TABLE attmp3 VALIDATE CONSTRAINT b_greater_than_ten; -- succeeds
ALTER TABLE attmp3 VALIDATE CONSTRAINT b_greater_than_ten; -- succeeds
ALTER TABLE attmp3 VALIDATE CONSTRAINT b_greater_than_ten_not_enforced; -- fail
-ERROR: cannot validated NOT ENFORCED constraint
+ERROR: cannot validate NOT ENFORCED constraint
-- Test inherited NOT VALID CHECK constraints
select * from attmp3;
a | b
diff --git a/src/test/regress/expected/constraints.out b/src/test/regress/expected/constraints.out
index 692a69fe457..f8767c5cacf 100644
--- a/src/test/regress/expected/constraints.out
+++ b/src/test/regress/expected/constraints.out
@@ -746,13 +746,9 @@ LINE 1: CREATE TABLE UNIQUE_NOTEN_TBL(i int UNIQUE NOT ENFORCED);
^
-- XXX: error message is misleading here
ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
-ERROR: ALTER CONSTRAINT statement constraints cannot be marked ENFORCED
-LINE 1: ...TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
- ^
+ERROR: constraint "unique_tbl_i_key" of relation "unique_tbl" is not a foreign key constraint
ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORCED;
-ERROR: ALTER CONSTRAINT statement constraints cannot be marked NOT ENFORCED
-LINE 1: ...ABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORC...
- ^
+ERROR: constraint "unique_tbl_i_key" of relation "unique_tbl" is not a foreign key constraint
DROP TABLE unique_tbl;
--
-- EXCLUDE constraints
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 3f459f70ac1..352b1bcfe6b 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -1,12 +1,44 @@
--
-- FOREIGN KEY
--
--- MATCH FULL
+-- NOT ENFORCED
--
-- First test, check and cascade
--
CREATE TABLE PKTABLE ( ptest1 int PRIMARY KEY, ptest2 text );
-CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE, ftest2 int );
+CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE NOT ENFORCED,
+ ftest2 int );
+-- Inserting into the foreign key table will not result in an error, even if
+-- there is no matching key in the referenced table.
+INSERT INTO FKTABLE VALUES (1, 2);
+INSERT INTO FKTABLE VALUES (2, 3);
+-- Check FKTABLE
+SELECT * FROM FKTABLE;
+ ftest1 | ftest2
+--------+--------
+ 1 | 2
+ 2 | 3
+(2 rows)
+
+-- Changing it back to ENFORCED will leave the constraint in the NOT VALID state
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+-- Which needs to be explicitly validated.
+ALTER TABLE FKTABLE VALIDATE CONSTRAINT fktable_ftest1_fkey;
+ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_fkey"
+DETAIL: Key (ftest1)=(1) is not present in table "pktable".
+-- Remove any existing rows that violate the constraint, then attempt to change
+-- it.
+TRUNCATE FKTABLE;
+ALTER TABLE FKTABLE VALIDATE CONSTRAINT fktable_ftest1_fkey;
+-- Any further inserts will fail due to the enforcement.
+INSERT INTO FKTABLE VALUES (3, 4);
+ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_fkey"
+DETAIL: Key (ftest1)=(3) is not present in table "pktable".
+--
+-- MATCH FULL
+--
+-- First test, check and cascade
+--
-- Insert test data into PKTABLE
INSERT INTO PKTABLE VALUES (1, 'Test1');
INSERT INTO PKTABLE VALUES (2, 'Test2');
@@ -351,6 +383,8 @@ INSERT INTO FKTABLE VALUES (1, NULL);
ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL;
ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_ftest2_fkey"
DETAIL: MATCH FULL does not allow mixing of null and nonnull key values.
+-- NOT ENFORCED will bypass the initial check.
+ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL NOT ENFORCED;
DROP TABLE FKTABLE;
DROP TABLE PKTABLE;
-- MATCH SIMPLE
@@ -1276,13 +1310,28 @@ INSERT INTO fktable VALUES (0, 20);
ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_fk_fkey"
DETAIL: Key (fk)=(20) is not present in table "pktable".
COMMIT;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT ENFORCED;
+BEGIN;
+-- doesn't match FK, no error.
+UPDATE pktable SET id = 10 WHERE id = 5;
+-- doesn't match PK, no error.
+INSERT INTO fktable VALUES (0, 20);
+ROLLBACK;
-- try additional syntax
-ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE ENFORCED;
-- illegal option
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE INITIALLY DEFERRED;
ERROR: constraint declared INITIALLY DEFERRED must be DEFERRABLE
LINE 1: ...e ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE INITIALLY ...
^
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORCED;
+ERROR: conflicting constraint properties
+LINE 1: ...fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORC...
+ ^
+CREATE TEMP TABLE fktable2 (fk int references pktable ENFORCED NOT ENFORCED);
+ERROR: multiple ENFORCED/NOT ENFORCED clauses not allowed
+LINE 1: ...ABLE fktable2 (fk int references pktable ENFORCED NOT ENFORC...
+ ^
-- test order of firing of FK triggers when several RI-induced changes need to
-- be made to the same row. This was broken by subtransaction-related
-- changes in 8.0.
@@ -1580,10 +1629,14 @@ ALTER TABLE fk_partitioned_fk DROP COLUMN fdrop1;
CREATE TABLE fk_partitioned_fk_1 (fdrop1 int, fdrop2 int, a int, fdrop3 int, b int);
ALTER TABLE fk_partitioned_fk_1 DROP COLUMN fdrop1, DROP COLUMN fdrop2, DROP COLUMN fdrop3;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FROM (0,0) TO (1000,1000);
-ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk;
+ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
+ALTER TABLE fk_partitioned_fk_2 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
+-- Changing to ENFORCED does not alter the validation status; it must be explicitly validated.
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+ALTER TABLE fk_partitioned_fk VALIDATE CONSTRAINT fk_partitioned_fk_a_b_fkey ;
CREATE TABLE fk_partitioned_fk_3 (fdrop1 int, fdrop2 int, fdrop3 int, fdrop4 int, b int, a int)
PARTITION BY HASH (a);
ALTER TABLE fk_partitioned_fk_3 DROP COLUMN fdrop1, DROP COLUMN fdrop2,
@@ -1612,10 +1665,10 @@ INSERT INTO fk_partitioned_fk_1 (a,b) VALUES (500, 501);
ERROR: insert or update on table "fk_partitioned_fk_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
DETAIL: Key (a, b)=(500, 501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (1500, 1501);
-ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_2_a_b_fkey"
DETAIL: Key (a, b)=(1500, 1501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk_2 (a,b) VALUES (1500, 1501);
-ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_2_a_b_fkey"
DETAIL: Key (a, b)=(1500, 1501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (2500, 2502);
ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
@@ -1665,6 +1718,37 @@ Indexes:
Referenced by:
TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b)
+-- Check the exsting FK trigger
+CREATE TEMP TABLE tmp_trg_info AS SELECT tgtype, tgrelid::regclass, tgconstraint
+FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+SELECT count(1) FROM tmp_trg_info;
+ count
+-------
+ 14
+(1 row)
+
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+-- No triggers
+SELECT count(1) FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+ count
+-------
+ 0
+(1 row)
+
+-- Changing it back to ENFORCED will recreate the necessary triggers.
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+-- Should be exactly the same number of triggers found as before
+SELECT COUNT(1) FROM pg_trigger pgt JOIN tmp_trg_info tt
+ON (pgt.tgtype = tt.tgtype AND pgt.tgrelid = tt.tgrelid AND pgt.tgconstraint = tt.tgconstraint);
+ count
+-------
+ 14
+(1 row)
+
ALTER TABLE fk_partitioned_fk DROP CONSTRAINT fk_partitioned_fk_a_b_fkey;
-- done.
DROP TABLE fk_notpartitioned_pk, fk_partitioned_fk;
@@ -1868,6 +1952,40 @@ Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
Foreign-key constraints:
TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
+DROP TABLE fk_partitioned_fk_2;
+CREATE TABLE fk_partitioned_fk_2 (b int, a int,
+ FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
+BEGIN;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have two constraints
+\d fk_partitioned_fk_2
+ Table "public.fk_partitioned_fk_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ b | integer | | |
+ a | integer | | |
+Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
+Foreign-key constraints:
+ "fk_partitioned_fk_2_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED
+ TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
+
+DROP TABLE fk_partitioned_fk_2;
+ROLLBACK;
+BEGIN;
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have only one constraint
+\d fk_partitioned_fk_2
+ Table "public.fk_partitioned_fk_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ b | integer | | |
+ a | integer | | |
+Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
+Foreign-key constraints:
+ TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED
+
+ROLLBACK;
DROP TABLE fk_partitioned_fk_2;
CREATE TABLE fk_partitioned_fk_4 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE) PARTITION BY RANGE (b, a);
CREATE TABLE fk_partitioned_fk_4_1 PARTITION OF fk_partitioned_fk_4 FOR VALUES FROM (1,1) TO (100,100);
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index 2e710e419c2..5931a9ac19e 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -2,13 +2,40 @@
-- FOREIGN KEY
--
--- MATCH FULL
+-- NOT ENFORCED
--
-- First test, check and cascade
--
CREATE TABLE PKTABLE ( ptest1 int PRIMARY KEY, ptest2 text );
-CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE, ftest2 int );
+CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE NOT ENFORCED,
+ ftest2 int );
+-- Inserting into the foreign key table will not result in an error, even if
+-- there is no matching key in the referenced table.
+INSERT INTO FKTABLE VALUES (1, 2);
+INSERT INTO FKTABLE VALUES (2, 3);
+
+-- Check FKTABLE
+SELECT * FROM FKTABLE;
+
+-- Changing it back to ENFORCED will leave the constraint in the NOT VALID state
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+-- Which needs to be explicitly validated.
+ALTER TABLE FKTABLE VALIDATE CONSTRAINT fktable_ftest1_fkey;
+
+-- Remove any existing rows that violate the constraint, then attempt to change
+-- it.
+TRUNCATE FKTABLE;
+ALTER TABLE FKTABLE VALIDATE CONSTRAINT fktable_ftest1_fkey;
+
+-- Any further inserts will fail due to the enforcement.
+INSERT INTO FKTABLE VALUES (3, 4);
+
+--
+-- MATCH FULL
+--
+-- First test, check and cascade
+--
-- Insert test data into PKTABLE
INSERT INTO PKTABLE VALUES (1, 'Test1');
INSERT INTO PKTABLE VALUES (2, 'Test2');
@@ -230,6 +257,9 @@ INSERT INTO FKTABLE VALUES (1, NULL);
ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL;
+-- NOT ENFORCED will bypass the initial check.
+ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL NOT ENFORCED;
+
DROP TABLE FKTABLE;
DROP TABLE PKTABLE;
@@ -968,10 +998,23 @@ INSERT INTO fktable VALUES (0, 20);
COMMIT;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT ENFORCED;
+
+BEGIN;
+
+-- doesn't match FK, no error.
+UPDATE pktable SET id = 10 WHERE id = 5;
+-- doesn't match PK, no error.
+INSERT INTO fktable VALUES (0, 20);
+
+ROLLBACK;
+
-- try additional syntax
-ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE ENFORCED;
-- illegal option
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORCED;
+CREATE TEMP TABLE fktable2 (fk int references pktable ENFORCED NOT ENFORCED);
-- test order of firing of FK triggers when several RI-induced changes need to
-- be made to the same row. This was broken by subtransaction-related
@@ -1182,11 +1225,16 @@ ALTER TABLE fk_partitioned_fk DROP COLUMN fdrop1;
CREATE TABLE fk_partitioned_fk_1 (fdrop1 int, fdrop2 int, a int, fdrop3 int, b int);
ALTER TABLE fk_partitioned_fk_1 DROP COLUMN fdrop1, DROP COLUMN fdrop2, DROP COLUMN fdrop3;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FROM (0,0) TO (1000,1000);
-ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk;
+ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
+ALTER TABLE fk_partitioned_fk_2 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
+-- Changing to ENFORCED does not alter the validation status; it must be explicitly validated.
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+ALTER TABLE fk_partitioned_fk VALIDATE CONSTRAINT fk_partitioned_fk_a_b_fkey ;
+
CREATE TABLE fk_partitioned_fk_3 (fdrop1 int, fdrop2 int, fdrop3 int, fdrop4 int, b int, a int)
PARTITION BY HASH (a);
ALTER TABLE fk_partitioned_fk_3 DROP COLUMN fdrop1, DROP COLUMN fdrop2,
@@ -1236,6 +1284,27 @@ UPDATE fk_notpartitioned_pk SET b = 1502 WHERE a = 1500;
UPDATE fk_notpartitioned_pk SET b = 2504 WHERE a = 2500;
-- check psql behavior
\d fk_notpartitioned_pk
+
+-- Check the exsting FK trigger
+CREATE TEMP TABLE tmp_trg_info AS SELECT tgtype, tgrelid::regclass, tgconstraint
+FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+SELECT count(1) FROM tmp_trg_info;
+
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+-- No triggers
+SELECT count(1) FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+
+-- Changing it back to ENFORCED will recreate the necessary triggers.
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+
+-- Should be exactly the same number of triggers found as before
+SELECT COUNT(1) FROM pg_trigger pgt JOIN tmp_trg_info tt
+ON (pgt.tgtype = tt.tgtype AND pgt.tgrelid = tt.tgrelid AND pgt.tgconstraint = tt.tgconstraint);
+
ALTER TABLE fk_partitioned_fk DROP CONSTRAINT fk_partitioned_fk_a_b_fkey;
-- done.
DROP TABLE fk_notpartitioned_pk, fk_partitioned_fk;
@@ -1372,6 +1441,22 @@ ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN
\d fk_partitioned_fk_2
DROP TABLE fk_partitioned_fk_2;
+CREATE TABLE fk_partitioned_fk_2 (b int, a int,
+ FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
+BEGIN;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have two constraints
+\d fk_partitioned_fk_2
+DROP TABLE fk_partitioned_fk_2;
+ROLLBACK;
+BEGIN;
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have only one constraint
+\d fk_partitioned_fk_2
+ROLLBACK;
+DROP TABLE fk_partitioned_fk_2;
+
CREATE TABLE fk_partitioned_fk_4 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE) PARTITION BY RANGE (b, a);
CREATE TABLE fk_partitioned_fk_4_1 PARTITION OF fk_partitioned_fk_4 FOR VALUES FROM (1,1) TO (100,100);
CREATE TABLE fk_partitioned_fk_4_2 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE SET NULL);
--
2.43.5
On Tue, Dec 10, 2024 at 7:48 PM Amul Sul <sulamul@gmail.com> wrote:
static bool MergeWithExistingConstraint(Relation rel, const char *ccname, Node *expr, bool allow_merge, bool is_local, + bool is_enforced, bool is_initially_valid, bool is_no_inherit) { @@ -2729,12 +2738,24 @@ MergeWithExistingConstraint(Relation rel, const char *ccname, Node *expr, * If the child constraint is "not valid" then cannot merge with a * valid parent constraint. */ - if (is_initially_valid && !con->convalidated) + if (is_initially_valid && con->conenforced && !con->convalidated) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), errmsg("constraint \"%s\" conflicts with NOT VALID constraint on relation \"%s\"", ccname, RelationGetRelationName(rel))));There are no tests for this change. I think this change is not necessary.
It is necessary; otherwise, it would raise an error for a NOT ENFORCED
constraint, which is NOT VALID by default.
got it.
overall v8-0001 looks good to me!
do you have a patch for
alter check constraint set [not] enforced?
If not, I will probably try to work on it.
I am playing around with the remaining patch.
ATExecAlterConstrRecurse
ATExecAlterConstrEnforceability
ATExecAlterChildConstr
AlterConstrTriggerDeferrability
These four functions take a lot of arguments.
more comments about these arguments would be helpful.
we only need to mention it at ATExecAlterConstrRecurse.
for example:
ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
const Oid fkrelid, const Oid pkrelid,
HeapTuple contuple, List **otherrelids,
LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
Oid ReferencedParentUpdTrigger,
Oid ReferencingParentInsTrigger,
Oid ReferencingParentUpdTrigger)
the comments only explained otherrelids.
LOCKMODE lockmode,
Oid ReferencedParentDelTrigger,
Oid ReferencedParentUpdTrigger,
Oid ReferencingParentInsTrigger,
Oid ReferencingParentUpdTrigger
The above arguments are pretty intuitive.
Constraint *cmdcon
Relation conrel
Relation tgrel
HeapTuple contuple
but these arguments are not that very intuitive,
especially these arguments passing to another function.
On Wed, Dec 11, 2024 at 6:12 PM jian he <jian.universality@gmail.com> wrote:
On Tue, Dec 10, 2024 at 7:48 PM Amul Sul <sulamul@gmail.com> wrote:
static bool MergeWithExistingConstraint(Relation rel, const char *ccname, Node *expr, bool allow_merge, bool is_local, + bool is_enforced, bool is_initially_valid, bool is_no_inherit) { @@ -2729,12 +2738,24 @@ MergeWithExistingConstraint(Relation rel, const char *ccname, Node *expr, * If the child constraint is "not valid" then cannot merge with a * valid parent constraint. */ - if (is_initially_valid && !con->convalidated) + if (is_initially_valid && con->conenforced && !con->convalidated) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), errmsg("constraint \"%s\" conflicts with NOT VALID constraint on relation \"%s\"", ccname, RelationGetRelationName(rel))));There are no tests for this change. I think this change is not necessary.
It is necessary; otherwise, it would raise an error for a NOT ENFORCED
constraint, which is NOT VALID by default.got it.
overall v8-0001 looks good to me!
Thank you.
do you have a patch for
alter check constraint set [not] enforced?
If not, I will probably try to work on it.
Not yet; I believe I need to first look into allowing NOT VALID
foreign key constraints on partitioned tables.
I am playing around with the remaining patch.
ATExecAlterConstrRecurse
ATExecAlterConstrEnforceability
ATExecAlterChildConstr
AlterConstrTriggerDeferrability
These four functions take a lot of arguments.
more comments about these arguments would be helpful.
we only need to mention it at ATExecAlterConstrRecurse.for example:
ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
const Oid fkrelid, const Oid pkrelid,
HeapTuple contuple, List **otherrelids,
LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
Oid ReferencedParentUpdTrigger,
Oid ReferencingParentInsTrigger,
Oid ReferencingParentUpdTrigger)
the comments only explained otherrelids.LOCKMODE lockmode,
Oid ReferencedParentDelTrigger,
Oid ReferencedParentUpdTrigger,
Oid ReferencingParentInsTrigger,
Oid ReferencingParentUpdTriggerThe above arguments are pretty intuitive.
Constraint *cmdcon
Relation conrel
Relation tgrel
HeapTuple contuplebut these arguments are not that very intuitive,
especially these arguments passing to another function.
Those are the existing ones; let me think what can be done with them.
Regards,
Amul
Hi,
I have tested different scenarios involving CHECK constraint with NOT
ENFORCED specification on Inherited and Partitioned tables. Additionally, I
explored various situations with foreign key constraints. I have also
examined how pg_dump, pg_dumpall, pg_basebackup, and pg_upgrade handle NOT
ENFORCED constraints.
On Tue, Dec 17, 2024 at 12:23 PM tushar <tushar.ahuja@enterprisedb.com>
wrote:
FYI
---------- Forwarded message ---------
From: Amul Sul <sulamul@gmail.com>
Date: Tue, Dec 10, 2024 at 5:18 PM
Subject: Re: NOT ENFORCED constraint feature
To: jian he <jian.universality@gmail.com>
Cc: Alvaro Herrera <alvherre@alvh.no-ip.org>, Peter Eisentraut <
peter@eisentraut.org>, pgsql-hackers <pgsql-hackers@postgresql.org>, Joel
Jacobson <joel@compiler.org>On Tue, Dec 10, 2024 at 1:21 PM jian he <jian.universality@gmail.com>
wrote:hi. some minor issue about v7-0001.
there are 5 appearances of "sizeof(CookedConstraint)"
to make it safe, it would be nice to manual do
`
cooked->is_enforced = true;
`
for other kinds of constraints.I am not sure if it's necessary, but it doesn't seem like a bad idea,
did the same in the attached version.static bool
MergeWithExistingConstraint(Relation rel, const char *ccname, Node*expr,
bool allow_merge, bool is_local, + bool is_enforced, bool is_initially_valid, bool is_no_inherit) { @@ -2729,12 +2738,24 @@ MergeWithExistingConstraint(Relation rel, const char *ccname, Node *expr, * If the child constraint is "not valid" then cannot merge with a * valid parent constraint. */ - if (is_initially_valid && !con->convalidated) + if (is_initially_valid && con->conenforced && !con->convalidated) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), errmsg("constraint \"%s\" conflicts with NOT VALID constraint on relation \"%s\"", ccname, RelationGetRelationName(rel))));There are no tests for this change. I think this change is not necessary.
It is necessary; otherwise, it would raise an error for a NOT ENFORCED
constraint, which is NOT VALID by default.- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out ... +ALTER TABLE attmp3 VALIDATE CONSTRAINT b_greater_than_ten_not_enforced;-- fail
+ERROR: cannot validated NOT ENFORCED constraint
there should be
ERROR: cannot validate NOT ENFORCED constraint
?Are you sure you're looking at the latest patch? The error string is
already the same as you suggested.Do we need to update create_foreign_table.sgml
and alter_foreign_table.sgml?Yes, I think we just need to add the syntax; a description isn't
necessary, imo, since constraints on foreign constraints are never
enforced.Regards,
Amul
--
Warm regards,
Triveni
Import Notes
Reply to msg id not found: CAC6VRoYeaX+0kcx78oTZ6NJqgXqSCD0KCUDk9GL2p_ThuB8M+w@mail.gmail.com
I have applied v8-0001, with some editing of the documentation and in
the tests. I'll continue reviewing the subsequent patches.
On Saturday, 11 January 2025, Peter Eisentraut <peter@eisentraut.org> wrote:
I have applied v8-0001, with some editing of the documentation and in the
tests. I'll continue reviewing the subsequent patches.
Thank you for the improvement and commit.
Regards,
Amul
--
Regards,
Amul Sul
EDB: http://www.enterprisedb.com
On 11.01.25 18:26, Amul Sul wrote:
On Saturday, 11 January 2025, Peter Eisentraut <peter@eisentraut.org
<mailto:peter@eisentraut.org>> wrote:I have applied v8-0001, with some editing of the documentation and
in the tests. I'll continue reviewing the subsequent patches.Thank you for the improvement and commit.
I have also committed v8-0002, the refactor of ATExecAlterConstrRecurse().
All the remaining patches go together, so w are now waiting on an
updated patch set from you.
On Thu, Jan 16, 2025 at 6:07 PM Peter Eisentraut <peter@eisentraut.org> wrote:
On 11.01.25 18:26, Amul Sul wrote:
On Saturday, 11 January 2025, Peter Eisentraut <peter@eisentraut.org
<mailto:peter@eisentraut.org>> wrote:I have applied v8-0001, with some editing of the documentation and
in the tests. I'll continue reviewing the subsequent patches.Thank you for the improvement and commit.
I have also committed v8-0002, the refactor of ATExecAlterConstrRecurse().
All the remaining patches go together, so w are now waiting on an
updated patch set from you.
Thanks!
Yes, I'm working on it and will post it tomorrow.
Regards,
Amul
On Thu, Jan 16, 2025 at 6:55 PM Amul Sul <sulamul@gmail.com> wrote:
On Thu, Jan 16, 2025 at 6:07 PM Peter Eisentraut <peter@eisentraut.org> wrote:
On 11.01.25 18:26, Amul Sul wrote:
On Saturday, 11 January 2025, Peter Eisentraut <peter@eisentraut.org
<mailto:peter@eisentraut.org>> wrote:I have applied v8-0001, with some editing of the documentation and
in the tests. I'll continue reviewing the subsequent patches.Thank you for the improvement and commit.
I have also committed v8-0002, the refactor of ATExecAlterConstrRecurse().
All the remaining patches go together, so w are now waiting on an
updated patch set from you.Thanks!
Yes, I'm working on it and will post it tomorrow.
Attached is a new set of patches. Please ignore patch 0001 here, which
was posted separately [1] -- proposes allowing invalid foreign key
constraints on partitioned tables. Patch 0002 refactors
tryAttachPartitionForeignKey(), primarily needed by the new patch
v9-0006 included in this set. This patch handles merging constraints
with different enforceability. Without it, a new constraint would be
created on the child. However, this patch introduces additional code
that may appear somewhat messy or confusing. I've added plenty of
comments to clarify the logic. While I’ve done some testing, it hasn’t
been extensive. I plan to do more testing in the next week.
Please let me know if we should continue with patch 0006 improvement,
or if the feature up to patch 0005, which enforces matching
enforceability before merging constraints, is sufficient.
1] /messages/by-id/CAAJ_b96Bp=-ZwihPPtuaNX=SrZ0U6ZsXD3+fgARO0JuKa8v2jQ@mail.gmail.com
Regards,
Amul
Attachments:
v9-0001-POSTED_SEPARATLY-Allow_NOT_VALID_FK_on_Partitione.patchapplication/x-patch; name=v9-0001-POSTED_SEPARATLY-Allow_NOT_VALID_FK_on_Partitione.patchDownload
From c56b6e091b55221eeaee71080ef9fd841b0382a9 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Thu, 16 Jan 2025 22:09:38 +0530
Subject: [PATCH v9 1/6] POSTED_SEPARATLY-Allow_NOT_VALID_FK_on_Partitioned
This is necessary before adding the NOT ENFORCED constraint,
especially on partitioned tables where NOT VALID foreign key
constraints are currently prohibited. As a result, a patch to enable
support for NOT VALID FK constraints on partitioned tables has been
posted here:
https://postgr.es/m/CAAJ_b96Bp=-ZwihPPtuaNX=SrZ0U6ZsXD3+fgARO0JuKa8v2jQ@mail.gmail.com
---
doc/src/sgml/ref/alter_table.sgml | 2 -
src/backend/commands/tablecmds.c | 126 +++++++++++++++-------
src/test/regress/expected/foreign_key.out | 76 +++++++++++--
src/test/regress/sql/foreign_key.sql | 58 +++++++++-
4 files changed, 214 insertions(+), 48 deletions(-)
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index df4f5d5bbd8..f9576da435e 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -486,8 +486,6 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
<para>
Additional restrictions apply when unique or primary key constraints
are added to partitioned tables; see <link linkend="sql-createtable"><command>CREATE TABLE</command></link>.
- Also, foreign key constraints on partitioned
- tables may not be declared <literal>NOT VALID</literal> at present.
</para>
</listitem>
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d2420a9558c..efa47dee5c4 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -574,8 +574,9 @@ static void createForeignKeyActionTriggers(Relation rel, Oid refRelOid,
Oid indexOid,
Oid parentDelTrigger, Oid parentUpdTrigger,
Oid *deleteTrigOid, Oid *updateTrigOid);
-static bool tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
- Oid partRelid,
+static bool tryAttachPartitionForeignKey(List **wqueue,
+ ForeignKeyCacheInfo *fk,
+ Relation partition,
Oid parentConstrOid, int numfks,
AttrNumber *mapped_conkey, AttrNumber *confkey,
Oid *conpfeqop,
@@ -9772,22 +9773,12 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
* Validity checks (permission checks wait till we have the column
* numbers)
*/
- if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
- {
- if (!recurse)
- ereport(ERROR,
- (errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("cannot use ONLY for foreign key on partitioned table \"%s\" referencing relation \"%s\"",
- RelationGetRelationName(rel),
- RelationGetRelationName(pkrel))));
- if (fkconstraint->skip_validation && !fkconstraint->initially_valid)
- ereport(ERROR,
- (errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("cannot add NOT VALID foreign key on partitioned table \"%s\" referencing relation \"%s\"",
- RelationGetRelationName(rel),
- RelationGetRelationName(pkrel)),
- errdetail("This feature is not yet supported on partitioned tables.")));
- }
+ if (!recurse && rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot use ONLY for foreign key on partitioned table \"%s\" referencing relation \"%s\"",
+ RelationGetRelationName(rel),
+ RelationGetRelationName(pkrel))));
if (pkrel->rd_rel->relkind != RELKIND_RELATION &&
pkrel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
@@ -10780,8 +10771,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
*/
for (int i = 0; i < pd->nparts; i++)
{
- Oid partitionId = pd->oids[i];
- Relation partition = table_open(partitionId, lockmode);
+ Relation partition = table_open(pd->oids[i], lockmode);
List *partFKs;
AttrMap *attmap;
AttrNumber mapped_fkattnum[INDEX_MAX_KEYS];
@@ -10805,8 +10795,9 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
ForeignKeyCacheInfo *fk;
fk = lfirst_node(ForeignKeyCacheInfo, cell);
- if (tryAttachPartitionForeignKey(fk,
- partitionId,
+ if (tryAttachPartitionForeignKey(wqueue,
+ fk,
+ partition,
parentConstr,
numfks,
mapped_fkattnum,
@@ -11258,8 +11249,9 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
{
ForeignKeyCacheInfo *fk = lfirst_node(ForeignKeyCacheInfo, lc);
- if (tryAttachPartitionForeignKey(fk,
- RelationGetRelid(partRel),
+ if (tryAttachPartitionForeignKey(wqueue,
+ fk,
+ partRel,
parentConstrOid,
numfks,
mapped_conkey,
@@ -11362,8 +11354,9 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
* return false.
*/
static bool
-tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
- Oid partRelid,
+tryAttachPartitionForeignKey(List **wqueue,
+ ForeignKeyCacheInfo *fk,
+ Relation partition,
Oid parentConstrOid,
int numfks,
AttrNumber *mapped_conkey,
@@ -11382,12 +11375,15 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
HeapTuple trigtup;
Oid insertTriggerOid,
updateTriggerOid;
+ bool parentConstrIsValid;
+ bool partConstrIsValid;
parentConstrTup = SearchSysCache1(CONSTROID,
ObjectIdGetDatum(parentConstrOid));
if (!HeapTupleIsValid(parentConstrTup))
elog(ERROR, "cache lookup failed for constraint %u", parentConstrOid);
parentConstr = (Form_pg_constraint) GETSTRUCT(parentConstrTup);
+ parentConstrIsValid = parentConstr->convalidated;
/*
* Do some quick & easy initial checks. If any of these fail, we cannot
@@ -11410,17 +11406,18 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
}
/*
- * Looks good so far; do some more extensive checks. Presumably the check
- * for 'convalidated' could be dropped, since we don't really care about
- * that, but let's be careful for now.
+ * Looks good so far; perform more extensive checks except for
+ * convalidated. There’s no need to worry about it because attaching a
+ * valid parent constraint to an invalid child constraint will eventually
+ * trigger implicit data validation for the child.
*/
partcontup = SearchSysCache1(CONSTROID,
ObjectIdGetDatum(fk->conoid));
if (!HeapTupleIsValid(partcontup))
elog(ERROR, "cache lookup failed for constraint %u", fk->conoid);
partConstr = (Form_pg_constraint) GETSTRUCT(partcontup);
+ partConstrIsValid = parentConstr->convalidated;
if (OidIsValid(partConstr->conparentid) ||
- !partConstr->convalidated ||
partConstr->condeferrable != parentConstr->condeferrable ||
partConstr->condeferred != parentConstr->condeferred ||
partConstr->confupdtype != parentConstr->confupdtype ||
@@ -11479,7 +11476,8 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
systable_endscan(scan);
- ConstraintSetParentConstraint(fk->conoid, parentConstrOid, partRelid);
+ ConstraintSetParentConstraint(fk->conoid, parentConstrOid,
+ RelationGetRelid(partition));
/*
* Like the constraint, attach partition's "check" triggers to the
@@ -11490,10 +11488,10 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
&insertTriggerOid, &updateTriggerOid);
Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
- partRelid);
+ RelationGetRelid(partition));
Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
- partRelid);
+ RelationGetRelid(partition));
/*
* If the referenced table is partitioned, then the partition we're
@@ -11571,6 +11569,32 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
}
CommandCounterIncrement();
+
+ /*
+ * If a valid parent constraint is attached to a NOT VALID child
+ * constraint, implicitly trigger validation for the child constraint.
+ * This behavior aligns with creating a new constraint on the child table
+ * rather than attaching it to the existing one, as it would ultimately
+ * validate the child data. Conversely, having an invalid parent
+ * constraint while the child constraint is valid doesn't cause any harm.
+ */
+ if (parentConstrIsValid && !partConstrIsValid);
+ {
+ Relation conrel;
+
+ conrel = table_open(ConstraintRelationId, RowExclusiveLock);
+
+ partcontup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
+ if (!HeapTupleIsValid(partcontup))
+ elog(ERROR, "cache lookup failed for constraint %u", fk->conoid);
+
+ /* Using the same lock as used in AT_ValidateConstraint */
+ QueueFKConstraintValidation(wqueue, conrel, partition, partcontup,
+ ShareUpdateExclusiveLock);
+ ReleaseSysCache(partcontup);
+ table_close(conrel, RowExclusiveLock);
+ }
+
return true;
}
@@ -12111,7 +12135,7 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
*
* Add an entry to the wqueue to validate the given foreign key constraint in
* Phase 3 and update the convalidated field in the pg_constraint catalog
- * for the specified relation.
+ * for the specified relation and all its children.
*/
static void
QueueFKConstraintValidation(List **wqueue, Relation conrel, Relation rel,
@@ -12149,9 +12173,39 @@ QueueFKConstraintValidation(List **wqueue, Relation conrel, Relation rel,
}
/*
- * We disallow creating invalid foreign keys to or from partitioned
- * tables, so ignoring the recursion bit is okay.
+ * If the table at either end of the constraint is partitioned, we need to
+ * recurse and handle every constraint that is a child of this constraint.
*/
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(con->confrelid) == RELKIND_PARTITIONED_TABLE)
+ {
+ ScanKeyData pkey;
+ SysScanDesc pscan;
+ HeapTuple childtup;
+
+ ScanKeyInit(&pkey,
+ Anum_pg_constraint_conparentid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(con->oid));
+
+ pscan = systable_beginscan(conrel, ConstraintParentIndexId,
+ true, NULL, 1, &pkey);
+
+ while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
+ {
+ Form_pg_constraint childcon;
+ Relation childrel;
+
+ childcon = (Form_pg_constraint) GETSTRUCT(childtup);
+ childrel = table_open(childcon->conrelid, lockmode);
+
+ QueueFKConstraintValidation(wqueue, conrel, childrel, childtup,
+ lockmode);
+ table_close(childrel, NoLock);
+ }
+
+ systable_endscan(pscan);
+ }
/*
* Now update the catalog, while we have the door open.
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 3f459f70ac1..f617d519aef 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -1571,6 +1571,76 @@ drop table pktable2, fktable2;
--
-- Foreign keys and partitioned tables
--
+-- NOT VALID foreign key on partitioned table
+CREATE TABLE fk_notpartitioned_pk (a int, b int, PRIMARY KEY (a, b));
+CREATE TABLE fk_partitioned_fk (b int, a int) PARTITION BY RANGE (a, b);
+ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT VALID;
+-- Attaching a child table with the same valid foreign key constraint.
+CREATE TABLE fk_partitioned_fk_1 (a int, b int);
+ALTER TABLE fk_partitioned_fk_1 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FROM (0,0) TO (1000,1000);
+-- Child constraint will remain valid.
+SELECT conname, convalidated, conrelid::regclass FROM pg_constraint
+WHERE conrelid::regclass::text like 'fk_partitioned_fk%' ORDER BY oid;
+ conname | convalidated | conrelid
+------------------------------+--------------+---------------------
+ fk_partitioned_fk_a_b_fkey | f | fk_partitioned_fk
+ fk_partitioned_fk_1_a_b_fkey | t | fk_partitioned_fk_1
+(2 rows)
+
+-- Validate the constraint
+ALTER TABLE fk_partitioned_fk VALIDATE CONSTRAINT fk_partitioned_fk_a_b_fkey;
+-- All constraints are now valid.
+SELECT conname, convalidated, conrelid::regclass FROM pg_constraint
+WHERE conrelid::regclass::text like 'fk_partitioned_fk%' ORDER BY oid;
+ conname | convalidated | conrelid
+------------------------------+--------------+---------------------
+ fk_partitioned_fk_a_b_fkey | t | fk_partitioned_fk
+ fk_partitioned_fk_1_a_b_fkey | t | fk_partitioned_fk_1
+(2 rows)
+
+-- Attaching a child with a NOT VALID constraint.
+CREATE TABLE fk_partitioned_fk_2 (a int, b int);
+INSERT INTO fk_partitioned_fk_2 VALUES(1000, 1000); -- doesn't exist in referenced table
+ALTER TABLE fk_partitioned_fk_2 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT VALID;
+-- It will fail because the attach operation implicitly validates the data.
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
+ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_2_a_b_fkey"
+DETAIL: Key (a, b)=(1000, 1000) is not present in table "fk_notpartitioned_pk".
+-- Remove the invalid data and try again.
+TRUNCATE fk_partitioned_fk_2;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
+-- The child constraint will also be valid.
+SELECT conname, convalidated FROM pg_constraint WHERE conrelid = 'fk_partitioned_fk_2'::regclass;
+ conname | convalidated
+------------------------------+--------------
+ fk_partitioned_fk_2_a_b_fkey | t
+(1 row)
+
+DROP TABLE fk_partitioned_fk, fk_notpartitioned_pk;
+-- NOT VALID foreign key on a non-partitioned table referencing a partitioned table
+CREATE TABLE fk_partitioned_pk (a int, b int, PRIMARY KEY (a, b)) PARTITION BY RANGE (a, b);
+CREATE TABLE fk_partitioned_pk_1 PARTITION OF fk_partitioned_pk FOR VALUES FROM (0,0) TO (1000,1000);
+CREATE TABLE fk_notpartitioned_fk (b int, a int);
+ALTER TABLE fk_notpartitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk NOT VALID;
+-- Constraint will be invalid.
+SELECT conname, convalidated FROM pg_constraint WHERE conrelid = 'fk_notpartitioned_fk'::regclass;
+ conname | convalidated
+--------------------------------+--------------
+ fk_notpartitioned_fk_a_b_fkey | f
+ fk_notpartitioned_fk_a_b_fkey1 | f
+(2 rows)
+
+ALTER TABLE fk_notpartitioned_fk VALIDATE CONSTRAINT fk_notpartitioned_fk_a_b_fkey;
+-- All constraints are now valid.
+SELECT conname, convalidated FROM pg_constraint WHERE conrelid = 'fk_notpartitioned_fk'::regclass;
+ conname | convalidated
+--------------------------------+--------------
+ fk_notpartitioned_fk_a_b_fkey | t
+ fk_notpartitioned_fk_a_b_fkey1 | t
+(2 rows)
+
+DROP TABLE fk_notpartitioned_fk, fk_partitioned_pk;
-- Creation of a partitioned hierarchy with irregular definitions
CREATE TABLE fk_notpartitioned_pk (fdrop1 int, a int, fdrop2 int, b int,
PRIMARY KEY (a, b));
@@ -1597,12 +1667,6 @@ ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_3
ALTER TABLE ONLY fk_partitioned_fk ADD FOREIGN KEY (a, b)
REFERENCES fk_notpartitioned_pk;
ERROR: cannot use ONLY for foreign key on partitioned table "fk_partitioned_fk" referencing relation "fk_notpartitioned_pk"
--- Adding a NOT VALID foreign key on a partitioned table referencing
--- a non-partitioned table fails.
-ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b)
- REFERENCES fk_notpartitioned_pk NOT VALID;
-ERROR: cannot add NOT VALID foreign key on partitioned table "fk_partitioned_fk" referencing relation "fk_notpartitioned_pk"
-DETAIL: This feature is not yet supported on partitioned tables.
-- these inserts, targeting both the partition directly as well as the
-- partitioned table, should all fail
INSERT INTO fk_partitioned_fk (a,b) VALUES (500, 501);
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index 2e710e419c2..d5f68812ba6 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -1173,6 +1173,60 @@ drop table pktable2, fktable2;
-- Foreign keys and partitioned tables
--
+-- NOT VALID foreign key on partitioned table
+CREATE TABLE fk_notpartitioned_pk (a int, b int, PRIMARY KEY (a, b));
+CREATE TABLE fk_partitioned_fk (b int, a int) PARTITION BY RANGE (a, b);
+ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT VALID;
+
+-- Attaching a child table with the same valid foreign key constraint.
+CREATE TABLE fk_partitioned_fk_1 (a int, b int);
+ALTER TABLE fk_partitioned_fk_1 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FROM (0,0) TO (1000,1000);
+
+-- Child constraint will remain valid.
+SELECT conname, convalidated, conrelid::regclass FROM pg_constraint
+WHERE conrelid::regclass::text like 'fk_partitioned_fk%' ORDER BY oid;
+
+-- Validate the constraint
+ALTER TABLE fk_partitioned_fk VALIDATE CONSTRAINT fk_partitioned_fk_a_b_fkey;
+
+-- All constraints are now valid.
+SELECT conname, convalidated, conrelid::regclass FROM pg_constraint
+WHERE conrelid::regclass::text like 'fk_partitioned_fk%' ORDER BY oid;
+
+-- Attaching a child with a NOT VALID constraint.
+CREATE TABLE fk_partitioned_fk_2 (a int, b int);
+INSERT INTO fk_partitioned_fk_2 VALUES(1000, 1000); -- doesn't exist in referenced table
+ALTER TABLE fk_partitioned_fk_2 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT VALID;
+
+-- It will fail because the attach operation implicitly validates the data.
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
+
+-- Remove the invalid data and try again.
+TRUNCATE fk_partitioned_fk_2;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
+
+-- The child constraint will also be valid.
+SELECT conname, convalidated FROM pg_constraint WHERE conrelid = 'fk_partitioned_fk_2'::regclass;
+
+DROP TABLE fk_partitioned_fk, fk_notpartitioned_pk;
+
+-- NOT VALID foreign key on a non-partitioned table referencing a partitioned table
+CREATE TABLE fk_partitioned_pk (a int, b int, PRIMARY KEY (a, b)) PARTITION BY RANGE (a, b);
+CREATE TABLE fk_partitioned_pk_1 PARTITION OF fk_partitioned_pk FOR VALUES FROM (0,0) TO (1000,1000);
+CREATE TABLE fk_notpartitioned_fk (b int, a int);
+ALTER TABLE fk_notpartitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk NOT VALID;
+
+-- Constraint will be invalid.
+SELECT conname, convalidated FROM pg_constraint WHERE conrelid = 'fk_notpartitioned_fk'::regclass;
+
+ALTER TABLE fk_notpartitioned_fk VALIDATE CONSTRAINT fk_notpartitioned_fk_a_b_fkey;
+
+-- All constraints are now valid.
+SELECT conname, convalidated FROM pg_constraint WHERE conrelid = 'fk_notpartitioned_fk'::regclass;
+
+DROP TABLE fk_notpartitioned_fk, fk_partitioned_pk;
+
-- Creation of a partitioned hierarchy with irregular definitions
CREATE TABLE fk_notpartitioned_pk (fdrop1 int, a int, fdrop2 int, b int,
PRIMARY KEY (a, b));
@@ -1200,10 +1254,6 @@ ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_3
-- a non-partitioned table fails.
ALTER TABLE ONLY fk_partitioned_fk ADD FOREIGN KEY (a, b)
REFERENCES fk_notpartitioned_pk;
--- Adding a NOT VALID foreign key on a partitioned table referencing
--- a non-partitioned table fails.
-ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b)
- REFERENCES fk_notpartitioned_pk NOT VALID;
-- these inserts, targeting both the partition directly as well as the
-- partitioned table, should all fail
--
2.43.5
v9-0002-refactor-Split-tryAttachPartitionForeignKey.patchapplication/x-patch; name=v9-0002-refactor-Split-tryAttachPartitionForeignKey.patchDownload
From 8a91f5f86c0507ea640735d6c11fb7f4693e03d7 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Wed, 15 Jan 2025 12:57:15 +0530
Subject: [PATCH v9 2/6] refactor: Split tryAttachPartitionForeignKey()
Split tryAttachPartitionForeignKey() into three functions:
AttachPartitionForeignKey(), RemoveInheritedConstraint() and
DropForeignKeyConstraintTriggers(), so they can be reused in the next
patch.
---
XXX: Function names could be better
---
---
src/backend/commands/tablecmds.c | 324 ++++++++++++++++++++-----------
1 file changed, 206 insertions(+), 118 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index efa47dee5c4..c7c35c52a3d 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -583,6 +583,14 @@ static bool tryAttachPartitionForeignKey(List **wqueue,
Oid parentInsTrigger,
Oid parentUpdTrigger,
Relation trigrel);
+static void AttachPartitionForeignKey(List **wqueue, Relation partition,
+ Oid partConstrOid, Oid parentConstrOid,
+ Oid parentInsTrigger, Oid parentUpdTrigger,
+ Relation trigrel);
+static void RemoveInheritedConstraint(Relation conrel, Relation trigrel, Oid conrelid,
+ Oid conparentid);
+static void DropForeignKeyConstraintTriggers(Relation trigrel, Oid conoid,
+ Oid confrelid, Oid conrelid);
static void GetForeignKeyActionTriggers(Relation trigrel,
Oid conoid, Oid confrelid, Oid conrelid,
Oid *deleteTriggerOid,
@@ -11370,20 +11378,12 @@ tryAttachPartitionForeignKey(List **wqueue,
Form_pg_constraint parentConstr;
HeapTuple partcontup;
Form_pg_constraint partConstr;
- ScanKeyData key;
- SysScanDesc scan;
- HeapTuple trigtup;
- Oid insertTriggerOid,
- updateTriggerOid;
- bool parentConstrIsValid;
- bool partConstrIsValid;
parentConstrTup = SearchSysCache1(CONSTROID,
ObjectIdGetDatum(parentConstrOid));
if (!HeapTupleIsValid(parentConstrTup))
elog(ERROR, "cache lookup failed for constraint %u", parentConstrOid);
parentConstr = (Form_pg_constraint) GETSTRUCT(parentConstrTup);
- parentConstrIsValid = parentConstr->convalidated;
/*
* Do some quick & easy initial checks. If any of these fail, we cannot
@@ -11416,7 +11416,6 @@ tryAttachPartitionForeignKey(List **wqueue,
if (!HeapTupleIsValid(partcontup))
elog(ERROR, "cache lookup failed for constraint %u", fk->conoid);
partConstr = (Form_pg_constraint) GETSTRUCT(partcontup);
- partConstrIsValid = parentConstr->convalidated;
if (OidIsValid(partConstr->conparentid) ||
partConstr->condeferrable != parentConstr->condeferrable ||
partConstr->condeferred != parentConstr->condeferred ||
@@ -11429,54 +11428,75 @@ tryAttachPartitionForeignKey(List **wqueue,
return false;
}
+ ReleaseSysCache(parentConstrTup);
+ ReleaseSysCache(partcontup);
+
+ /* Looks good! Attach this constraint. */
+ AttachPartitionForeignKey(wqueue, partition, fk->conoid,
+ parentConstrOid, parentInsTrigger,
+ parentUpdTrigger, trigrel);
+
+ return true;
+}
+
+/*
+ * AttachPartitionForeignKey
+ *
+ * The subroutine for tryAttachPartitionForeignKey performs the final tasks of
+ * attaching the constraint, removing redundant triggers and entries from
+ * pg_constraint, and setting the constraint's parent.
+ */
+static void
+AttachPartitionForeignKey(List **wqueue,
+ Relation partition,
+ Oid partConstrOid,
+ Oid parentConstrOid,
+ Oid parentInsTrigger,
+ Oid parentUpdTrigger,
+ Relation trigrel)
+{
+ HeapTuple parentConstrTup;
+ Form_pg_constraint parentConstr;
+ HeapTuple partcontup;
+ Form_pg_constraint partConstr;
+ Oid partConstrFrelid;
+ Oid partConstrRelid;
+ bool parentConstrIsValid;
+ bool partConstrIsValid;
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
+ /* Fetch the parent constraint tuple */
+ parentConstrTup = SearchSysCache1(CONSTROID,
+ ObjectIdGetDatum(parentConstrOid));
+ if (!HeapTupleIsValid(parentConstrTup))
+ elog(ERROR, "cache lookup failed for constraint %u", parentConstrOid);
+ parentConstr = (Form_pg_constraint) GETSTRUCT(parentConstrTup);
+ parentConstrIsValid = parentConstr->convalidated;
+
+ /* Fetch the child constraint tuple */
+ partcontup = SearchSysCache1(CONSTROID,
+ ObjectIdGetDatum(partConstrOid));
+ if (!HeapTupleIsValid(partcontup))
+ elog(ERROR, "cache lookup failed for constraint %u", partConstrOid);
+ partConstr = (Form_pg_constraint) GETSTRUCT(partcontup);
+ partConstrIsValid = parentConstr->convalidated;
+ partConstrFrelid = partConstr->confrelid;
+ partConstrRelid = partConstr->conrelid;
+
ReleaseSysCache(partcontup);
ReleaseSysCache(parentConstrTup);
/*
- * Looks good! Attach this constraint. The action triggers in the new
- * partition become redundant -- the parent table already has equivalent
- * ones, and those will be able to reach the partition. Remove the ones
- * in the partition. We identify them because they have our constraint
- * OID, as well as being on the referenced rel.
+ * The action triggers in the new partition become redundant -- the parent
+ * table already has equivalent ones, and those will be able to reach the
+ * partition. Remove the ones in the partition. We identify them because
+ * they have our constraint OID, as well as being on the referenced rel.
*/
- ScanKeyInit(&key,
- Anum_pg_trigger_tgconstraint,
- BTEqualStrategyNumber, F_OIDEQ,
- ObjectIdGetDatum(fk->conoid));
- scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
- NULL, 1, &key);
- while ((trigtup = systable_getnext(scan)) != NULL)
- {
- Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
- ObjectAddress trigger;
+ DropForeignKeyConstraintTriggers(trigrel, partConstrOid, partConstrFrelid,
+ partConstrRelid);
- if (trgform->tgconstrrelid != fk->conrelid)
- continue;
- if (trgform->tgrelid != fk->confrelid)
- continue;
-
- /*
- * The constraint is originally set up to contain this trigger as an
- * implementation object, so there's a dependency record that links
- * the two; however, since the trigger is no longer needed, we remove
- * the dependency link in order to be able to drop the trigger while
- * keeping the constraint intact.
- */
- deleteDependencyRecordsFor(TriggerRelationId,
- trgform->oid,
- false);
- /* make dependency deletion visible to performDeletion */
- CommandCounterIncrement();
- ObjectAddressSet(trigger, TriggerRelationId,
- trgform->oid);
- performDeletion(&trigger, DROP_RESTRICT, 0);
- /* make trigger drop visible, in case the loop iterates */
- CommandCounterIncrement();
- }
-
- systable_endscan(scan);
-
- ConstraintSetParentConstraint(fk->conoid, parentConstrOid,
+ ConstraintSetParentConstraint(partConstrOid, parentConstrOid,
RelationGetRelid(partition));
/*
@@ -11484,7 +11504,7 @@ tryAttachPartitionForeignKey(List **wqueue,
* corresponding parent triggers.
*/
GetForeignKeyCheckTriggers(trigrel,
- fk->conoid, fk->confrelid, fk->conrelid,
+ partConstrOid, partConstrFrelid, partConstrRelid,
&insertTriggerOid, &updateTriggerOid);
Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
@@ -11498,72 +11518,12 @@ tryAttachPartitionForeignKey(List **wqueue,
* attaching now has extra pg_constraint rows and action triggers that are
* no longer needed. Remove those.
*/
- if (get_rel_relkind(fk->confrelid) == RELKIND_PARTITIONED_TABLE)
+ if (get_rel_relkind(partConstrFrelid) == RELKIND_PARTITIONED_TABLE)
{
Relation pg_constraint = table_open(ConstraintRelationId, RowShareLock);
- ObjectAddresses *objs;
- HeapTuple consttup;
- ScanKeyInit(&key,
- Anum_pg_constraint_conrelid,
- BTEqualStrategyNumber, F_OIDEQ,
- ObjectIdGetDatum(fk->conrelid));
-
- scan = systable_beginscan(pg_constraint,
- ConstraintRelidTypidNameIndexId,
- true, NULL, 1, &key);
- objs = new_object_addresses();
- while ((consttup = systable_getnext(scan)) != NULL)
- {
- Form_pg_constraint conform = (Form_pg_constraint) GETSTRUCT(consttup);
-
- if (conform->conparentid != fk->conoid)
- continue;
- else
- {
- ObjectAddress addr;
- SysScanDesc scan2;
- ScanKeyData key2;
- int n PG_USED_FOR_ASSERTS_ONLY;
-
- ObjectAddressSet(addr, ConstraintRelationId, conform->oid);
- add_exact_object_address(&addr, objs);
-
- /*
- * First we must delete the dependency record that binds the
- * constraint records together.
- */
- n = deleteDependencyRecordsForSpecific(ConstraintRelationId,
- conform->oid,
- DEPENDENCY_INTERNAL,
- ConstraintRelationId,
- fk->conoid);
- Assert(n == 1); /* actually only one is expected */
-
- /*
- * Now search for the triggers for this constraint and set
- * them up for deletion too
- */
- ScanKeyInit(&key2,
- Anum_pg_trigger_tgconstraint,
- BTEqualStrategyNumber, F_OIDEQ,
- ObjectIdGetDatum(conform->oid));
- scan2 = systable_beginscan(trigrel, TriggerConstraintIndexId,
- true, NULL, 1, &key2);
- while ((trigtup = systable_getnext(scan2)) != NULL)
- {
- ObjectAddressSet(addr, TriggerRelationId,
- ((Form_pg_trigger) GETSTRUCT(trigtup))->oid);
- add_exact_object_address(&addr, objs);
- }
- systable_endscan(scan2);
- }
- }
- /* make the dependency deletions visible */
- CommandCounterIncrement();
- performMultipleDeletions(objs, DROP_RESTRICT,
- PERFORM_DELETION_INTERNAL);
- systable_endscan(scan);
+ RemoveInheritedConstraint(pg_constraint, trigrel, partConstrRelid,
+ partConstrOid);
table_close(pg_constraint, RowShareLock);
}
@@ -11584,9 +11544,9 @@ tryAttachPartitionForeignKey(List **wqueue,
conrel = table_open(ConstraintRelationId, RowExclusiveLock);
- partcontup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
+ partcontup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(partConstrOid));
if (!HeapTupleIsValid(partcontup))
- elog(ERROR, "cache lookup failed for constraint %u", fk->conoid);
+ elog(ERROR, "cache lookup failed for constraint %u", partConstrOid);
/* Using the same lock as used in AT_ValidateConstraint */
QueueFKConstraintValidation(wqueue, conrel, partition, partcontup,
@@ -11594,8 +11554,136 @@ tryAttachPartitionForeignKey(List **wqueue,
ReleaseSysCache(partcontup);
table_close(conrel, RowExclusiveLock);
}
+}
- return true;
+/*
+ * RemoveInheritedConstraint
+ *
+ * Removes the constraint and its associated trigger from the specified
+ * relation, which inherited the given constraint.
+ */
+static void
+RemoveInheritedConstraint(Relation conrel, Relation trigrel, Oid conrelid,
+ Oid conparentid)
+{
+ ObjectAddresses *objs;
+ HeapTuple consttup;
+ ScanKeyData key;
+ SysScanDesc scan;
+ HeapTuple trigtup;
+
+ ScanKeyInit(&key,
+ Anum_pg_constraint_conrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conrelid));
+
+ scan = systable_beginscan(conrel,
+ ConstraintRelidTypidNameIndexId,
+ true, NULL, 1, &key);
+ objs = new_object_addresses();
+ while ((consttup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_constraint conform = (Form_pg_constraint) GETSTRUCT(consttup);
+
+ if (conform->conparentid != conparentid)
+ continue;
+ else
+ {
+ ObjectAddress addr;
+ SysScanDesc scan2;
+ ScanKeyData key2;
+ int n PG_USED_FOR_ASSERTS_ONLY;
+
+ ObjectAddressSet(addr, ConstraintRelationId, conform->oid);
+ add_exact_object_address(&addr, objs);
+
+ /*
+ * First we must delete the dependency record that binds the
+ * constraint records together.
+ */
+ n = deleteDependencyRecordsForSpecific(ConstraintRelationId,
+ conform->oid,
+ DEPENDENCY_INTERNAL,
+ ConstraintRelationId,
+ conparentid);
+ Assert(n == 1); /* actually only one is expected */
+
+ /*
+ * Now search for the triggers for this constraint and set them up
+ * for deletion too
+ */
+ ScanKeyInit(&key2,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conform->oid));
+ scan2 = systable_beginscan(trigrel, TriggerConstraintIndexId,
+ true, NULL, 1, &key2);
+ while ((trigtup = systable_getnext(scan2)) != NULL)
+ {
+ ObjectAddressSet(addr, TriggerRelationId,
+ ((Form_pg_trigger) GETSTRUCT(trigtup))->oid);
+ add_exact_object_address(&addr, objs);
+ }
+ systable_endscan(scan2);
+ }
+ }
+ /* make the dependency deletions visible */
+ CommandCounterIncrement();
+ performMultipleDeletions(objs, DROP_RESTRICT,
+ PERFORM_DELETION_INTERNAL);
+ systable_endscan(scan);
+}
+
+/*
+ * DropForeignKeyConstraintTriggers
+ *
+ * The subroutine for tryAttachPartitionForeignKey handles the deletion of
+ * action triggers for the foreign key constraint.
+ */
+static void
+DropForeignKeyConstraintTriggers(Relation trigrel, Oid conoid, Oid confrelid,
+ Oid conrelid)
+{
+ ScanKeyData key;
+ SysScanDesc scan;
+ HeapTuple trigtup;
+
+ ScanKeyInit(&key,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+ scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
+ NULL, 1, &key);
+ while ((trigtup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
+ ObjectAddress trigger;
+
+ if (trgform->tgconstrrelid != conrelid)
+ continue;
+ if (trgform->tgrelid != confrelid)
+ continue;
+
+ /*
+ * The constraint is originally set up to contain this trigger as an
+ * implementation object, so there's a dependency record that links
+ * the two; however, since the trigger is no longer needed, we remove
+ * the dependency link in order to be able to drop the trigger while
+ * keeping the constraint intact.
+ */
+ deleteDependencyRecordsFor(TriggerRelationId,
+ trgform->oid,
+ false);
+ /* make dependency deletion visible to performDeletion */
+ CommandCounterIncrement();
+ ObjectAddressSet(trigger, TriggerRelationId,
+ trgform->oid);
+ performDeletion(&trigger, DROP_RESTRICT, 0);
+ /* make trigger drop visible, in case the loop iterates */
+ CommandCounterIncrement();
+ }
+
+ systable_endscan(scan);
}
/*
--
2.43.5
v9-0003-refactor-Change-ATExecAlterConstrRecurse-argument.patchapplication/x-patch; name=v9-0003-refactor-Change-ATExecAlterConstrRecurse-argument.patchDownload
From 4d3d8675e07499af8280764690458b2a17f15919 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Tue, 14 Jan 2025 22:35:32 +0530
Subject: [PATCH v9 3/6] refactor: Change ATExecAlterConstrRecurse() argument.
Instead of passing the Relation of the referencing relation, we will
pass a relid. Opening the relation using the relid again should not
cause significant issues. However, this approach makes recursion more
efficient, especially in the later patch where we will be recreating
the referencing and referred triggers. In addition to that passing
relid of the referred relation too.
----
NOTE: This patch is not meant to be committed separately. It should
be squashed with the main patch that adds ENFORCED/NOT ENFORCED.
----
---
src/backend/commands/tablecmds.c | 36 +++++++++++++++++---------------
1 file changed, 19 insertions(+), 17 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c7c35c52a3d..e4bde88d432 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -392,13 +392,15 @@ static void AlterSeqNamespaces(Relation classRel, Relation rel,
static ObjectAddress ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd,
bool recurse, bool recursing, LOCKMODE lockmode);
static bool ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
LOCKMODE lockmode);
static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
bool deferrable, bool initdeferred,
List **otherrelids);
static void ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
LOCKMODE lockmode);
static ObjectAddress ATExecValidateConstraint(List **wqueue,
Relation rel, char *constrName,
@@ -11924,8 +11926,9 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
currcon->condeferred != cmdcon->initdeferred ||
rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
- if (ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, rel, contuple,
- &otherrelids, lockmode))
+ if (ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, currcon->conrelid,
+ currcon->confrelid, contuple, &otherrelids,
+ lockmode))
ObjectAddressSet(address, ConstraintRelationId, currcon->oid);
}
@@ -11958,13 +11961,15 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
*/
static bool
ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
LOCKMODE lockmode)
{
Form_pg_constraint currcon;
Oid conoid;
Oid refrelid;
bool changed = false;
+ Relation rel;
/* since this function recurses, it could be driven to stack overflow */
check_stack_depth();
@@ -11973,6 +11978,8 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
conoid = currcon->oid;
refrelid = currcon->confrelid;
+ rel = table_open(currcon->conrelid, lockmode);
+
/*
* Update pg_constraint with the flags from cmdcon.
*
@@ -12018,8 +12025,9 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
*/
if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)
- ATExecAlterChildConstr(cmdcon, conrel, tgrel, rel, contuple,
- otherrelids, lockmode);
+ ATExecAlterChildConstr(cmdcon, conrel, tgrel, fkrelid, pkrelid,
+ contuple, otherrelids, lockmode);
+ table_close(rel, NoLock);
return changed;
}
@@ -12098,7 +12106,8 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
*/
static void
ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
LOCKMODE lockmode)
{
Form_pg_constraint currcon;
@@ -12119,15 +12128,8 @@ ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
true, NULL, 1, &pkey);
while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
- {
- Form_pg_constraint childcon = (Form_pg_constraint) GETSTRUCT(childtup);
- Relation childrel;
-
- childrel = table_open(childcon->conrelid, lockmode);
- ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, childrel, childtup,
- otherrelids, lockmode);
- table_close(childrel, NoLock);
- }
+ ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, fkrelid, pkrelid,
+ childtup, otherrelids, lockmode);
systable_endscan(pscan);
}
--
2.43.5
v9-0004-Remove-hastriggers-flag-check-before-fetching-FK-.patchapplication/x-patch; name=v9-0004-Remove-hastriggers-flag-check-before-fetching-FK-.patchDownload
From e592edd2b95ec85a1ff469b2bfdfb08f7ddbb593 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 7 Oct 2024 12:40:07 +0530
Subject: [PATCH v9 4/6] Remove hastriggers flag check before fetching FK
constraints.
With NOT ENFORCED, foreign key constraints will be created without
triggers. Therefore, the criteria for fetching foreign keys based on
the presence of triggers no longer apply.
----
NOTE: This patch is intended to reduce the diff noise from the main
patch and is not meant to be committed separately. It should be
squashed with the main patch that adds ENFORCED/NOT ENFORCED.
----
---
src/backend/utils/cache/relcache.c | 5 -
src/bin/pg_dump/pg_dump.c | 8 +-
src/bin/psql/describe.c | 226 ++++++++++++++---------------
3 files changed, 108 insertions(+), 131 deletions(-)
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 43219a9629c..485cd2178bc 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4658,11 +4658,6 @@ RelationGetFKeyList(Relation relation)
if (relation->rd_fkeyvalid)
return relation->rd_fkeylist;
- /* Fast path: non-partitioned tables without triggers can't have FKs */
- if (!relation->rd_rel->relhastriggers &&
- relation->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
- return NIL;
-
/*
* We build the list we intend to return (in the caller's context) while
* doing the scan. After successfully completing the scan, we copy that
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 8f73a5df956..166106f363c 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -7841,13 +7841,7 @@ getConstraints(Archive *fout, TableInfo tblinfo[], int numTables)
{
TableInfo *tinfo = &tblinfo[i];
- /*
- * For partitioned tables, foreign keys have no triggers so they must
- * be included anyway in case some foreign keys are defined.
- */
- if ((!tinfo->hastriggers &&
- tinfo->relkind != RELKIND_PARTITIONED_TABLE) ||
- !(tinfo->dobj.dump & DUMP_COMPONENT_DEFINITION))
+ if (!(tinfo->dobj.dump & DUMP_COMPONENT_DEFINITION))
continue;
/* OK, we need info for this table */
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 2ef99971ac0..233b4a38857 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2543,136 +2543,124 @@ describeOneTableDetails(const char *schemaname,
PQclear(result);
}
- /*
- * Print foreign-key constraints (there are none if no triggers,
- * except if the table is partitioned, in which case the triggers
- * appear in the partitions)
- */
- if (tableinfo.hastriggers ||
- tableinfo.relkind == RELKIND_PARTITIONED_TABLE)
+ /* Print foreign-key constraints */
+ if (pset.sversion >= 120000 &&
+ (tableinfo.ispartition || tableinfo.relkind == RELKIND_PARTITIONED_TABLE))
{
- if (pset.sversion >= 120000 &&
- (tableinfo.ispartition || tableinfo.relkind == RELKIND_PARTITIONED_TABLE))
+ /*
+ * Put the constraints defined in this table first, followed
+ * by the constraints defined in ancestor partitioned tables.
+ */
+ printfPQExpBuffer(&buf,
+ "SELECT conrelid = '%s'::pg_catalog.regclass AS sametable,\n"
+ " conname,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef,\n"
+ " conrelid::pg_catalog.regclass AS ontable\n"
+ " FROM pg_catalog.pg_constraint,\n"
+ " pg_catalog.pg_partition_ancestors('%s')\n"
+ " WHERE conrelid = relid AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
+ "ORDER BY sametable DESC, conname;",
+ oid, oid);
+ }
+ else
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT true as sametable, conname,\n"
+ " pg_catalog.pg_get_constraintdef(r.oid, true) as condef,\n"
+ " conrelid::pg_catalog.regclass AS ontable\n"
+ "FROM pg_catalog.pg_constraint r\n"
+ "WHERE r.conrelid = '%s' AND r.contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n",
+ oid);
+
+ if (pset.sversion >= 120000)
+ appendPQExpBufferStr(&buf, " AND conparentid = 0\n");
+ appendPQExpBufferStr(&buf, "ORDER BY conname");
+ }
+
+ result = PSQLexec(buf.data);
+ if (!result)
+ goto error_return;
+ else
+ tuples = PQntuples(result);
+
+ if (tuples > 0)
+ {
+ int i_sametable = PQfnumber(result, "sametable"),
+ i_conname = PQfnumber(result, "conname"),
+ i_condef = PQfnumber(result, "condef"),
+ i_ontable = PQfnumber(result, "ontable");
+
+ printTableAddFooter(&cont, _("Foreign-key constraints:"));
+ for (i = 0; i < tuples; i++)
{
/*
- * Put the constraints defined in this table first, followed
- * by the constraints defined in ancestor partitioned tables.
+ * Print untranslated constraint name and definition. Use
+ * a "TABLE tab" prefix when the constraint is defined in
+ * a parent partitioned table.
*/
- printfPQExpBuffer(&buf,
- "SELECT conrelid = '%s'::pg_catalog.regclass AS sametable,\n"
- " conname,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef,\n"
- " conrelid::pg_catalog.regclass AS ontable\n"
- " FROM pg_catalog.pg_constraint,\n"
- " pg_catalog.pg_partition_ancestors('%s')\n"
- " WHERE conrelid = relid AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
- "ORDER BY sametable DESC, conname;",
- oid, oid);
- }
- else
- {
- printfPQExpBuffer(&buf,
- "SELECT true as sametable, conname,\n"
- " pg_catalog.pg_get_constraintdef(r.oid, true) as condef,\n"
- " conrelid::pg_catalog.regclass AS ontable\n"
- "FROM pg_catalog.pg_constraint r\n"
- "WHERE r.conrelid = '%s' AND r.contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n",
- oid);
-
- if (pset.sversion >= 120000)
- appendPQExpBufferStr(&buf, " AND conparentid = 0\n");
- appendPQExpBufferStr(&buf, "ORDER BY conname");
- }
-
- result = PSQLexec(buf.data);
- if (!result)
- goto error_return;
- else
- tuples = PQntuples(result);
-
- if (tuples > 0)
- {
- int i_sametable = PQfnumber(result, "sametable"),
- i_conname = PQfnumber(result, "conname"),
- i_condef = PQfnumber(result, "condef"),
- i_ontable = PQfnumber(result, "ontable");
-
- printTableAddFooter(&cont, _("Foreign-key constraints:"));
- for (i = 0; i < tuples; i++)
- {
- /*
- * Print untranslated constraint name and definition. Use
- * a "TABLE tab" prefix when the constraint is defined in
- * a parent partitioned table.
- */
- if (strcmp(PQgetvalue(result, i, i_sametable), "f") == 0)
- printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
- PQgetvalue(result, i, i_ontable),
- PQgetvalue(result, i, i_conname),
- PQgetvalue(result, i, i_condef));
- else
- printfPQExpBuffer(&buf, " \"%s\" %s",
- PQgetvalue(result, i, i_conname),
- PQgetvalue(result, i, i_condef));
-
- printTableAddFooter(&cont, buf.data);
- }
- }
- PQclear(result);
- }
-
- /* print incoming foreign-key references */
- if (tableinfo.hastriggers ||
- tableinfo.relkind == RELKIND_PARTITIONED_TABLE)
- {
- if (pset.sversion >= 120000)
- {
- printfPQExpBuffer(&buf,
- "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
- " FROM pg_catalog.pg_constraint c\n"
- " WHERE confrelid IN (SELECT pg_catalog.pg_partition_ancestors('%s')\n"
- " UNION ALL VALUES ('%s'::pg_catalog.regclass))\n"
- " AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
- "ORDER BY conname;",
- oid, oid);
- }
- else
- {
- printfPQExpBuffer(&buf,
- "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
- " FROM pg_catalog.pg_constraint\n"
- " WHERE confrelid = %s AND contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n"
- "ORDER BY conname;",
- oid);
- }
-
- result = PSQLexec(buf.data);
- if (!result)
- goto error_return;
- else
- tuples = PQntuples(result);
-
- if (tuples > 0)
- {
- int i_conname = PQfnumber(result, "conname"),
- i_ontable = PQfnumber(result, "ontable"),
- i_condef = PQfnumber(result, "condef");
-
- printTableAddFooter(&cont, _("Referenced by:"));
- for (i = 0; i < tuples; i++)
- {
+ if (strcmp(PQgetvalue(result, i, i_sametable), "f") == 0)
printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
PQgetvalue(result, i, i_ontable),
PQgetvalue(result, i, i_conname),
PQgetvalue(result, i, i_condef));
+ else
+ printfPQExpBuffer(&buf, " \"%s\" %s",
+ PQgetvalue(result, i, i_conname),
+ PQgetvalue(result, i, i_condef));
- printTableAddFooter(&cont, buf.data);
- }
+ printTableAddFooter(&cont, buf.data);
}
- PQclear(result);
}
+ PQclear(result);
+
+ /* print incoming foreign-key references */
+ if (pset.sversion >= 120000)
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
+ " FROM pg_catalog.pg_constraint c\n"
+ " WHERE confrelid IN (SELECT pg_catalog.pg_partition_ancestors('%s')\n"
+ " UNION ALL VALUES ('%s'::pg_catalog.regclass))\n"
+ " AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
+ "ORDER BY conname;",
+ oid, oid);
+ }
+ else
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
+ " FROM pg_catalog.pg_constraint\n"
+ " WHERE confrelid = %s AND contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n"
+ "ORDER BY conname;",
+ oid);
+ }
+
+ result = PSQLexec(buf.data);
+ if (!result)
+ goto error_return;
+ else
+ tuples = PQntuples(result);
+
+ if (tuples > 0)
+ {
+ int i_conname = PQfnumber(result, "conname"),
+ i_ontable = PQfnumber(result, "ontable"),
+ i_condef = PQfnumber(result, "condef");
+
+ printTableAddFooter(&cont, _("Referenced by:"));
+ for (i = 0; i < tuples; i++)
+ {
+ printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
+ PQgetvalue(result, i, i_ontable),
+ PQgetvalue(result, i, i_conname),
+ PQgetvalue(result, i, i_condef));
+
+ printTableAddFooter(&cont, buf.data);
+ }
+ }
+ PQclear(result);
/* print any row-level policies */
if (pset.sversion >= 90500)
--
2.43.5
v9-0005-Add-support-for-NOT-ENFORCED-in-foreign-key-const.patchapplication/x-patch; name=v9-0005-Add-support-for-NOT-ENFORCED-in-foreign-key-const.patchDownload
From e8e10fbe72c093cf6991c98ca376a385b154b52b Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Wed, 15 Jan 2025 17:19:02 +0530
Subject: [PATCH v9 5/6] Add support for NOT ENFORCED in foreign key
constraints.
Typically, when a foreign key (FK) constraint is created on a table,
action and check triggers are added to maintain data integrity. With
this patch, if a constraint is marked as NOT ENFORCED, integrity
checks are no longer required, making these triggers unnecessary.
Consequently, when creating a NOT ENFORCED FK constraint, triggers
will not be created, and the constraint will be marked as NOT VALID.
Similarly, if an existing FK constraint is changed to NOT ENFORCED,
the associated triggers will be dropped, and the constraint will also
be marked as NOT VALID. Conversely, if a NOT ENFORCED FK constraint is
changed to ENFORCED, the necessary triggers will be created, and the
constraint will remain in the NOT VALID state.
----
NOTE: In this patch, the tryAttachPartitionForeignKey() function will
not merge the constraint if the enforcibility differs. This will be
addressed in the next patch.
----
---
doc/src/sgml/advanced.sgml | 13 +
doc/src/sgml/catalogs.sgml | 2 +-
doc/src/sgml/ddl.sgml | 12 +-
doc/src/sgml/ref/alter_table.sgml | 12 +-
doc/src/sgml/ref/create_table.sgml | 2 +-
src/backend/catalog/pg_constraint.c | 5 +-
src/backend/catalog/sql_features.txt | 2 +-
src/backend/commands/tablecmds.c | 398 ++++++++++++++++------
src/backend/parser/gram.y | 7 +-
src/backend/parser/parse_utilcmd.c | 14 +-
src/backend/utils/cache/relcache.c | 1 +
src/include/utils/rel.h | 3 +
src/test/regress/expected/constraints.out | 8 +-
src/test/regress/expected/foreign_key.out | 130 ++++++-
src/test/regress/sql/foreign_key.sql | 93 ++++-
15 files changed, 568 insertions(+), 134 deletions(-)
diff --git a/doc/src/sgml/advanced.sgml b/doc/src/sgml/advanced.sgml
index 755c9f14850..b7d90de69d7 100644
--- a/doc/src/sgml/advanced.sgml
+++ b/doc/src/sgml/advanced.sgml
@@ -125,6 +125,19 @@ ERROR: insert or update on table "weather" violates foreign key constraint "wea
DETAIL: Key (city)=(Berkeley) is not present in table "cities".
</screen>
</para>
+ <para>
+ If, for some reason, you do not want to enforce this constraint and wish to
+ avoid the error, you can modify the foreign key constraint to <literal>NOT ENFORCED</literal>,
+ or specify it in the <link linkend="sql-createtable"><command>CREATE TABLE</command></link>.
+ </para>
+ <para>
+ Let's change the constraint to <literal>NOT ENFORCED</literal>, and when you
+ attempt to reinsert the same record, it will not result in any error.
+<programlisting>
+ALTER TABLE weather ALTER CONSTRAINT weather_city_fkey NOT ENFORCED;
+INSERT INTO weather VALUES ('Berkeley', 45, 53, 0.0, '1994-11-28');
+</programlisting>
+ </para>
<para>
The behavior of foreign keys can be finely tuned to your
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index d3036c5ba9d..72b68cb8d2d 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2597,7 +2597,7 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l
</para>
<para>
Is the constraint enforced?
- Currently, can be false only for CHECK constraints
+ Currently, can be false only for foreign keys and CHECK constraints
</para></entry>
</row>
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index dea04d64db6..d3377503981 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -1054,11 +1054,13 @@ CREATE TABLE example (
</indexterm>
<para>
- A foreign key constraint specifies that the values in a column (or
- a group of columns) must match the values appearing in some row
- of another table.
- We say this maintains the <firstterm>referential
- integrity</firstterm> between two related tables.
+ A foreign key constraint with <literal>ENFORCED</literal>, the default
+ setting, specifies that the values in a column (or a group of columns)
+ must match the values appearing in some row of another table. We say this
+ maintains the <firstterm>referential integrity</firstterm> between two
+ related tables. If a foreign key constraint is created with <literal>NOT ENFORCED</literal>,
+ the check for aforementioned <firstterm>referential integrity</firstterm>
+ will not be performed.
</para>
<para>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index f9576da435e..8d4d0dec9d2 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -58,7 +58,7 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable>
ALTER [ COLUMN ] <replaceable class="parameter">column_name</replaceable> SET COMPRESSION <replaceable class="parameter">compression_method</replaceable>
ADD <replaceable class="parameter">table_constraint</replaceable> [ NOT VALID ]
ADD <replaceable class="parameter">table_constraint_using_index</replaceable>
- ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+ ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]
VALIDATE CONSTRAINT <replaceable class="parameter">constraint_name</replaceable>
DROP CONSTRAINT [ IF EXISTS ] <replaceable class="parameter">constraint_name</replaceable> [ RESTRICT | CASCADE ]
DISABLE TRIGGER [ <replaceable class="parameter">trigger_name</replaceable> | ALL | USER ]
@@ -553,6 +553,13 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
This form alters the attributes of a constraint that was previously
created. Currently only foreign key constraints may be altered.
</para>
+ <para>
+ Note that when a constraint is changed to <literal>NOT ENFORCED</literal>,
+ it is also marked as <literal>NOT VALID</literal>. While this might seem
+ unnecessary, it is done for consistency and should be set accordingly.
+ This ensures that if the constraint is later changed to <literal>ENFORCED</literal>,
+ it will automatically be in the correct <literal>NOT VALID</literal> state.
+ </para>
</listitem>
</varlistentry>
@@ -563,7 +570,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
This form validates a foreign key or check constraint that was
previously created as <literal>NOT VALID</literal>, by scanning the
table to ensure there are no rows for which the constraint is not
- satisfied. Nothing happens if the constraint is already marked valid.
+ satisfied. If the constraint is not enforced, an error is thrown.
+ Nothing happens if the constraint is already marked valid.
(See <xref linkend="sql-altertable-notes"/> below for an explanation
of the usefulness of this command.)
</para>
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index 2237321cb4f..5071dfeabf9 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -1401,7 +1401,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</para>
<para>
- This is currently only supported for <literal>CHECK</literal>
+ This is currently only supported for foreign key and <literal>CHECK</literal>
constraints.
</para>
</listitem>
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index bbf4742e18c..1d7b29de40d 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -100,8 +100,9 @@ CreateConstraintEntry(const char *constraintName,
ObjectAddresses *addrs_auto;
ObjectAddresses *addrs_normal;
- /* Only CHECK constraint can be not enforced */
- Assert(isEnforced || constraintType == CONSTRAINT_CHECK);
+ /* Only CHECK or FOREIGN KEY constraint can be not enforced */
+ Assert(isEnforced || constraintType == CONSTRAINT_CHECK ||
+ constraintType == CONSTRAINT_FOREIGN);
/* NOT ENFORCED constraint must be NOT VALID */
Assert(isEnforced || !isValidated);
diff --git a/src/backend/catalog/sql_features.txt b/src/backend/catalog/sql_features.txt
index 2f250d2c57b..d49471bbdf2 100644
--- a/src/backend/catalog/sql_features.txt
+++ b/src/backend/catalog/sql_features.txt
@@ -281,7 +281,7 @@ F461 Named character sets NO
F471 Scalar subquery values YES
F481 Expanded NULL predicate YES
F491 Constraint management YES
-F492 Optional table constraint enforcement NO check constraints only
+F492 Optional table constraint enforcement NO foreign key and check constraints only
F501 Features and conformance views YES
F501 Features and conformance views 01 SQL_FEATURES view YES
F501 Features and conformance views 02 SQL_SIZING view YES
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index e4bde88d432..61ff9975af4 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -391,11 +391,23 @@ static void AlterSeqNamespaces(Relation classRel, Relation rel,
LOCKMODE lockmode);
static ObjectAddress ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd,
bool recurse, bool recursing, LOCKMODE lockmode);
-static bool ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
- const Oid fkrelid, const Oid pkrelid,
- HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode);
-static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
+static bool ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel,
+ Relation tgrel, const Oid fkrelid,
+ const Oid pkrelid, HeapTuple contuple,
+ List **otherrelids, LOCKMODE lockmode,
+ Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger);
+static void ATExecAlterConstrEnforceability(Constraint *cmdcon, Relation conrel,
+ Relation tgrel, const Oid fkrelid,
+ const Oid pkrelid, HeapTuple contuple,
+ List **otherrelids, LOCKMODE lockmode,
+ Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger);
+static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Oid fkrel,
bool deferrable, bool initdeferred,
List **otherrelids);
static void ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
@@ -10455,7 +10467,7 @@ addFkConstraint(addFkConstraintSides fkside,
CONSTRAINT_FOREIGN,
fkconstraint->deferrable,
fkconstraint->initdeferred,
- true, /* Is Enforced */
+ fkconstraint->is_enforced,
fkconstraint->initially_valid,
parentConstr,
RelationGetRelid(rel),
@@ -10573,20 +10585,22 @@ addFkRecurseReferenced(Constraint *fkconstraint, Relation rel,
Oid parentDelTrigger, Oid parentUpdTrigger,
bool with_period)
{
- Oid deleteTriggerOid,
- updateTriggerOid;
+ Oid deleteTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
Assert(CheckRelationLockedByMe(pkrel, ShareRowExclusiveLock, true));
Assert(CheckRelationLockedByMe(rel, ShareRowExclusiveLock, true));
/*
- * Create the action triggers that enforce the constraint.
+ * Create action triggers to enforce the constraint, or skip them if the
+ * constraint is not enforced.
*/
- createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
- fkconstraint,
- parentConstr, indexOid,
- parentDelTrigger, parentUpdTrigger,
- &deleteTriggerOid, &updateTriggerOid);
+ if (fkconstraint->is_enforced)
+ createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
+ fkconstraint,
+ parentConstr, indexOid,
+ parentDelTrigger, parentUpdTrigger,
+ &deleteTriggerOid, &updateTriggerOid);
/*
* If the referenced table is partitioned, recurse on ourselves to handle
@@ -10707,8 +10721,8 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
Oid parentInsTrigger, Oid parentUpdTrigger,
bool with_period)
{
- Oid insertTriggerOid,
- updateTriggerOid;
+ Oid insertTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
Assert(OidIsValid(parentConstr));
Assert(CheckRelationLockedByMe(rel, ShareRowExclusiveLock, true));
@@ -10720,29 +10734,32 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
errmsg("foreign key constraints are not supported on foreign tables")));
/*
- * Add the check triggers to it and, if necessary, schedule it to be
- * checked in Phase 3.
+ * Add check triggers if the constraint is enforced, and if needed,
+ * schedule them to be checked in Phase 3.
*
* If the relation is partitioned, drill down to do it to its partitions.
*/
- createForeignKeyCheckTriggers(RelationGetRelid(rel),
- RelationGetRelid(pkrel),
- fkconstraint,
- parentConstr,
- indexOid,
- parentInsTrigger, parentUpdTrigger,
- &insertTriggerOid, &updateTriggerOid);
+ if (fkconstraint->is_enforced)
+ createForeignKeyCheckTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
+ fkconstraint,
+ parentConstr,
+ indexOid,
+ parentInsTrigger, parentUpdTrigger,
+ &insertTriggerOid, &updateTriggerOid);
if (rel->rd_rel->relkind == RELKIND_RELATION)
{
/*
* Tell Phase 3 to check that the constraint is satisfied by existing
- * rows. We can skip this during table creation, when requested
- * explicitly by specifying NOT VALID in an ADD FOREIGN KEY command,
- * and when we're recreating a constraint following a SET DATA TYPE
- * operation that did not impugn its validity.
+ * rows. We can skip this during table creation, when constraint is
+ * specified as NOT ENFORCED, when requested explicitly by specifying
+ * NOT VALID in an ADD FOREIGN KEY command, and when we're recreating
+ * a constraint following a SET DATA TYPE operation that did not
+ * impugn its validity.
*/
- if (wqueue && !old_check_ok && !fkconstraint->skip_validation)
+ if (wqueue && !old_check_ok && !fkconstraint->skip_validation &&
+ fkconstraint->is_enforced)
{
NewConstraint *newcon;
AlteredTableInfo *tab;
@@ -10977,8 +10994,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
AttrNumber confdelsetcols[INDEX_MAX_KEYS];
Constraint *fkconstraint;
ObjectAddress address;
- Oid deleteTriggerOid,
- updateTriggerOid;
+ Oid deleteTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(constrOid));
if (!HeapTupleIsValid(tuple))
@@ -11038,8 +11055,9 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
fkconstraint->fk_del_set_cols = NIL;
fkconstraint->old_conpfeqop = NIL;
fkconstraint->old_pktable_oid = InvalidOid;
+ fkconstraint->is_enforced = constrForm->conenforced;
fkconstraint->skip_validation = false;
- fkconstraint->initially_valid = true;
+ fkconstraint->initially_valid = constrForm->convalidated;
/* set up colnames that are used to generate the constraint name */
for (int i = 0; i < numfks; i++)
@@ -11067,9 +11085,10 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
* parent OIDs for similar triggers that will be created on the
* partition in addFkRecurseReferenced().
*/
- GetForeignKeyActionTriggers(trigrel, constrOid,
- constrForm->confrelid, constrForm->conrelid,
- &deleteTriggerOid, &updateTriggerOid);
+ if (constrForm->conenforced)
+ GetForeignKeyActionTriggers(trigrel, constrOid,
+ constrForm->confrelid, constrForm->conrelid,
+ &deleteTriggerOid, &updateTriggerOid);
/* Add this constraint ... */
address = addFkConstraint(addFkReferencedSide,
@@ -11235,17 +11254,18 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
mapped_conkey[i] = attmap->attnums[conkey[i] - 1];
/*
- * Get the "check" triggers belonging to the constraint to pass as
- * parent OIDs for similar triggers that will be created on the
- * partition in addFkRecurseReferencing(). They are also passed to
- * tryAttachPartitionForeignKey() below to simply assign as parents to
- * the partition's existing "check" triggers, that is, if the
- * corresponding constraints is deemed attachable to the parent
- * constraint.
+ * Get the "check" triggers belonging to the constraint, if it is
+ * enforced, to pass as parent OIDs for similar triggers that will be
+ * created on the partition in addFkRecurseReferencing(). They are
+ * also passed to tryAttachPartitionForeignKey() below to simply
+ * assign as parents to the partition's existing "check" triggers,
+ * that is, if the corresponding constraints is deemed attachable to
+ * the parent constraint.
*/
- GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
- constrForm->confrelid, constrForm->conrelid,
- &insertTriggerOid, &updateTriggerOid);
+ if (constrForm->conenforced)
+ GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
+ constrForm->confrelid, constrForm->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
/*
* Before creating a new constraint, see whether any existing FKs are
@@ -11298,6 +11318,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
fkconstraint->fk_del_set_cols = NIL;
fkconstraint->old_conpfeqop = NIL;
fkconstraint->old_pktable_oid = InvalidOid;
+ fkconstraint->is_enforced = constrForm->conenforced;
fkconstraint->skip_validation = false;
fkconstraint->initially_valid = constrForm->convalidated;
for (int i = 0; i < numfks; i++)
@@ -11421,6 +11442,7 @@ tryAttachPartitionForeignKey(List **wqueue,
if (OidIsValid(partConstr->conparentid) ||
partConstr->condeferrable != parentConstr->condeferrable ||
partConstr->condeferred != parentConstr->condeferred ||
+ partConstr->conenforced != parentConstr->conenforced ||
partConstr->confupdtype != parentConstr->confupdtype ||
partConstr->confdeltype != parentConstr->confdeltype ||
partConstr->confmatchtype != parentConstr->confmatchtype)
@@ -11463,6 +11485,7 @@ AttachPartitionForeignKey(List **wqueue,
Form_pg_constraint partConstr;
Oid partConstrFrelid;
Oid partConstrRelid;
+ bool parentConstrIsEnforced;
bool parentConstrIsValid;
bool partConstrIsValid;
Oid insertTriggerOid,
@@ -11474,6 +11497,7 @@ AttachPartitionForeignKey(List **wqueue,
if (!HeapTupleIsValid(parentConstrTup))
elog(ERROR, "cache lookup failed for constraint %u", parentConstrOid);
parentConstr = (Form_pg_constraint) GETSTRUCT(parentConstrTup);
+ parentConstrIsEnforced = parentConstr->conenforced;
parentConstrIsValid = parentConstr->convalidated;
/* Fetch the child constraint tuple */
@@ -11482,7 +11506,7 @@ AttachPartitionForeignKey(List **wqueue,
if (!HeapTupleIsValid(partcontup))
elog(ERROR, "cache lookup failed for constraint %u", partConstrOid);
partConstr = (Form_pg_constraint) GETSTRUCT(partcontup);
- partConstrIsValid = parentConstr->convalidated;
+ partConstrIsValid = partConstr->convalidated;
partConstrFrelid = partConstr->confrelid;
partConstrRelid = partConstr->conrelid;
@@ -11503,17 +11527,21 @@ AttachPartitionForeignKey(List **wqueue,
/*
* Like the constraint, attach partition's "check" triggers to the
- * corresponding parent triggers.
+ * corresponding parent triggers if the constraint is enforced.
+ * Non-enforced constraints do not have these triggers.
*/
- GetForeignKeyCheckTriggers(trigrel,
- partConstrOid, partConstrFrelid, partConstrRelid,
- &insertTriggerOid, &updateTriggerOid);
- Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
- TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
- RelationGetRelid(partition));
- Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
- TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
- RelationGetRelid(partition));
+ if (parentConstrIsEnforced)
+ {
+ GetForeignKeyCheckTriggers(trigrel,
+ partConstrOid, partConstrFrelid, partConstrRelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
+ RelationGetRelid(partition));
+ Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
+ RelationGetRelid(partition));
+ }
/*
* If the referenced table is partitioned, then the partition we're
@@ -11641,6 +11669,10 @@ RemoveInheritedConstraint(Relation conrel, Relation trigrel, Oid conrelid,
*
* The subroutine for tryAttachPartitionForeignKey handles the deletion of
* action triggers for the foreign key constraint.
+ *
+ * If valid confrelid and conrelid values are not provided, the respective
+ * trigger check will be skipped, and the trigger will be considered for
+ * removal.
*/
static void
DropForeignKeyConstraintTriggers(Relation trigrel, Oid conoid, Oid confrelid,
@@ -11661,10 +11693,27 @@ DropForeignKeyConstraintTriggers(Relation trigrel, Oid conoid, Oid confrelid,
Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
ObjectAddress trigger;
- if (trgform->tgconstrrelid != conrelid)
+ /* Invalid if trigger is not for a referential integrity constraint */
+ if (!OidIsValid(trgform->tgconstrrelid))
continue;
- if (trgform->tgrelid != confrelid)
+ if (OidIsValid(conrelid) && trgform->tgconstrrelid != conrelid)
continue;
+ if (OidIsValid(confrelid) && trgform->tgrelid != confrelid)
+ continue;
+
+ /* We should be droping trigger related to foreign ke constraint */
+ Assert(trgform->tgfoid == F_RI_FKEY_CHECK_INS ||
+ trgform->tgfoid == F_RI_FKEY_CHECK_UPD ||
+ trgform->tgfoid == F_RI_FKEY_CASCADE_DEL ||
+ trgform->tgfoid == F_RI_FKEY_CASCADE_UPD ||
+ trgform->tgfoid == F_RI_FKEY_RESTRICT_DEL ||
+ trgform->tgfoid == F_RI_FKEY_RESTRICT_UPD ||
+ trgform->tgfoid == F_RI_FKEY_SETNULL_DEL ||
+ trgform->tgfoid == F_RI_FKEY_SETNULL_UPD ||
+ trgform->tgfoid == F_RI_FKEY_SETDEFAULT_DEL ||
+ trgform->tgfoid == F_RI_FKEY_SETDEFAULT_UPD ||
+ trgform->tgfoid == F_RI_FKEY_NOACTION_DEL ||
+ trgform->tgfoid == F_RI_FKEY_NOACTION_UPD);
/*
* The constraint is originally set up to contain this trigger as an
@@ -11924,11 +11973,13 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
address = InvalidObjectAddress;
if (currcon->condeferrable != cmdcon->deferrable ||
currcon->condeferred != cmdcon->initdeferred ||
+ currcon->conenforced != cmdcon->is_enforced ||
rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
if (ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, currcon->conrelid,
currcon->confrelid, contuple, &otherrelids,
- lockmode))
+ lockmode, InvalidOid, InvalidOid, InvalidOid,
+ InvalidOid))
ObjectAddressSet(address, ConstraintRelationId, currcon->oid);
}
@@ -11963,23 +12014,25 @@ static bool
ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
const Oid fkrelid, const Oid pkrelid,
HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode)
+ LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger)
{
Form_pg_constraint currcon;
Oid conoid;
+ Oid conrelid;
Oid refrelid;
bool changed = false;
- Relation rel;
/* since this function recurses, it could be driven to stack overflow */
check_stack_depth();
currcon = (Form_pg_constraint) GETSTRUCT(contuple);
conoid = currcon->oid;
+ conrelid = currcon->conrelid;
refrelid = currcon->confrelid;
- rel = table_open(currcon->conrelid, lockmode);
-
/*
* Update pg_constraint with the flags from cmdcon.
*
@@ -11987,7 +12040,8 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
* silently do nothing.
*/
if (currcon->condeferrable != cmdcon->deferrable ||
- currcon->condeferred != cmdcon->initdeferred)
+ currcon->condeferred != cmdcon->initdeferred ||
+ currcon->conenforced != cmdcon->is_enforced)
{
HeapTuple copyTuple;
Form_pg_constraint copy_con;
@@ -11996,6 +12050,18 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
copy_con->condeferrable = cmdcon->deferrable;
copy_con->condeferred = cmdcon->initdeferred;
+ copy_con->conenforced = cmdcon->is_enforced;
+
+ /*
+ * NB: The convalidated status is irrelevant when the constraint is
+ * set to NOT ENFORCED, but for consistency, it should be set
+ * accordingly. This ensures that if the constraint is later changed
+ * to ENFORCED, it will automatically be in the correct NOT VALID
+ * state.
+ */
+ if (!cmdcon->is_enforced)
+ copy_con->convalidated = false;
+
CatalogTupleUpdate(conrel, ©Tuple->t_self, copyTuple);
InvokeObjectPostAlterHook(ConstraintRelationId,
@@ -12005,33 +12071,161 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
changed = true;
/* Make new constraint flags visible to others */
- CacheInvalidateRelcache(rel);
+ CacheInvalidateRelcacheByRelid(conrelid);
+ }
+ if (currcon->conenforced != cmdcon->is_enforced)
+ {
+ ATExecAlterConstrEnforceability(cmdcon, conrel, tgrel, fkrelid, pkrelid,
+ contuple, otherrelids, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
+ }
+ else
+ {
/*
* Now we need to update the multiple entries in pg_trigger that
* implement the constraint.
*/
- AlterConstrTriggerDeferrability(conoid, tgrel, rel, cmdcon->deferrable,
- cmdcon->initdeferred, otherrelids);
- }
+ AlterConstrTriggerDeferrability(conoid, tgrel, conrelid,
+ cmdcon->deferrable,
+ cmdcon->initdeferred,
+ otherrelids);
- /*
- * If the table at either end of the constraint is partitioned, we need to
- * recurse and handle every constraint that is a child of this one.
- *
- * (This assumes that the recurse flag is forcibly set for partitioned
- * tables, and not set for legacy inheritance, though we don't check for
- * that here.)
- */
- if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
- get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)
- ATExecAlterChildConstr(cmdcon, conrel, tgrel, fkrelid, pkrelid,
- contuple, otherrelids, lockmode);
- table_close(rel, NoLock);
+ /*
+ * If the table at either end of the constraint is partitioned, we
+ * need to recurse and handle every constraint that is a child of this
+ * one.
+ *
+ * (This assumes that the recurse flag is forcibly set for partitioned
+ * tables, and not set for legacy inheritance, though we don't check
+ * for that here.)
+ */
+ if (get_rel_relkind(conrelid) == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)
+ ATExecAlterChildConstr(cmdcon, conrel, tgrel, fkrelid, pkrelid,
+ contuple, otherrelids, lockmode);
+ }
return changed;
}
+/*
+ * A subroutine of ATExecAlterConstrRecurse that updates the enforceability of
+ * a foreign key constraint. Depending on whether the constraint is being set
+ * to enforced or not enforced, it creates or drops the trigger accordingly.
+ *
+ * The arguments to this function have the same meaning as the arguments to
+ * ATExecAlterConstrRecurse.
+ */
+static void
+ATExecAlterConstrEnforceability(Constraint *cmdcon, Relation conrel,
+ Relation tgrel, const Oid fkrelid,
+ const Oid pkrelid, HeapTuple contuple,
+ List **otherrelids, LOCKMODE lockmode,
+ Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger)
+{
+ Form_pg_constraint currcon;
+ Oid conoid;
+
+ currcon = (Form_pg_constraint) GETSTRUCT(contuple);
+ conoid = currcon->oid;
+
+ /* Drop triggers */
+ if (!cmdcon->is_enforced)
+ {
+ /*
+ * When setting a constraint to NOT ENFORCED, the constraint triggers
+ * need to be dropped. Therefore, we must process the child relations
+ * first, followed by the parent, to account for dependencies.
+ */
+ if (get_rel_relkind(currcon->conrelid) == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE)
+ ATExecAlterChildConstr(cmdcon, conrel, tgrel, fkrelid, pkrelid,
+ contuple, otherrelids, lockmode);
+
+ /* Drop all the triggers */
+ DropForeignKeyConstraintTriggers(tgrel, conoid, InvalidOid, InvalidOid);
+ }
+ else /* Create triggers */
+ {
+ Relation rel;
+ Oid ReferencedDelTriggerOid = InvalidOid,
+ ReferencedUpdTriggerOid = InvalidOid,
+ ReferencingInsTriggerOid = InvalidOid,
+ ReferencingUpdTriggerOid = InvalidOid;
+
+ /* Update the foreign key action required for trigger creation. */
+ cmdcon->fk_matchtype = currcon->confmatchtype;
+ cmdcon->fk_upd_action = currcon->confupdtype;
+ cmdcon->fk_del_action = currcon->confdeltype;
+
+ rel = table_open(currcon->conrelid, lockmode);
+
+ /* Create referenced triggers */
+ if (currcon->conrelid == fkrelid)
+ createForeignKeyActionTriggers(rel,
+ currcon->confrelid,
+ cmdcon,
+ conoid,
+ currcon->conindid,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ &ReferencedDelTriggerOid,
+ &ReferencedUpdTriggerOid);
+
+ /* Create referencing triggers */
+ if (currcon->confrelid == pkrelid)
+ createForeignKeyCheckTriggers(currcon->conrelid,
+ pkrelid,
+ cmdcon,
+ conoid,
+ currcon->conindid,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger,
+ &ReferencingInsTriggerOid,
+ &ReferencingUpdTriggerOid);
+
+ /*
+ * If the table at either end of the constraint is partitioned, we
+ * need to recurse and create triggers for each constraint that is a
+ * child of this one.
+ */
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE)
+ {
+ ScanKeyData pkey;
+ SysScanDesc pscan;
+ HeapTuple childtup;
+
+ ScanKeyInit(&pkey,
+ Anum_pg_constraint_conparentid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+
+ pscan = systable_beginscan(conrel, ConstraintParentIndexId,
+ true, NULL, 1, &pkey);
+
+ while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
+ ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, fkrelid, pkrelid,
+ childtup, otherrelids, lockmode,
+ ReferencedDelTriggerOid,
+ ReferencedUpdTriggerOid,
+ ReferencingInsTriggerOid,
+ ReferencingUpdTriggerOid);
+
+ systable_endscan(pscan);
+ }
+
+ table_close(rel, NoLock);
+ }
+}
+
/*
* A subroutine of ATExecAlterConstrRecurse that updated constraint trigger's
* deferrability.
@@ -12040,7 +12234,7 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
* ATExecAlterConstrRecurse.
*/
static void
-AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
+AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Oid fkrel,
bool deferrable, bool initdeferred,
List **otherrelids)
{
@@ -12066,7 +12260,7 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
* other rels that don't have a trigger whose properties change, but
* let's be conservative.)
*/
- if (tgform->tgrelid != RelationGetRelid(rel))
+ if (tgform->tgrelid != fkrel)
*otherrelids = list_append_unique_oid(*otherrelids,
tgform->tgrelid);
@@ -12129,7 +12323,8 @@ ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, fkrelid, pkrelid,
- childtup, otherrelids, lockmode);
+ childtup, otherrelids, lockmode, InvalidOid,
+ InvalidOid, InvalidOid, InvalidOid);
systable_endscan(pscan);
}
@@ -20154,8 +20349,6 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
Form_pg_constraint conform;
- Oid insertTriggerOid,
- updateTriggerOid;
contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
if (!HeapTupleIsValid(contup))
@@ -20178,17 +20371,25 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
/*
* Also, look up the partition's "check" triggers corresponding to the
- * constraint being detached and detach them from the parent triggers.
+ * enforced constraint being detached and detach them from the parent
+ * triggers. Non-enforced constraints do not have these triggers;
+ * therefore, this step is not needed.
*/
- GetForeignKeyCheckTriggers(trigrel,
- fk->conoid, fk->confrelid, fk->conrelid,
- &insertTriggerOid, &updateTriggerOid);
- Assert(OidIsValid(insertTriggerOid));
- TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
- RelationGetRelid(partRel));
- Assert(OidIsValid(updateTriggerOid));
- TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
- RelationGetRelid(partRel));
+ if (fk->conenforced)
+ {
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ Assert(OidIsValid(updateTriggerOid));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ }
/*
* Lastly, create the action triggers on the referenced table, using
@@ -20228,8 +20429,9 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
fkconstraint->conname = pstrdup(NameStr(conform->conname));
fkconstraint->deferrable = conform->condeferrable;
fkconstraint->initdeferred = conform->condeferred;
+ fkconstraint->is_enforced = conform->conenforced;
fkconstraint->skip_validation = true;
- fkconstraint->initially_valid = true;
+ fkconstraint->initially_valid = conform->convalidated;
/* a few irrelevant fields omitted here */
fkconstraint->pktable = NULL;
fkconstraint->fk_attrs = NIL;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index d7f9c00c409..37082bc511e 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -2666,7 +2666,7 @@ alter_table_cmd:
processCASbits($4, @4, "ALTER CONSTRAINT statement",
&c->deferrable,
&c->initdeferred,
- NULL, NULL, NULL, yyscanner);
+ &c->is_enforced, NULL, NULL, yyscanner);
$$ = (Node *) n;
}
/* ALTER TABLE <name> VALIDATE CONSTRAINT ... */
@@ -4035,6 +4035,7 @@ ColConstraintElem:
n->is_enforced = true;
n->skip_validation = false;
n->initially_valid = true;
+ n->is_enforced = true;
$$ = (Node *) n;
}
;
@@ -4309,8 +4310,8 @@ ConstraintElem:
n->fk_del_set_cols = ($11)->deleteAction->cols;
processCASbits($12, @12, "FOREIGN KEY",
&n->deferrable, &n->initdeferred,
- NULL, &n->skip_validation, NULL,
- yyscanner);
+ &n->is_enforced, &n->skip_validation,
+ NULL, yyscanner);
n->initially_valid = !n->skip_validation;
$$ = (Node *) n;
}
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index ca028d2a66d..170f8032d59 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -2961,8 +2961,10 @@ transformFKConstraints(CreateStmtContext *cxt,
/*
* If CREATE TABLE or adding a column with NULL default, we can safely
- * skip validation of FK constraints, and nonetheless mark them valid.
- * (This will override any user-supplied NOT VALID flag.)
+ * skip validation of FK constraints, and mark them as valid based on the
+ * constraint enforcement flag, since NOT ENFORCED constraints must always
+ * be marked as NOT VALID. (This will override any user-supplied NOT VALID
+ * flag.)
*/
if (skipValidation)
{
@@ -2971,7 +2973,7 @@ transformFKConstraints(CreateStmtContext *cxt,
Constraint *constraint = (Constraint *) lfirst(fkclist);
constraint->skip_validation = true;
- constraint->initially_valid = true;
+ constraint->initially_valid = constraint->is_enforced;
}
}
@@ -3966,7 +3968,8 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
case CONSTR_ATTR_ENFORCED:
if (lastprimarycon == NULL ||
- lastprimarycon->contype != CONSTR_CHECK)
+ (lastprimarycon->contype != CONSTR_CHECK &&
+ lastprimarycon->contype != CONSTR_FOREIGN))
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("misplaced ENFORCED clause"),
@@ -3982,7 +3985,8 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
case CONSTR_ATTR_NOT_ENFORCED:
if (lastprimarycon == NULL ||
- lastprimarycon->contype != CONSTR_CHECK)
+ (lastprimarycon->contype != CONSTR_CHECK &&
+ lastprimarycon->contype != CONSTR_FOREIGN))
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("misplaced NOT ENFORCED clause"),
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 485cd2178bc..cb3c19c7448 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4689,6 +4689,7 @@ RelationGetFKeyList(Relation relation)
info->conoid = constraint->oid;
info->conrelid = constraint->conrelid;
info->confrelid = constraint->confrelid;
+ info->conenforced = constraint->conenforced;
DeconstructFkConstraintRow(htup, &info->nkeys,
info->conkey,
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 33d1e4a4e2e..c707ebec7b6 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -284,6 +284,9 @@ typedef struct ForeignKeyCacheInfo
/* number of columns in the foreign key */
int nkeys;
+ /* Is enforced ? */
+ bool conenforced;
+
/*
* these arrays each have nkeys valid entries:
*/
diff --git a/src/test/regress/expected/constraints.out b/src/test/regress/expected/constraints.out
index 692a69fe457..f8767c5cacf 100644
--- a/src/test/regress/expected/constraints.out
+++ b/src/test/regress/expected/constraints.out
@@ -746,13 +746,9 @@ LINE 1: CREATE TABLE UNIQUE_NOTEN_TBL(i int UNIQUE NOT ENFORCED);
^
-- XXX: error message is misleading here
ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
-ERROR: ALTER CONSTRAINT statement constraints cannot be marked ENFORCED
-LINE 1: ...TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
- ^
+ERROR: constraint "unique_tbl_i_key" of relation "unique_tbl" is not a foreign key constraint
ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORCED;
-ERROR: ALTER CONSTRAINT statement constraints cannot be marked NOT ENFORCED
-LINE 1: ...ABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORC...
- ^
+ERROR: constraint "unique_tbl_i_key" of relation "unique_tbl" is not a foreign key constraint
DROP TABLE unique_tbl;
--
-- EXCLUDE constraints
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index f617d519aef..9f6f22dd988 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -1,12 +1,44 @@
--
-- FOREIGN KEY
--
--- MATCH FULL
+-- NOT ENFORCED
--
-- First test, check and cascade
--
CREATE TABLE PKTABLE ( ptest1 int PRIMARY KEY, ptest2 text );
-CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE, ftest2 int );
+CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE NOT ENFORCED,
+ ftest2 int );
+-- Inserting into the foreign key table will not result in an error, even if
+-- there is no matching key in the referenced table.
+INSERT INTO FKTABLE VALUES (1, 2);
+INSERT INTO FKTABLE VALUES (2, 3);
+-- Check FKTABLE
+SELECT * FROM FKTABLE;
+ ftest1 | ftest2
+--------+--------
+ 1 | 2
+ 2 | 3
+(2 rows)
+
+-- Changing it back to ENFORCED will leave the constraint in the NOT VALID state
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+-- Which needs to be explicitly validated.
+ALTER TABLE FKTABLE VALIDATE CONSTRAINT fktable_ftest1_fkey;
+ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_fkey"
+DETAIL: Key (ftest1)=(1) is not present in table "pktable".
+-- Remove any existing rows that violate the constraint, then attempt to change
+-- it.
+TRUNCATE FKTABLE;
+ALTER TABLE FKTABLE VALIDATE CONSTRAINT fktable_ftest1_fkey;
+-- Any further inserts will fail due to the enforcement.
+INSERT INTO FKTABLE VALUES (3, 4);
+ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_fkey"
+DETAIL: Key (ftest1)=(3) is not present in table "pktable".
+--
+-- MATCH FULL
+--
+-- First test, check and cascade
+--
-- Insert test data into PKTABLE
INSERT INTO PKTABLE VALUES (1, 'Test1');
INSERT INTO PKTABLE VALUES (2, 'Test2');
@@ -351,6 +383,8 @@ INSERT INTO FKTABLE VALUES (1, NULL);
ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL;
ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_ftest2_fkey"
DETAIL: MATCH FULL does not allow mixing of null and nonnull key values.
+-- NOT ENFORCED will bypass the initial check.
+ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL NOT ENFORCED;
DROP TABLE FKTABLE;
DROP TABLE PKTABLE;
-- MATCH SIMPLE
@@ -1276,13 +1310,28 @@ INSERT INTO fktable VALUES (0, 20);
ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_fk_fkey"
DETAIL: Key (fk)=(20) is not present in table "pktable".
COMMIT;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT ENFORCED;
+BEGIN;
+-- doesn't match FK, no error.
+UPDATE pktable SET id = 10 WHERE id = 5;
+-- doesn't match PK, no error.
+INSERT INTO fktable VALUES (0, 20);
+ROLLBACK;
-- try additional syntax
-ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE ENFORCED;
-- illegal option
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE INITIALLY DEFERRED;
ERROR: constraint declared INITIALLY DEFERRED must be DEFERRABLE
LINE 1: ...e ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE INITIALLY ...
^
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORCED;
+ERROR: conflicting constraint properties
+LINE 1: ...fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORC...
+ ^
+CREATE TEMP TABLE fktable2 (fk int references pktable ENFORCED NOT ENFORCED);
+ERROR: multiple ENFORCED/NOT ENFORCED clauses not allowed
+LINE 1: ...ABLE fktable2 (fk int references pktable ENFORCED NOT ENFORC...
+ ^
-- test order of firing of FK triggers when several RI-induced changes need to
-- be made to the same row. This was broken by subtransaction-related
-- changes in 8.0.
@@ -1650,10 +1699,14 @@ ALTER TABLE fk_partitioned_fk DROP COLUMN fdrop1;
CREATE TABLE fk_partitioned_fk_1 (fdrop1 int, fdrop2 int, a int, fdrop3 int, b int);
ALTER TABLE fk_partitioned_fk_1 DROP COLUMN fdrop1, DROP COLUMN fdrop2, DROP COLUMN fdrop3;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FROM (0,0) TO (1000,1000);
-ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk;
+ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
+ALTER TABLE fk_partitioned_fk_2 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
+-- Changing to ENFORCED does not alter the validation status; it must be explicitly validated.
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+ALTER TABLE fk_partitioned_fk VALIDATE CONSTRAINT fk_partitioned_fk_a_b_fkey ;
CREATE TABLE fk_partitioned_fk_3 (fdrop1 int, fdrop2 int, fdrop3 int, fdrop4 int, b int, a int)
PARTITION BY HASH (a);
ALTER TABLE fk_partitioned_fk_3 DROP COLUMN fdrop1, DROP COLUMN fdrop2,
@@ -1676,10 +1729,10 @@ INSERT INTO fk_partitioned_fk_1 (a,b) VALUES (500, 501);
ERROR: insert or update on table "fk_partitioned_fk_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
DETAIL: Key (a, b)=(500, 501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (1500, 1501);
-ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_2_a_b_fkey"
DETAIL: Key (a, b)=(1500, 1501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk_2 (a,b) VALUES (1500, 1501);
-ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_2_a_b_fkey"
DETAIL: Key (a, b)=(1500, 1501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (2500, 2502);
ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
@@ -1729,6 +1782,37 @@ Indexes:
Referenced by:
TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b)
+-- Check the exsting FK trigger
+CREATE TEMP TABLE tmp_trg_info AS SELECT tgtype, tgrelid::regclass, tgconstraint
+FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+SELECT count(1) FROM tmp_trg_info;
+ count
+-------
+ 14
+(1 row)
+
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+-- No triggers
+SELECT count(1) FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+ count
+-------
+ 0
+(1 row)
+
+-- Changing it back to ENFORCED will recreate the necessary triggers.
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+-- Should be exactly the same number of triggers found as before
+SELECT COUNT(1) FROM pg_trigger pgt JOIN tmp_trg_info tt
+ON (pgt.tgtype = tt.tgtype AND pgt.tgrelid = tt.tgrelid AND pgt.tgconstraint = tt.tgconstraint);
+ count
+-------
+ 14
+(1 row)
+
ALTER TABLE fk_partitioned_fk DROP CONSTRAINT fk_partitioned_fk_a_b_fkey;
-- done.
DROP TABLE fk_notpartitioned_pk, fk_partitioned_fk;
@@ -1932,6 +2016,40 @@ Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
Foreign-key constraints:
TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
+DROP TABLE fk_partitioned_fk_2;
+CREATE TABLE fk_partitioned_fk_2 (b int, a int,
+ FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
+BEGIN;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have two constraints
+\d fk_partitioned_fk_2
+ Table "public.fk_partitioned_fk_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ b | integer | | |
+ a | integer | | |
+Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
+Foreign-key constraints:
+ "fk_partitioned_fk_2_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED
+ TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
+
+DROP TABLE fk_partitioned_fk_2;
+ROLLBACK;
+BEGIN;
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have only one constraint
+\d fk_partitioned_fk_2
+ Table "public.fk_partitioned_fk_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ b | integer | | |
+ a | integer | | |
+Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
+Foreign-key constraints:
+ TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED
+
+ROLLBACK;
DROP TABLE fk_partitioned_fk_2;
CREATE TABLE fk_partitioned_fk_4 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE) PARTITION BY RANGE (b, a);
CREATE TABLE fk_partitioned_fk_4_1 PARTITION OF fk_partitioned_fk_4 FOR VALUES FROM (1,1) TO (100,100);
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index d5f68812ba6..98f47873d41 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -2,13 +2,40 @@
-- FOREIGN KEY
--
--- MATCH FULL
+-- NOT ENFORCED
--
-- First test, check and cascade
--
CREATE TABLE PKTABLE ( ptest1 int PRIMARY KEY, ptest2 text );
-CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE, ftest2 int );
+CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE NOT ENFORCED,
+ ftest2 int );
+-- Inserting into the foreign key table will not result in an error, even if
+-- there is no matching key in the referenced table.
+INSERT INTO FKTABLE VALUES (1, 2);
+INSERT INTO FKTABLE VALUES (2, 3);
+
+-- Check FKTABLE
+SELECT * FROM FKTABLE;
+
+-- Changing it back to ENFORCED will leave the constraint in the NOT VALID state
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+-- Which needs to be explicitly validated.
+ALTER TABLE FKTABLE VALIDATE CONSTRAINT fktable_ftest1_fkey;
+
+-- Remove any existing rows that violate the constraint, then attempt to change
+-- it.
+TRUNCATE FKTABLE;
+ALTER TABLE FKTABLE VALIDATE CONSTRAINT fktable_ftest1_fkey;
+
+-- Any further inserts will fail due to the enforcement.
+INSERT INTO FKTABLE VALUES (3, 4);
+
+--
+-- MATCH FULL
+--
+-- First test, check and cascade
+--
-- Insert test data into PKTABLE
INSERT INTO PKTABLE VALUES (1, 'Test1');
INSERT INTO PKTABLE VALUES (2, 'Test2');
@@ -230,6 +257,9 @@ INSERT INTO FKTABLE VALUES (1, NULL);
ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL;
+-- NOT ENFORCED will bypass the initial check.
+ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL NOT ENFORCED;
+
DROP TABLE FKTABLE;
DROP TABLE PKTABLE;
@@ -968,10 +998,23 @@ INSERT INTO fktable VALUES (0, 20);
COMMIT;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT ENFORCED;
+
+BEGIN;
+
+-- doesn't match FK, no error.
+UPDATE pktable SET id = 10 WHERE id = 5;
+-- doesn't match PK, no error.
+INSERT INTO fktable VALUES (0, 20);
+
+ROLLBACK;
+
-- try additional syntax
-ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE ENFORCED;
-- illegal option
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORCED;
+CREATE TEMP TABLE fktable2 (fk int references pktable ENFORCED NOT ENFORCED);
-- test order of firing of FK triggers when several RI-induced changes need to
-- be made to the same row. This was broken by subtransaction-related
@@ -1236,11 +1279,16 @@ ALTER TABLE fk_partitioned_fk DROP COLUMN fdrop1;
CREATE TABLE fk_partitioned_fk_1 (fdrop1 int, fdrop2 int, a int, fdrop3 int, b int);
ALTER TABLE fk_partitioned_fk_1 DROP COLUMN fdrop1, DROP COLUMN fdrop2, DROP COLUMN fdrop3;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FROM (0,0) TO (1000,1000);
-ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk;
+ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
+ALTER TABLE fk_partitioned_fk_2 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
+-- Changing to ENFORCED does not alter the validation status; it must be explicitly validated.
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+ALTER TABLE fk_partitioned_fk VALIDATE CONSTRAINT fk_partitioned_fk_a_b_fkey ;
+
CREATE TABLE fk_partitioned_fk_3 (fdrop1 int, fdrop2 int, fdrop3 int, fdrop4 int, b int, a int)
PARTITION BY HASH (a);
ALTER TABLE fk_partitioned_fk_3 DROP COLUMN fdrop1, DROP COLUMN fdrop2,
@@ -1286,6 +1334,27 @@ UPDATE fk_notpartitioned_pk SET b = 1502 WHERE a = 1500;
UPDATE fk_notpartitioned_pk SET b = 2504 WHERE a = 2500;
-- check psql behavior
\d fk_notpartitioned_pk
+
+-- Check the exsting FK trigger
+CREATE TEMP TABLE tmp_trg_info AS SELECT tgtype, tgrelid::regclass, tgconstraint
+FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+SELECT count(1) FROM tmp_trg_info;
+
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+-- No triggers
+SELECT count(1) FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+
+-- Changing it back to ENFORCED will recreate the necessary triggers.
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+
+-- Should be exactly the same number of triggers found as before
+SELECT COUNT(1) FROM pg_trigger pgt JOIN tmp_trg_info tt
+ON (pgt.tgtype = tt.tgtype AND pgt.tgrelid = tt.tgrelid AND pgt.tgconstraint = tt.tgconstraint);
+
ALTER TABLE fk_partitioned_fk DROP CONSTRAINT fk_partitioned_fk_a_b_fkey;
-- done.
DROP TABLE fk_notpartitioned_pk, fk_partitioned_fk;
@@ -1422,6 +1491,22 @@ ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN
\d fk_partitioned_fk_2
DROP TABLE fk_partitioned_fk_2;
+CREATE TABLE fk_partitioned_fk_2 (b int, a int,
+ FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
+BEGIN;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have two constraints
+\d fk_partitioned_fk_2
+DROP TABLE fk_partitioned_fk_2;
+ROLLBACK;
+BEGIN;
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have only one constraint
+\d fk_partitioned_fk_2
+ROLLBACK;
+DROP TABLE fk_partitioned_fk_2;
+
CREATE TABLE fk_partitioned_fk_4 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE) PARTITION BY RANGE (b, a);
CREATE TABLE fk_partitioned_fk_4_1 PARTITION OF fk_partitioned_fk_4 FOR VALUES FROM (1,1) TO (100,100);
CREATE TABLE fk_partitioned_fk_4_2 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE SET NULL);
--
2.43.5
v9-0006-Merge-the-parent-and-child-constraints-with-diffe.patchapplication/x-patch; name=v9-0006-Merge-the-parent-and-child-constraints-with-diffe.patchDownload
From aeb349371be4e09b1171be1732d61a8f757fcac2 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Fri, 17 Jan 2025 17:03:59 +0530
Subject: [PATCH v9 6/6] Merge the parent and child constraints with differing
enforcibility.
If an enforced parent constraint is attached to a non-enforced child
constraint, the child constraint will be made enforced, with
validation applied if the parent constraint is validated as well.
Otherwise, a new enforced constraint (with validation, if the parent
constraint is validated) would need to be created on the child table,
which would be unnecessary if a similar constraint already exists and
can be attached.
On the other hand, having a not-enforced parent constraint with an
enforced child constraint does not cause any issues, and no changes
are required.
----
NOTE: This patch is intended to reduce the diff noise from the main
patch and is not meant to be committed separately. It should be
squashed with the main patch that adds ENFORCED/NOT ENFORCED.
----
---
src/backend/commands/tablecmds.c | 150 ++++++++++++++++++++--
src/test/regress/expected/foreign_key.out | 36 +++++-
src/test/regress/sql/foreign_key.sql | 14 ++
3 files changed, 184 insertions(+), 16 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 61ff9975af4..7a2addb6f2b 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -11442,7 +11442,6 @@ tryAttachPartitionForeignKey(List **wqueue,
if (OidIsValid(partConstr->conparentid) ||
partConstr->condeferrable != parentConstr->condeferrable ||
partConstr->condeferred != parentConstr->condeferred ||
- partConstr->conenforced != parentConstr->conenforced ||
partConstr->confupdtype != parentConstr->confupdtype ||
partConstr->confdeltype != parentConstr->confdeltype ||
partConstr->confmatchtype != parentConstr->confmatchtype)
@@ -11487,7 +11486,9 @@ AttachPartitionForeignKey(List **wqueue,
Oid partConstrRelid;
bool parentConstrIsEnforced;
bool parentConstrIsValid;
+ bool partConstrIsEnforced;
bool partConstrIsValid;
+ bool partConstrParentIsSet;
Oid insertTriggerOid,
updateTriggerOid;
@@ -11506,10 +11507,80 @@ AttachPartitionForeignKey(List **wqueue,
if (!HeapTupleIsValid(partcontup))
elog(ERROR, "cache lookup failed for constraint %u", partConstrOid);
partConstr = (Form_pg_constraint) GETSTRUCT(partcontup);
+ partConstrIsEnforced = partConstr->conenforced;
partConstrIsValid = partConstr->convalidated;
partConstrFrelid = partConstr->confrelid;
partConstrRelid = partConstr->conrelid;
+ /*
+ * When the enforcibility of both constraints is the same, no special
+ * action is taken, and the flow remains unchanged. However, there are two
+ * scenarios to consider when it differs:
+ *
+ * 1. Parent constraint is not-enforced and child constraint is enforced:
+ *
+ * This is allowed because the not-enforced parent constraint does not
+ * have triggers, so no redundancy issues arise with the child constraint,
+ * even if it is enforced. In this case, we allow the child constraint to
+ * remain enforced and keep its trigger intact. This ensures that
+ * referential integrity checks for the child continue as before, even
+ * with the parent constraint not enforced. The relationship between the
+ * two constraints is maintained by setting the parent constraint, which
+ * enables us to locate the child constraint. This is important if the
+ * parent constraint is later changed to enforced, at which point the
+ * necessary trigger will be created for the parent. Any potential
+ * redundancy from these triggers will be addressed accordingly.
+ *
+ * 2. Parent constraint is enforced and child constraint is not-enforced:
+ *
+ * This is not allowed, as it would violate referential integrity. In such
+ * cases, the child constraint will first be made enforced before merging
+ * it with the enforced parent constraint. Then, action triggers and
+ * constraint redundancy will be handled in the usual manner, similar to
+ * how two enforced constraints are merged.
+ */
+ if (!parentConstrIsEnforced && partConstrIsEnforced)
+ {
+ ReleaseSysCache(partcontup);
+ ReleaseSysCache(parentConstrTup);
+
+ ConstraintSetParentConstraint(partConstrOid, parentConstrOid,
+ RelationGetRelid(partition));
+ CommandCounterIncrement();
+
+ return;
+ }
+ else if (parentConstrIsEnforced && !partConstrIsEnforced)
+ {
+ Constraint *cmdcon = makeNode(Constraint);
+ Relation conrel;
+ List *otherrelids = NIL;
+
+ cmdcon->contype = partConstr->contype;
+ cmdcon->conname = NameStr(partConstr->conname);
+ cmdcon->deferrable = partConstr->condeferrable;
+ cmdcon->initdeferred = partConstr->condeferred;
+ cmdcon->is_enforced = true;
+
+ conrel = table_open(ConstraintRelationId, RowExclusiveLock);
+
+ ATExecAlterConstrRecurse(cmdcon, conrel, trigrel, partConstr->conrelid,
+ partConstr->confrelid, partcontup, &otherrelids,
+ AccessExclusiveLock, InvalidOid, InvalidOid,
+ InvalidOid, InvalidOid);
+
+ table_close(conrel, RowExclusiveLock);
+
+ CommandCounterIncrement();
+ }
+
+ /*
+ * The constraint parent shouldn't be set beforehand, or if it's already
+ * set, it should be the specified parent.
+ */
+ partConstrParentIsSet = OidIsValid(partConstr->conparentid);
+ Assert(!partConstrParentIsSet || partConstr->conparentid == parentConstrOid);
+
ReleaseSysCache(partcontup);
ReleaseSysCache(parentConstrTup);
@@ -11522,8 +11593,10 @@ AttachPartitionForeignKey(List **wqueue,
DropForeignKeyConstraintTriggers(trigrel, partConstrOid, partConstrFrelid,
partConstrRelid);
- ConstraintSetParentConstraint(partConstrOid, parentConstrOid,
- RelationGetRelid(partition));
+ /* Skip if the parent is already set */
+ if (!partConstrParentIsSet)
+ ConstraintSetParentConstraint(partConstrOid, parentConstrOid,
+ RelationGetRelid(partition));
/*
* Like the constraint, attach partition's "check" triggers to the
@@ -11568,7 +11641,7 @@ AttachPartitionForeignKey(List **wqueue,
* validate the child data. Conversely, having an invalid parent
* constraint while the child constraint is valid doesn't cause any harm.
*/
- if (parentConstrIsValid && !partConstrIsValid);
+ if (wqueue && parentConstrIsValid && !partConstrIsValid)
{
Relation conrel;
@@ -12151,6 +12224,17 @@ ATExecAlterConstrEnforceability(Constraint *cmdcon, Relation conrel,
/* Drop all the triggers */
DropForeignKeyConstraintTriggers(tgrel, conoid, InvalidOid, InvalidOid);
+
+ /*
+ * If the referenced table is partitioned, the child constraint we're
+ * changing to not-enforced may have additional pg_constraint rows and
+ * action triggers that remain untouched while this child constraint
+ * is attached to the not-enforced parent. These must now be removed.
+ * For more details, see AttachPartitionForeignKey().
+ */
+ if (OidIsValid(currcon->conparentid) &&
+ get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE)
+ RemoveInheritedConstraint(conrel, tgrel, currcon->conrelid, conoid);
}
else /* Create triggers */
{
@@ -12212,13 +12296,35 @@ ATExecAlterConstrEnforceability(Constraint *cmdcon, Relation conrel,
true, NULL, 1, &pkey);
while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
- ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, fkrelid, pkrelid,
- childtup, otherrelids, lockmode,
- ReferencedDelTriggerOid,
- ReferencedUpdTriggerOid,
- ReferencingInsTriggerOid,
- ReferencingUpdTriggerOid);
+ {
+ Form_pg_constraint childcon;
+ childcon = (Form_pg_constraint) GETSTRUCT(childtup);
+
+ if (childcon->conenforced)
+ {
+ /*
+ * The child constraint is already attached to the parent
+ * constraint. However, when the parent constraint is
+ * changed to enforced, some constraints and action
+ * triggers on the existing enforced child constraint may
+ * become redundant and need to be addressed.
+ */
+ if (currcon->confrelid == pkrelid)
+ AttachPartitionForeignKey(NULL, rel, childcon->oid,
+ conoid,
+ ReferencingInsTriggerOid,
+ ReferencingUpdTriggerOid,
+ tgrel);
+ }
+ else
+ ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, childtup, otherrelids,
+ lockmode, ReferencedDelTriggerOid,
+ ReferencedUpdTriggerOid,
+ ReferencingInsTriggerOid,
+ ReferencingUpdTriggerOid);
+ }
systable_endscan(pscan);
}
@@ -20348,7 +20454,9 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
{
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
+ HeapTuple parentContup;
Form_pg_constraint conform;
+ Oid parentConstrIsEnforced;
contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
if (!HeapTupleIsValid(contup))
@@ -20363,12 +20471,34 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
continue;
}
+ /* Get the enforcibility of the parent constraint */
+ parentContup = SearchSysCache1(CONSTROID,
+ ObjectIdGetDatum(conform->conparentid));
+ if (!HeapTupleIsValid(parentContup))
+ elog(ERROR, "cache lookup failed for constraint %u",
+ conform->conparentid);
+ parentConstrIsEnforced =
+ ((Form_pg_constraint) GETSTRUCT(parentContup))->conenforced;
+ ReleaseSysCache(parentContup);
+
/*
* The constraint on this table must be marked no longer a child of
* the parent's constraint, as do its check triggers.
*/
ConstraintSetParentConstraint(fk->conoid, InvalidOid, InvalidOid);
+ /*
+ * Unsetting the parent is sufficient when the parent constraint is
+ * not enforced and the child constraint is enforced, as we link them
+ * by setting the constraint parent, while leaving the rest unchanged.
+ * For more details, see AttachPartitionForeignKey().
+ */
+ if (!parentConstrIsEnforced && fk->conenforced)
+ {
+ ReleaseSysCache(contup);
+ continue;
+ }
+
/*
* Also, look up the partition's "check" triggers corresponding to the
* enforced constraint being detached and detach them from the parent
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 9f6f22dd988..d894beaf1c0 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -1713,8 +1713,33 @@ ALTER TABLE fk_partitioned_fk_3 DROP COLUMN fdrop1, DROP COLUMN fdrop2,
DROP COLUMN fdrop3, DROP COLUMN fdrop4;
CREATE TABLE fk_partitioned_fk_3_0 PARTITION OF fk_partitioned_fk_3 FOR VALUES WITH (MODULUS 5, REMAINDER 0);
CREATE TABLE fk_partitioned_fk_3_1 PARTITION OF fk_partitioned_fk_3 FOR VALUES WITH (MODULUS 5, REMAINDER 1);
+-- Merge the not-enforced parent constraint with the enforced and not-enforced child constraints.
+ALTER TABLE fk_partitioned_fk_3_0 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ENFORCED;
+ALTER TABLE fk_partitioned_fk_3_1 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk_3 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+SELECT conname, conenforced, convalidated, conrelid::regclass FROM pg_constraint
+WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+ conname | conenforced | convalidated | conrelid
+--------------------------------+-------------+--------------+-----------------------
+ fk_partitioned_fk_3_0_a_b_fkey | t | t | fk_partitioned_fk_3_0
+ fk_partitioned_fk_3_1_a_b_fkey | f | f | fk_partitioned_fk_3_1
+ fk_partitioned_fk_3_a_b_fkey | f | f | fk_partitioned_fk_3
+(3 rows)
+
+-- Merging an enforced parent constraint (validated) with a not-enforced child
+-- constraint will implicitly change the child constraint to enforced and apply
+-- the validation as well.
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_3
FOR VALUES FROM (2000,2000) TO (3000,3000);
+SELECT conname, conenforced, convalidated, conrelid::regclass FROM pg_constraint
+WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+ conname | conenforced | convalidated | conrelid
+--------------------------------+-------------+--------------+-----------------------
+ fk_partitioned_fk_3_0_a_b_fkey | t | t | fk_partitioned_fk_3_0
+ fk_partitioned_fk_3_1_a_b_fkey | t | t | fk_partitioned_fk_3_1
+ fk_partitioned_fk_3_a_b_fkey | t | t | fk_partitioned_fk_3
+(3 rows)
+
-- Creating a foreign key with ONLY on a partitioned table referencing
-- a non-partitioned table fails.
ALTER TABLE ONLY fk_partitioned_fk ADD FOREIGN KEY (a, b)
@@ -1735,16 +1760,16 @@ INSERT INTO fk_partitioned_fk_2 (a,b) VALUES (1500, 1501);
ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_2_a_b_fkey"
DETAIL: Key (a, b)=(1500, 1501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (2500, 2502);
-ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_3_1_a_b_fkey"
DETAIL: Key (a, b)=(2500, 2502) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk_3 (a,b) VALUES (2500, 2502);
-ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_3_1_a_b_fkey"
DETAIL: Key (a, b)=(2500, 2502) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (2501, 2503);
-ERROR: insert or update on table "fk_partitioned_fk_3_0" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_0" violates foreign key constraint "fk_partitioned_fk_3_0_a_b_fkey"
DETAIL: Key (a, b)=(2501, 2503) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk_3 (a,b) VALUES (2501, 2503);
-ERROR: insert or update on table "fk_partitioned_fk_3_0" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_0" violates foreign key constraint "fk_partitioned_fk_3_0_a_b_fkey"
DETAIL: Key (a, b)=(2501, 2503) is not present in table "fk_notpartitioned_pk".
-- but if we insert the values that make them valid, then they work
INSERT INTO fk_notpartitioned_pk VALUES (500, 501), (1500, 1501),
@@ -1755,7 +1780,7 @@ INSERT INTO fk_partitioned_fk (a,b) VALUES (2500, 2502);
INSERT INTO fk_partitioned_fk (a,b) VALUES (2501, 2503);
-- this update fails because there is no referenced row
UPDATE fk_partitioned_fk SET a = a + 1 WHERE a = 2501;
-ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_3_1_a_b_fkey"
DETAIL: Key (a, b)=(2502, 2503) is not present in table "fk_notpartitioned_pk".
-- but we can fix it thusly:
INSERT INTO fk_notpartitioned_pk (a,b) VALUES (2502, 2503);
@@ -2030,7 +2055,6 @@ ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN
a | integer | | |
Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
Foreign-key constraints:
- "fk_partitioned_fk_2_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED
TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
DROP TABLE fk_partitioned_fk_2;
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index 98f47873d41..8dda6b6bbae 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -1295,9 +1295,23 @@ ALTER TABLE fk_partitioned_fk_3 DROP COLUMN fdrop1, DROP COLUMN fdrop2,
DROP COLUMN fdrop3, DROP COLUMN fdrop4;
CREATE TABLE fk_partitioned_fk_3_0 PARTITION OF fk_partitioned_fk_3 FOR VALUES WITH (MODULUS 5, REMAINDER 0);
CREATE TABLE fk_partitioned_fk_3_1 PARTITION OF fk_partitioned_fk_3 FOR VALUES WITH (MODULUS 5, REMAINDER 1);
+-- Merge the not-enforced parent constraint with the enforced and not-enforced child constraints.
+ALTER TABLE fk_partitioned_fk_3_0 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ENFORCED;
+ALTER TABLE fk_partitioned_fk_3_1 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk_3 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+
+SELECT conname, conenforced, convalidated, conrelid::regclass FROM pg_constraint
+WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+
+-- Merging an enforced parent constraint (validated) with a not-enforced child
+-- constraint will implicitly change the child constraint to enforced and apply
+-- the validation as well.
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_3
FOR VALUES FROM (2000,2000) TO (3000,3000);
+SELECT conname, conenforced, convalidated, conrelid::regclass FROM pg_constraint
+WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+
-- Creating a foreign key with ONLY on a partitioned table referencing
-- a non-partitioned table fails.
ALTER TABLE ONLY fk_partitioned_fk ADD FOREIGN KEY (a, b)
--
2.43.5
On Fri, Jan 17, 2025 at 6:20 PM Amul Sul <sulamul@gmail.com> wrote:
On Thu, Jan 16, 2025 at 6:55 PM Amul Sul <sulamul@gmail.com> wrote:
On Thu, Jan 16, 2025 at 6:07 PM Peter Eisentraut <peter@eisentraut.org> wrote:
On 11.01.25 18:26, Amul Sul wrote:
On Saturday, 11 January 2025, Peter Eisentraut <peter@eisentraut.org
<mailto:peter@eisentraut.org>> wrote:I have applied v8-0001, with some editing of the documentation and
in the tests. I'll continue reviewing the subsequent patches.Thank you for the improvement and commit.
I have also committed v8-0002, the refactor of ATExecAlterConstrRecurse().
All the remaining patches go together, so w are now waiting on an
updated patch set from you.Thanks!
Yes, I'm working on it and will post it tomorrow.
Attached is a new set of patches. Please ignore patch 0001 here, which
was posted separately [1] -- proposes allowing invalid foreign key
constraints on partitioned tables. Patch 0002 refactors
tryAttachPartitionForeignKey(), primarily needed by the new patch
v9-0006 included in this set. This patch handles merging constraints
with different enforceability. Without it, a new constraint would be
created on the child. However, this patch introduces additional code
that may appear somewhat messy or confusing. I've added plenty of
comments to clarify the logic. While I’ve done some testing, it hasn’t
been extensive. I plan to do more testing in the next week.Please let me know if we should continue with patch 0006 improvement,
or if the feature up to patch 0005, which enforces matching
enforceability before merging constraints, is sufficient.1] /messages/by-id/CAAJ_b96Bp=-ZwihPPtuaNX=SrZ0U6ZsXD3+fgARO0JuKa8v2jQ@mail.gmail.com
I made minor updates to the attached version, particularly ensuring
that the order of relation opening and closing remains the same as
before in ATExecAlterConstrRecurse(). Additionally, I’ve added a
refactoring patch to make createForeignKeyActionTriggers() accept a
relation OID instead of a Relation, making this function consistent
with others like createForeignKeyCheckTriggers().
Regards,
Amul
Attachments:
v10-0001-POSTED_SEPARATLY-Allow_NOT_VALID_FK_on_Partition.patchapplication/octet-stream; name=v10-0001-POSTED_SEPARATLY-Allow_NOT_VALID_FK_on_Partition.patchDownload
From f104f8c2fb66c441d75b8f0db6afc450f57edf20 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Thu, 16 Jan 2025 22:09:38 +0530
Subject: [PATCH v10 1/7] POSTED_SEPARATLY-Allow_NOT_VALID_FK_on_Partitioned
This is necessary before adding the NOT ENFORCED constraint,
especially on partitioned tables where NOT VALID foreign key
constraints are currently prohibited. As a result, a patch to enable
support for NOT VALID FK constraints on partitioned tables has been
posted here:
https://postgr.es/m/CAAJ_b96Bp=-ZwihPPtuaNX=SrZ0U6ZsXD3+fgARO0JuKa8v2jQ@mail.gmail.com
---
doc/src/sgml/ref/alter_table.sgml | 2 -
src/backend/commands/tablecmds.c | 126 +++++++++++++++-------
src/test/regress/expected/foreign_key.out | 76 +++++++++++--
src/test/regress/sql/foreign_key.sql | 58 +++++++++-
4 files changed, 214 insertions(+), 48 deletions(-)
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index df4f5d5bbd8..f9576da435e 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -486,8 +486,6 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
<para>
Additional restrictions apply when unique or primary key constraints
are added to partitioned tables; see <link linkend="sql-createtable"><command>CREATE TABLE</command></link>.
- Also, foreign key constraints on partitioned
- tables may not be declared <literal>NOT VALID</literal> at present.
</para>
</listitem>
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d2420a9558c..efa47dee5c4 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -574,8 +574,9 @@ static void createForeignKeyActionTriggers(Relation rel, Oid refRelOid,
Oid indexOid,
Oid parentDelTrigger, Oid parentUpdTrigger,
Oid *deleteTrigOid, Oid *updateTrigOid);
-static bool tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
- Oid partRelid,
+static bool tryAttachPartitionForeignKey(List **wqueue,
+ ForeignKeyCacheInfo *fk,
+ Relation partition,
Oid parentConstrOid, int numfks,
AttrNumber *mapped_conkey, AttrNumber *confkey,
Oid *conpfeqop,
@@ -9772,22 +9773,12 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
* Validity checks (permission checks wait till we have the column
* numbers)
*/
- if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
- {
- if (!recurse)
- ereport(ERROR,
- (errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("cannot use ONLY for foreign key on partitioned table \"%s\" referencing relation \"%s\"",
- RelationGetRelationName(rel),
- RelationGetRelationName(pkrel))));
- if (fkconstraint->skip_validation && !fkconstraint->initially_valid)
- ereport(ERROR,
- (errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("cannot add NOT VALID foreign key on partitioned table \"%s\" referencing relation \"%s\"",
- RelationGetRelationName(rel),
- RelationGetRelationName(pkrel)),
- errdetail("This feature is not yet supported on partitioned tables.")));
- }
+ if (!recurse && rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot use ONLY for foreign key on partitioned table \"%s\" referencing relation \"%s\"",
+ RelationGetRelationName(rel),
+ RelationGetRelationName(pkrel))));
if (pkrel->rd_rel->relkind != RELKIND_RELATION &&
pkrel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
@@ -10780,8 +10771,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
*/
for (int i = 0; i < pd->nparts; i++)
{
- Oid partitionId = pd->oids[i];
- Relation partition = table_open(partitionId, lockmode);
+ Relation partition = table_open(pd->oids[i], lockmode);
List *partFKs;
AttrMap *attmap;
AttrNumber mapped_fkattnum[INDEX_MAX_KEYS];
@@ -10805,8 +10795,9 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
ForeignKeyCacheInfo *fk;
fk = lfirst_node(ForeignKeyCacheInfo, cell);
- if (tryAttachPartitionForeignKey(fk,
- partitionId,
+ if (tryAttachPartitionForeignKey(wqueue,
+ fk,
+ partition,
parentConstr,
numfks,
mapped_fkattnum,
@@ -11258,8 +11249,9 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
{
ForeignKeyCacheInfo *fk = lfirst_node(ForeignKeyCacheInfo, lc);
- if (tryAttachPartitionForeignKey(fk,
- RelationGetRelid(partRel),
+ if (tryAttachPartitionForeignKey(wqueue,
+ fk,
+ partRel,
parentConstrOid,
numfks,
mapped_conkey,
@@ -11362,8 +11354,9 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
* return false.
*/
static bool
-tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
- Oid partRelid,
+tryAttachPartitionForeignKey(List **wqueue,
+ ForeignKeyCacheInfo *fk,
+ Relation partition,
Oid parentConstrOid,
int numfks,
AttrNumber *mapped_conkey,
@@ -11382,12 +11375,15 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
HeapTuple trigtup;
Oid insertTriggerOid,
updateTriggerOid;
+ bool parentConstrIsValid;
+ bool partConstrIsValid;
parentConstrTup = SearchSysCache1(CONSTROID,
ObjectIdGetDatum(parentConstrOid));
if (!HeapTupleIsValid(parentConstrTup))
elog(ERROR, "cache lookup failed for constraint %u", parentConstrOid);
parentConstr = (Form_pg_constraint) GETSTRUCT(parentConstrTup);
+ parentConstrIsValid = parentConstr->convalidated;
/*
* Do some quick & easy initial checks. If any of these fail, we cannot
@@ -11410,17 +11406,18 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
}
/*
- * Looks good so far; do some more extensive checks. Presumably the check
- * for 'convalidated' could be dropped, since we don't really care about
- * that, but let's be careful for now.
+ * Looks good so far; perform more extensive checks except for
+ * convalidated. There’s no need to worry about it because attaching a
+ * valid parent constraint to an invalid child constraint will eventually
+ * trigger implicit data validation for the child.
*/
partcontup = SearchSysCache1(CONSTROID,
ObjectIdGetDatum(fk->conoid));
if (!HeapTupleIsValid(partcontup))
elog(ERROR, "cache lookup failed for constraint %u", fk->conoid);
partConstr = (Form_pg_constraint) GETSTRUCT(partcontup);
+ partConstrIsValid = parentConstr->convalidated;
if (OidIsValid(partConstr->conparentid) ||
- !partConstr->convalidated ||
partConstr->condeferrable != parentConstr->condeferrable ||
partConstr->condeferred != parentConstr->condeferred ||
partConstr->confupdtype != parentConstr->confupdtype ||
@@ -11479,7 +11476,8 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
systable_endscan(scan);
- ConstraintSetParentConstraint(fk->conoid, parentConstrOid, partRelid);
+ ConstraintSetParentConstraint(fk->conoid, parentConstrOid,
+ RelationGetRelid(partition));
/*
* Like the constraint, attach partition's "check" triggers to the
@@ -11490,10 +11488,10 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
&insertTriggerOid, &updateTriggerOid);
Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
- partRelid);
+ RelationGetRelid(partition));
Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
- partRelid);
+ RelationGetRelid(partition));
/*
* If the referenced table is partitioned, then the partition we're
@@ -11571,6 +11569,32 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
}
CommandCounterIncrement();
+
+ /*
+ * If a valid parent constraint is attached to a NOT VALID child
+ * constraint, implicitly trigger validation for the child constraint.
+ * This behavior aligns with creating a new constraint on the child table
+ * rather than attaching it to the existing one, as it would ultimately
+ * validate the child data. Conversely, having an invalid parent
+ * constraint while the child constraint is valid doesn't cause any harm.
+ */
+ if (parentConstrIsValid && !partConstrIsValid);
+ {
+ Relation conrel;
+
+ conrel = table_open(ConstraintRelationId, RowExclusiveLock);
+
+ partcontup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
+ if (!HeapTupleIsValid(partcontup))
+ elog(ERROR, "cache lookup failed for constraint %u", fk->conoid);
+
+ /* Using the same lock as used in AT_ValidateConstraint */
+ QueueFKConstraintValidation(wqueue, conrel, partition, partcontup,
+ ShareUpdateExclusiveLock);
+ ReleaseSysCache(partcontup);
+ table_close(conrel, RowExclusiveLock);
+ }
+
return true;
}
@@ -12111,7 +12135,7 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
*
* Add an entry to the wqueue to validate the given foreign key constraint in
* Phase 3 and update the convalidated field in the pg_constraint catalog
- * for the specified relation.
+ * for the specified relation and all its children.
*/
static void
QueueFKConstraintValidation(List **wqueue, Relation conrel, Relation rel,
@@ -12149,9 +12173,39 @@ QueueFKConstraintValidation(List **wqueue, Relation conrel, Relation rel,
}
/*
- * We disallow creating invalid foreign keys to or from partitioned
- * tables, so ignoring the recursion bit is okay.
+ * If the table at either end of the constraint is partitioned, we need to
+ * recurse and handle every constraint that is a child of this constraint.
*/
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(con->confrelid) == RELKIND_PARTITIONED_TABLE)
+ {
+ ScanKeyData pkey;
+ SysScanDesc pscan;
+ HeapTuple childtup;
+
+ ScanKeyInit(&pkey,
+ Anum_pg_constraint_conparentid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(con->oid));
+
+ pscan = systable_beginscan(conrel, ConstraintParentIndexId,
+ true, NULL, 1, &pkey);
+
+ while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
+ {
+ Form_pg_constraint childcon;
+ Relation childrel;
+
+ childcon = (Form_pg_constraint) GETSTRUCT(childtup);
+ childrel = table_open(childcon->conrelid, lockmode);
+
+ QueueFKConstraintValidation(wqueue, conrel, childrel, childtup,
+ lockmode);
+ table_close(childrel, NoLock);
+ }
+
+ systable_endscan(pscan);
+ }
/*
* Now update the catalog, while we have the door open.
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 3f459f70ac1..f617d519aef 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -1571,6 +1571,76 @@ drop table pktable2, fktable2;
--
-- Foreign keys and partitioned tables
--
+-- NOT VALID foreign key on partitioned table
+CREATE TABLE fk_notpartitioned_pk (a int, b int, PRIMARY KEY (a, b));
+CREATE TABLE fk_partitioned_fk (b int, a int) PARTITION BY RANGE (a, b);
+ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT VALID;
+-- Attaching a child table with the same valid foreign key constraint.
+CREATE TABLE fk_partitioned_fk_1 (a int, b int);
+ALTER TABLE fk_partitioned_fk_1 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FROM (0,0) TO (1000,1000);
+-- Child constraint will remain valid.
+SELECT conname, convalidated, conrelid::regclass FROM pg_constraint
+WHERE conrelid::regclass::text like 'fk_partitioned_fk%' ORDER BY oid;
+ conname | convalidated | conrelid
+------------------------------+--------------+---------------------
+ fk_partitioned_fk_a_b_fkey | f | fk_partitioned_fk
+ fk_partitioned_fk_1_a_b_fkey | t | fk_partitioned_fk_1
+(2 rows)
+
+-- Validate the constraint
+ALTER TABLE fk_partitioned_fk VALIDATE CONSTRAINT fk_partitioned_fk_a_b_fkey;
+-- All constraints are now valid.
+SELECT conname, convalidated, conrelid::regclass FROM pg_constraint
+WHERE conrelid::regclass::text like 'fk_partitioned_fk%' ORDER BY oid;
+ conname | convalidated | conrelid
+------------------------------+--------------+---------------------
+ fk_partitioned_fk_a_b_fkey | t | fk_partitioned_fk
+ fk_partitioned_fk_1_a_b_fkey | t | fk_partitioned_fk_1
+(2 rows)
+
+-- Attaching a child with a NOT VALID constraint.
+CREATE TABLE fk_partitioned_fk_2 (a int, b int);
+INSERT INTO fk_partitioned_fk_2 VALUES(1000, 1000); -- doesn't exist in referenced table
+ALTER TABLE fk_partitioned_fk_2 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT VALID;
+-- It will fail because the attach operation implicitly validates the data.
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
+ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_2_a_b_fkey"
+DETAIL: Key (a, b)=(1000, 1000) is not present in table "fk_notpartitioned_pk".
+-- Remove the invalid data and try again.
+TRUNCATE fk_partitioned_fk_2;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
+-- The child constraint will also be valid.
+SELECT conname, convalidated FROM pg_constraint WHERE conrelid = 'fk_partitioned_fk_2'::regclass;
+ conname | convalidated
+------------------------------+--------------
+ fk_partitioned_fk_2_a_b_fkey | t
+(1 row)
+
+DROP TABLE fk_partitioned_fk, fk_notpartitioned_pk;
+-- NOT VALID foreign key on a non-partitioned table referencing a partitioned table
+CREATE TABLE fk_partitioned_pk (a int, b int, PRIMARY KEY (a, b)) PARTITION BY RANGE (a, b);
+CREATE TABLE fk_partitioned_pk_1 PARTITION OF fk_partitioned_pk FOR VALUES FROM (0,0) TO (1000,1000);
+CREATE TABLE fk_notpartitioned_fk (b int, a int);
+ALTER TABLE fk_notpartitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk NOT VALID;
+-- Constraint will be invalid.
+SELECT conname, convalidated FROM pg_constraint WHERE conrelid = 'fk_notpartitioned_fk'::regclass;
+ conname | convalidated
+--------------------------------+--------------
+ fk_notpartitioned_fk_a_b_fkey | f
+ fk_notpartitioned_fk_a_b_fkey1 | f
+(2 rows)
+
+ALTER TABLE fk_notpartitioned_fk VALIDATE CONSTRAINT fk_notpartitioned_fk_a_b_fkey;
+-- All constraints are now valid.
+SELECT conname, convalidated FROM pg_constraint WHERE conrelid = 'fk_notpartitioned_fk'::regclass;
+ conname | convalidated
+--------------------------------+--------------
+ fk_notpartitioned_fk_a_b_fkey | t
+ fk_notpartitioned_fk_a_b_fkey1 | t
+(2 rows)
+
+DROP TABLE fk_notpartitioned_fk, fk_partitioned_pk;
-- Creation of a partitioned hierarchy with irregular definitions
CREATE TABLE fk_notpartitioned_pk (fdrop1 int, a int, fdrop2 int, b int,
PRIMARY KEY (a, b));
@@ -1597,12 +1667,6 @@ ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_3
ALTER TABLE ONLY fk_partitioned_fk ADD FOREIGN KEY (a, b)
REFERENCES fk_notpartitioned_pk;
ERROR: cannot use ONLY for foreign key on partitioned table "fk_partitioned_fk" referencing relation "fk_notpartitioned_pk"
--- Adding a NOT VALID foreign key on a partitioned table referencing
--- a non-partitioned table fails.
-ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b)
- REFERENCES fk_notpartitioned_pk NOT VALID;
-ERROR: cannot add NOT VALID foreign key on partitioned table "fk_partitioned_fk" referencing relation "fk_notpartitioned_pk"
-DETAIL: This feature is not yet supported on partitioned tables.
-- these inserts, targeting both the partition directly as well as the
-- partitioned table, should all fail
INSERT INTO fk_partitioned_fk (a,b) VALUES (500, 501);
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index 2e710e419c2..d5f68812ba6 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -1173,6 +1173,60 @@ drop table pktable2, fktable2;
-- Foreign keys and partitioned tables
--
+-- NOT VALID foreign key on partitioned table
+CREATE TABLE fk_notpartitioned_pk (a int, b int, PRIMARY KEY (a, b));
+CREATE TABLE fk_partitioned_fk (b int, a int) PARTITION BY RANGE (a, b);
+ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT VALID;
+
+-- Attaching a child table with the same valid foreign key constraint.
+CREATE TABLE fk_partitioned_fk_1 (a int, b int);
+ALTER TABLE fk_partitioned_fk_1 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FROM (0,0) TO (1000,1000);
+
+-- Child constraint will remain valid.
+SELECT conname, convalidated, conrelid::regclass FROM pg_constraint
+WHERE conrelid::regclass::text like 'fk_partitioned_fk%' ORDER BY oid;
+
+-- Validate the constraint
+ALTER TABLE fk_partitioned_fk VALIDATE CONSTRAINT fk_partitioned_fk_a_b_fkey;
+
+-- All constraints are now valid.
+SELECT conname, convalidated, conrelid::regclass FROM pg_constraint
+WHERE conrelid::regclass::text like 'fk_partitioned_fk%' ORDER BY oid;
+
+-- Attaching a child with a NOT VALID constraint.
+CREATE TABLE fk_partitioned_fk_2 (a int, b int);
+INSERT INTO fk_partitioned_fk_2 VALUES(1000, 1000); -- doesn't exist in referenced table
+ALTER TABLE fk_partitioned_fk_2 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT VALID;
+
+-- It will fail because the attach operation implicitly validates the data.
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
+
+-- Remove the invalid data and try again.
+TRUNCATE fk_partitioned_fk_2;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
+
+-- The child constraint will also be valid.
+SELECT conname, convalidated FROM pg_constraint WHERE conrelid = 'fk_partitioned_fk_2'::regclass;
+
+DROP TABLE fk_partitioned_fk, fk_notpartitioned_pk;
+
+-- NOT VALID foreign key on a non-partitioned table referencing a partitioned table
+CREATE TABLE fk_partitioned_pk (a int, b int, PRIMARY KEY (a, b)) PARTITION BY RANGE (a, b);
+CREATE TABLE fk_partitioned_pk_1 PARTITION OF fk_partitioned_pk FOR VALUES FROM (0,0) TO (1000,1000);
+CREATE TABLE fk_notpartitioned_fk (b int, a int);
+ALTER TABLE fk_notpartitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk NOT VALID;
+
+-- Constraint will be invalid.
+SELECT conname, convalidated FROM pg_constraint WHERE conrelid = 'fk_notpartitioned_fk'::regclass;
+
+ALTER TABLE fk_notpartitioned_fk VALIDATE CONSTRAINT fk_notpartitioned_fk_a_b_fkey;
+
+-- All constraints are now valid.
+SELECT conname, convalidated FROM pg_constraint WHERE conrelid = 'fk_notpartitioned_fk'::regclass;
+
+DROP TABLE fk_notpartitioned_fk, fk_partitioned_pk;
+
-- Creation of a partitioned hierarchy with irregular definitions
CREATE TABLE fk_notpartitioned_pk (fdrop1 int, a int, fdrop2 int, b int,
PRIMARY KEY (a, b));
@@ -1200,10 +1254,6 @@ ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_3
-- a non-partitioned table fails.
ALTER TABLE ONLY fk_partitioned_fk ADD FOREIGN KEY (a, b)
REFERENCES fk_notpartitioned_pk;
--- Adding a NOT VALID foreign key on a partitioned table referencing
--- a non-partitioned table fails.
-ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b)
- REFERENCES fk_notpartitioned_pk NOT VALID;
-- these inserts, targeting both the partition directly as well as the
-- partitioned table, should all fail
--
2.43.5
v10-0002-refactor-Split-tryAttachPartitionForeignKey.patchapplication/octet-stream; name=v10-0002-refactor-Split-tryAttachPartitionForeignKey.patchDownload
From b045142993184c23693cf49460bd6cd4d288214f Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Wed, 15 Jan 2025 12:57:15 +0530
Subject: [PATCH v10 2/7] refactor: Split tryAttachPartitionForeignKey()
Split tryAttachPartitionForeignKey() into three functions:
AttachPartitionForeignKey(), RemoveInheritedConstraint() and
DropForeignKeyConstraintTriggers(), so they can be reused in the next
patch.
---
XXX: Function names could be better
---
---
src/backend/commands/tablecmds.c | 324 ++++++++++++++++++++-----------
1 file changed, 206 insertions(+), 118 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index efa47dee5c4..c7c35c52a3d 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -583,6 +583,14 @@ static bool tryAttachPartitionForeignKey(List **wqueue,
Oid parentInsTrigger,
Oid parentUpdTrigger,
Relation trigrel);
+static void AttachPartitionForeignKey(List **wqueue, Relation partition,
+ Oid partConstrOid, Oid parentConstrOid,
+ Oid parentInsTrigger, Oid parentUpdTrigger,
+ Relation trigrel);
+static void RemoveInheritedConstraint(Relation conrel, Relation trigrel, Oid conrelid,
+ Oid conparentid);
+static void DropForeignKeyConstraintTriggers(Relation trigrel, Oid conoid,
+ Oid confrelid, Oid conrelid);
static void GetForeignKeyActionTriggers(Relation trigrel,
Oid conoid, Oid confrelid, Oid conrelid,
Oid *deleteTriggerOid,
@@ -11370,20 +11378,12 @@ tryAttachPartitionForeignKey(List **wqueue,
Form_pg_constraint parentConstr;
HeapTuple partcontup;
Form_pg_constraint partConstr;
- ScanKeyData key;
- SysScanDesc scan;
- HeapTuple trigtup;
- Oid insertTriggerOid,
- updateTriggerOid;
- bool parentConstrIsValid;
- bool partConstrIsValid;
parentConstrTup = SearchSysCache1(CONSTROID,
ObjectIdGetDatum(parentConstrOid));
if (!HeapTupleIsValid(parentConstrTup))
elog(ERROR, "cache lookup failed for constraint %u", parentConstrOid);
parentConstr = (Form_pg_constraint) GETSTRUCT(parentConstrTup);
- parentConstrIsValid = parentConstr->convalidated;
/*
* Do some quick & easy initial checks. If any of these fail, we cannot
@@ -11416,7 +11416,6 @@ tryAttachPartitionForeignKey(List **wqueue,
if (!HeapTupleIsValid(partcontup))
elog(ERROR, "cache lookup failed for constraint %u", fk->conoid);
partConstr = (Form_pg_constraint) GETSTRUCT(partcontup);
- partConstrIsValid = parentConstr->convalidated;
if (OidIsValid(partConstr->conparentid) ||
partConstr->condeferrable != parentConstr->condeferrable ||
partConstr->condeferred != parentConstr->condeferred ||
@@ -11429,54 +11428,75 @@ tryAttachPartitionForeignKey(List **wqueue,
return false;
}
+ ReleaseSysCache(parentConstrTup);
+ ReleaseSysCache(partcontup);
+
+ /* Looks good! Attach this constraint. */
+ AttachPartitionForeignKey(wqueue, partition, fk->conoid,
+ parentConstrOid, parentInsTrigger,
+ parentUpdTrigger, trigrel);
+
+ return true;
+}
+
+/*
+ * AttachPartitionForeignKey
+ *
+ * The subroutine for tryAttachPartitionForeignKey performs the final tasks of
+ * attaching the constraint, removing redundant triggers and entries from
+ * pg_constraint, and setting the constraint's parent.
+ */
+static void
+AttachPartitionForeignKey(List **wqueue,
+ Relation partition,
+ Oid partConstrOid,
+ Oid parentConstrOid,
+ Oid parentInsTrigger,
+ Oid parentUpdTrigger,
+ Relation trigrel)
+{
+ HeapTuple parentConstrTup;
+ Form_pg_constraint parentConstr;
+ HeapTuple partcontup;
+ Form_pg_constraint partConstr;
+ Oid partConstrFrelid;
+ Oid partConstrRelid;
+ bool parentConstrIsValid;
+ bool partConstrIsValid;
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
+ /* Fetch the parent constraint tuple */
+ parentConstrTup = SearchSysCache1(CONSTROID,
+ ObjectIdGetDatum(parentConstrOid));
+ if (!HeapTupleIsValid(parentConstrTup))
+ elog(ERROR, "cache lookup failed for constraint %u", parentConstrOid);
+ parentConstr = (Form_pg_constraint) GETSTRUCT(parentConstrTup);
+ parentConstrIsValid = parentConstr->convalidated;
+
+ /* Fetch the child constraint tuple */
+ partcontup = SearchSysCache1(CONSTROID,
+ ObjectIdGetDatum(partConstrOid));
+ if (!HeapTupleIsValid(partcontup))
+ elog(ERROR, "cache lookup failed for constraint %u", partConstrOid);
+ partConstr = (Form_pg_constraint) GETSTRUCT(partcontup);
+ partConstrIsValid = parentConstr->convalidated;
+ partConstrFrelid = partConstr->confrelid;
+ partConstrRelid = partConstr->conrelid;
+
ReleaseSysCache(partcontup);
ReleaseSysCache(parentConstrTup);
/*
- * Looks good! Attach this constraint. The action triggers in the new
- * partition become redundant -- the parent table already has equivalent
- * ones, and those will be able to reach the partition. Remove the ones
- * in the partition. We identify them because they have our constraint
- * OID, as well as being on the referenced rel.
+ * The action triggers in the new partition become redundant -- the parent
+ * table already has equivalent ones, and those will be able to reach the
+ * partition. Remove the ones in the partition. We identify them because
+ * they have our constraint OID, as well as being on the referenced rel.
*/
- ScanKeyInit(&key,
- Anum_pg_trigger_tgconstraint,
- BTEqualStrategyNumber, F_OIDEQ,
- ObjectIdGetDatum(fk->conoid));
- scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
- NULL, 1, &key);
- while ((trigtup = systable_getnext(scan)) != NULL)
- {
- Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
- ObjectAddress trigger;
+ DropForeignKeyConstraintTriggers(trigrel, partConstrOid, partConstrFrelid,
+ partConstrRelid);
- if (trgform->tgconstrrelid != fk->conrelid)
- continue;
- if (trgform->tgrelid != fk->confrelid)
- continue;
-
- /*
- * The constraint is originally set up to contain this trigger as an
- * implementation object, so there's a dependency record that links
- * the two; however, since the trigger is no longer needed, we remove
- * the dependency link in order to be able to drop the trigger while
- * keeping the constraint intact.
- */
- deleteDependencyRecordsFor(TriggerRelationId,
- trgform->oid,
- false);
- /* make dependency deletion visible to performDeletion */
- CommandCounterIncrement();
- ObjectAddressSet(trigger, TriggerRelationId,
- trgform->oid);
- performDeletion(&trigger, DROP_RESTRICT, 0);
- /* make trigger drop visible, in case the loop iterates */
- CommandCounterIncrement();
- }
-
- systable_endscan(scan);
-
- ConstraintSetParentConstraint(fk->conoid, parentConstrOid,
+ ConstraintSetParentConstraint(partConstrOid, parentConstrOid,
RelationGetRelid(partition));
/*
@@ -11484,7 +11504,7 @@ tryAttachPartitionForeignKey(List **wqueue,
* corresponding parent triggers.
*/
GetForeignKeyCheckTriggers(trigrel,
- fk->conoid, fk->confrelid, fk->conrelid,
+ partConstrOid, partConstrFrelid, partConstrRelid,
&insertTriggerOid, &updateTriggerOid);
Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
@@ -11498,72 +11518,12 @@ tryAttachPartitionForeignKey(List **wqueue,
* attaching now has extra pg_constraint rows and action triggers that are
* no longer needed. Remove those.
*/
- if (get_rel_relkind(fk->confrelid) == RELKIND_PARTITIONED_TABLE)
+ if (get_rel_relkind(partConstrFrelid) == RELKIND_PARTITIONED_TABLE)
{
Relation pg_constraint = table_open(ConstraintRelationId, RowShareLock);
- ObjectAddresses *objs;
- HeapTuple consttup;
- ScanKeyInit(&key,
- Anum_pg_constraint_conrelid,
- BTEqualStrategyNumber, F_OIDEQ,
- ObjectIdGetDatum(fk->conrelid));
-
- scan = systable_beginscan(pg_constraint,
- ConstraintRelidTypidNameIndexId,
- true, NULL, 1, &key);
- objs = new_object_addresses();
- while ((consttup = systable_getnext(scan)) != NULL)
- {
- Form_pg_constraint conform = (Form_pg_constraint) GETSTRUCT(consttup);
-
- if (conform->conparentid != fk->conoid)
- continue;
- else
- {
- ObjectAddress addr;
- SysScanDesc scan2;
- ScanKeyData key2;
- int n PG_USED_FOR_ASSERTS_ONLY;
-
- ObjectAddressSet(addr, ConstraintRelationId, conform->oid);
- add_exact_object_address(&addr, objs);
-
- /*
- * First we must delete the dependency record that binds the
- * constraint records together.
- */
- n = deleteDependencyRecordsForSpecific(ConstraintRelationId,
- conform->oid,
- DEPENDENCY_INTERNAL,
- ConstraintRelationId,
- fk->conoid);
- Assert(n == 1); /* actually only one is expected */
-
- /*
- * Now search for the triggers for this constraint and set
- * them up for deletion too
- */
- ScanKeyInit(&key2,
- Anum_pg_trigger_tgconstraint,
- BTEqualStrategyNumber, F_OIDEQ,
- ObjectIdGetDatum(conform->oid));
- scan2 = systable_beginscan(trigrel, TriggerConstraintIndexId,
- true, NULL, 1, &key2);
- while ((trigtup = systable_getnext(scan2)) != NULL)
- {
- ObjectAddressSet(addr, TriggerRelationId,
- ((Form_pg_trigger) GETSTRUCT(trigtup))->oid);
- add_exact_object_address(&addr, objs);
- }
- systable_endscan(scan2);
- }
- }
- /* make the dependency deletions visible */
- CommandCounterIncrement();
- performMultipleDeletions(objs, DROP_RESTRICT,
- PERFORM_DELETION_INTERNAL);
- systable_endscan(scan);
+ RemoveInheritedConstraint(pg_constraint, trigrel, partConstrRelid,
+ partConstrOid);
table_close(pg_constraint, RowShareLock);
}
@@ -11584,9 +11544,9 @@ tryAttachPartitionForeignKey(List **wqueue,
conrel = table_open(ConstraintRelationId, RowExclusiveLock);
- partcontup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
+ partcontup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(partConstrOid));
if (!HeapTupleIsValid(partcontup))
- elog(ERROR, "cache lookup failed for constraint %u", fk->conoid);
+ elog(ERROR, "cache lookup failed for constraint %u", partConstrOid);
/* Using the same lock as used in AT_ValidateConstraint */
QueueFKConstraintValidation(wqueue, conrel, partition, partcontup,
@@ -11594,8 +11554,136 @@ tryAttachPartitionForeignKey(List **wqueue,
ReleaseSysCache(partcontup);
table_close(conrel, RowExclusiveLock);
}
+}
- return true;
+/*
+ * RemoveInheritedConstraint
+ *
+ * Removes the constraint and its associated trigger from the specified
+ * relation, which inherited the given constraint.
+ */
+static void
+RemoveInheritedConstraint(Relation conrel, Relation trigrel, Oid conrelid,
+ Oid conparentid)
+{
+ ObjectAddresses *objs;
+ HeapTuple consttup;
+ ScanKeyData key;
+ SysScanDesc scan;
+ HeapTuple trigtup;
+
+ ScanKeyInit(&key,
+ Anum_pg_constraint_conrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conrelid));
+
+ scan = systable_beginscan(conrel,
+ ConstraintRelidTypidNameIndexId,
+ true, NULL, 1, &key);
+ objs = new_object_addresses();
+ while ((consttup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_constraint conform = (Form_pg_constraint) GETSTRUCT(consttup);
+
+ if (conform->conparentid != conparentid)
+ continue;
+ else
+ {
+ ObjectAddress addr;
+ SysScanDesc scan2;
+ ScanKeyData key2;
+ int n PG_USED_FOR_ASSERTS_ONLY;
+
+ ObjectAddressSet(addr, ConstraintRelationId, conform->oid);
+ add_exact_object_address(&addr, objs);
+
+ /*
+ * First we must delete the dependency record that binds the
+ * constraint records together.
+ */
+ n = deleteDependencyRecordsForSpecific(ConstraintRelationId,
+ conform->oid,
+ DEPENDENCY_INTERNAL,
+ ConstraintRelationId,
+ conparentid);
+ Assert(n == 1); /* actually only one is expected */
+
+ /*
+ * Now search for the triggers for this constraint and set them up
+ * for deletion too
+ */
+ ScanKeyInit(&key2,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conform->oid));
+ scan2 = systable_beginscan(trigrel, TriggerConstraintIndexId,
+ true, NULL, 1, &key2);
+ while ((trigtup = systable_getnext(scan2)) != NULL)
+ {
+ ObjectAddressSet(addr, TriggerRelationId,
+ ((Form_pg_trigger) GETSTRUCT(trigtup))->oid);
+ add_exact_object_address(&addr, objs);
+ }
+ systable_endscan(scan2);
+ }
+ }
+ /* make the dependency deletions visible */
+ CommandCounterIncrement();
+ performMultipleDeletions(objs, DROP_RESTRICT,
+ PERFORM_DELETION_INTERNAL);
+ systable_endscan(scan);
+}
+
+/*
+ * DropForeignKeyConstraintTriggers
+ *
+ * The subroutine for tryAttachPartitionForeignKey handles the deletion of
+ * action triggers for the foreign key constraint.
+ */
+static void
+DropForeignKeyConstraintTriggers(Relation trigrel, Oid conoid, Oid confrelid,
+ Oid conrelid)
+{
+ ScanKeyData key;
+ SysScanDesc scan;
+ HeapTuple trigtup;
+
+ ScanKeyInit(&key,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+ scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
+ NULL, 1, &key);
+ while ((trigtup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
+ ObjectAddress trigger;
+
+ if (trgform->tgconstrrelid != conrelid)
+ continue;
+ if (trgform->tgrelid != confrelid)
+ continue;
+
+ /*
+ * The constraint is originally set up to contain this trigger as an
+ * implementation object, so there's a dependency record that links
+ * the two; however, since the trigger is no longer needed, we remove
+ * the dependency link in order to be able to drop the trigger while
+ * keeping the constraint intact.
+ */
+ deleteDependencyRecordsFor(TriggerRelationId,
+ trgform->oid,
+ false);
+ /* make dependency deletion visible to performDeletion */
+ CommandCounterIncrement();
+ ObjectAddressSet(trigger, TriggerRelationId,
+ trgform->oid);
+ performDeletion(&trigger, DROP_RESTRICT, 0);
+ /* make trigger drop visible, in case the loop iterates */
+ CommandCounterIncrement();
+ }
+
+ systable_endscan(scan);
}
/*
--
2.43.5
v10-0003-refactor-Pass-Relid-instead-of-Relation-to-creat.patchapplication/octet-stream; name=v10-0003-refactor-Pass-Relid-instead-of-Relation-to-creat.patchDownload
From bd6e5a2fd28a585b183e9a0bf5ee13d500a9f830 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 20 Jan 2025 18:49:41 +0530
Subject: [PATCH v10 3/7] refactor: Pass Relid instead of Relation to
createForeignKeyCheckTriggers().
Currently, createForeignKeyCheckTriggers() takes a Relation type as
its first argument, but it doesn't use that argument directly.
Instead, it fetches the Relid by calling RelationGetRelid().
Therefore, it would be more consistent with other functions (e.g.,
createForeignKeyCheckTriggers()) to pass the Relid directly instead of
the whole Relation.
---
src/backend/commands/tablecmds.c | 20 ++++++++++----------
1 file changed, 10 insertions(+), 10 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c7c35c52a3d..b1c93320c6b 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -569,7 +569,7 @@ static void createForeignKeyCheckTriggers(Oid myRelOid, Oid refRelOid,
Oid indexOid,
Oid parentInsTrigger, Oid parentUpdTrigger,
Oid *insertTrigOid, Oid *updateTrigOid);
-static void createForeignKeyActionTriggers(Relation rel, Oid refRelOid,
+static void createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid,
Constraint *fkconstraint, Oid constraintOid,
Oid indexOid,
Oid parentDelTrigger, Oid parentUpdTrigger,
@@ -10580,7 +10580,8 @@ addFkRecurseReferenced(Constraint *fkconstraint, Relation rel,
/*
* Create the action triggers that enforce the constraint.
*/
- createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
+ createForeignKeyActionTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
fkconstraint,
parentConstr, indexOid,
parentDelTrigger, parentUpdTrigger,
@@ -12953,10 +12954,11 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
* *updateTrigOid.
*/
static void
-createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstraint,
- Oid constraintOid, Oid indexOid,
- Oid parentDelTrigger, Oid parentUpdTrigger,
- Oid *deleteTrigOid, Oid *updateTrigOid)
+createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid,
+ Constraint *fkconstraint, Oid constraintOid,
+ Oid indexOid, Oid parentDelTrigger,
+ Oid parentUpdTrigger, Oid *deleteTrigOid,
+ Oid *updateTrigOid)
{
CreateTrigStmt *fk_trigger;
ObjectAddress trigAddress;
@@ -13012,8 +13014,7 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
break;
}
- trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid,
- RelationGetRelid(rel),
+ trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid, myRelOid,
constraintOid, indexOid, InvalidOid,
parentDelTrigger, NULL, true, false);
if (deleteTrigOid)
@@ -13073,8 +13074,7 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
break;
}
- trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid,
- RelationGetRelid(rel),
+ trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid, myRelOid,
constraintOid, indexOid, InvalidOid,
parentUpdTrigger, NULL, true, false);
if (updateTrigOid)
--
2.43.5
v10-0004-refactor-Change-ATExecAlterConstrRecurse-argumen.patchapplication/octet-stream; name=v10-0004-refactor-Change-ATExecAlterConstrRecurse-argumen.patchDownload
From 8ce61fa793200f54514bae916cedb0eb7d3c8032 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Tue, 14 Jan 2025 22:35:32 +0530
Subject: [PATCH v10 4/7] refactor: Change ATExecAlterConstrRecurse() argument.
Instead of passing the Relation of the referencing relation, we will
pass a relid. Opening the relation using the relid again should not
cause significant issues. However, this approach makes recursion more
efficient, especially in the later patch where we will be recreating
the referencing and referred triggers. In addition to that passing
relid of the referred relation too.
----
NOTE: This patch is not meant to be committed separately. It should
be squashed with the main patch that adds ENFORCED/NOT ENFORCED.
----
---
src/backend/commands/tablecmds.c | 36 +++++++++++++++++---------------
1 file changed, 19 insertions(+), 17 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index b1c93320c6b..2164eaf017b 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -392,13 +392,15 @@ static void AlterSeqNamespaces(Relation classRel, Relation rel,
static ObjectAddress ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd,
bool recurse, bool recursing, LOCKMODE lockmode);
static bool ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
LOCKMODE lockmode);
static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
bool deferrable, bool initdeferred,
List **otherrelids);
static void ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
LOCKMODE lockmode);
static ObjectAddress ATExecValidateConstraint(List **wqueue,
Relation rel, char *constrName,
@@ -11925,8 +11927,9 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
currcon->condeferred != cmdcon->initdeferred ||
rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
- if (ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, rel, contuple,
- &otherrelids, lockmode))
+ if (ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, currcon->conrelid,
+ currcon->confrelid, contuple, &otherrelids,
+ lockmode))
ObjectAddressSet(address, ConstraintRelationId, currcon->oid);
}
@@ -11959,13 +11962,15 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
*/
static bool
ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
LOCKMODE lockmode)
{
Form_pg_constraint currcon;
Oid conoid;
Oid refrelid;
bool changed = false;
+ Relation rel;
/* since this function recurses, it could be driven to stack overflow */
check_stack_depth();
@@ -11974,6 +11979,8 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
conoid = currcon->oid;
refrelid = currcon->confrelid;
+ rel = table_open(currcon->conrelid, lockmode);
+
/*
* Update pg_constraint with the flags from cmdcon.
*
@@ -12019,8 +12026,9 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
*/
if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)
- ATExecAlterChildConstr(cmdcon, conrel, tgrel, rel, contuple,
- otherrelids, lockmode);
+ ATExecAlterChildConstr(cmdcon, conrel, tgrel, fkrelid, pkrelid,
+ contuple, otherrelids, lockmode);
+ table_close(rel, NoLock);
return changed;
}
@@ -12099,7 +12107,8 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
*/
static void
ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
LOCKMODE lockmode)
{
Form_pg_constraint currcon;
@@ -12120,15 +12129,8 @@ ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
true, NULL, 1, &pkey);
while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
- {
- Form_pg_constraint childcon = (Form_pg_constraint) GETSTRUCT(childtup);
- Relation childrel;
-
- childrel = table_open(childcon->conrelid, lockmode);
- ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, childrel, childtup,
- otherrelids, lockmode);
- table_close(childrel, NoLock);
- }
+ ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, fkrelid, pkrelid,
+ childtup, otherrelids, lockmode);
systable_endscan(pscan);
}
--
2.43.5
v10-0005-Remove-hastriggers-flag-check-before-fetching-FK.patchapplication/octet-stream; name=v10-0005-Remove-hastriggers-flag-check-before-fetching-FK.patchDownload
From 9ec5b97b9d6893ddec40ebe54d1555e79b7a28f9 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 7 Oct 2024 12:40:07 +0530
Subject: [PATCH v10 5/7] Remove hastriggers flag check before fetching FK
constraints.
With NOT ENFORCED, foreign key constraints will be created without
triggers. Therefore, the criteria for fetching foreign keys based on
the presence of triggers no longer apply.
----
NOTE: This patch is intended to reduce the diff noise from the main
patch and is not meant to be committed separately. It should be
squashed with the main patch that adds ENFORCED/NOT ENFORCED.
----
---
src/backend/utils/cache/relcache.c | 5 -
src/bin/pg_dump/pg_dump.c | 8 +-
src/bin/psql/describe.c | 226 ++++++++++++++---------------
3 files changed, 108 insertions(+), 131 deletions(-)
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 43219a9629c..485cd2178bc 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4658,11 +4658,6 @@ RelationGetFKeyList(Relation relation)
if (relation->rd_fkeyvalid)
return relation->rd_fkeylist;
- /* Fast path: non-partitioned tables without triggers can't have FKs */
- if (!relation->rd_rel->relhastriggers &&
- relation->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
- return NIL;
-
/*
* We build the list we intend to return (in the caller's context) while
* doing the scan. After successfully completing the scan, we copy that
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 8f73a5df956..166106f363c 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -7841,13 +7841,7 @@ getConstraints(Archive *fout, TableInfo tblinfo[], int numTables)
{
TableInfo *tinfo = &tblinfo[i];
- /*
- * For partitioned tables, foreign keys have no triggers so they must
- * be included anyway in case some foreign keys are defined.
- */
- if ((!tinfo->hastriggers &&
- tinfo->relkind != RELKIND_PARTITIONED_TABLE) ||
- !(tinfo->dobj.dump & DUMP_COMPONENT_DEFINITION))
+ if (!(tinfo->dobj.dump & DUMP_COMPONENT_DEFINITION))
continue;
/* OK, we need info for this table */
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 2ef99971ac0..233b4a38857 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2543,136 +2543,124 @@ describeOneTableDetails(const char *schemaname,
PQclear(result);
}
- /*
- * Print foreign-key constraints (there are none if no triggers,
- * except if the table is partitioned, in which case the triggers
- * appear in the partitions)
- */
- if (tableinfo.hastriggers ||
- tableinfo.relkind == RELKIND_PARTITIONED_TABLE)
+ /* Print foreign-key constraints */
+ if (pset.sversion >= 120000 &&
+ (tableinfo.ispartition || tableinfo.relkind == RELKIND_PARTITIONED_TABLE))
{
- if (pset.sversion >= 120000 &&
- (tableinfo.ispartition || tableinfo.relkind == RELKIND_PARTITIONED_TABLE))
+ /*
+ * Put the constraints defined in this table first, followed
+ * by the constraints defined in ancestor partitioned tables.
+ */
+ printfPQExpBuffer(&buf,
+ "SELECT conrelid = '%s'::pg_catalog.regclass AS sametable,\n"
+ " conname,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef,\n"
+ " conrelid::pg_catalog.regclass AS ontable\n"
+ " FROM pg_catalog.pg_constraint,\n"
+ " pg_catalog.pg_partition_ancestors('%s')\n"
+ " WHERE conrelid = relid AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
+ "ORDER BY sametable DESC, conname;",
+ oid, oid);
+ }
+ else
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT true as sametable, conname,\n"
+ " pg_catalog.pg_get_constraintdef(r.oid, true) as condef,\n"
+ " conrelid::pg_catalog.regclass AS ontable\n"
+ "FROM pg_catalog.pg_constraint r\n"
+ "WHERE r.conrelid = '%s' AND r.contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n",
+ oid);
+
+ if (pset.sversion >= 120000)
+ appendPQExpBufferStr(&buf, " AND conparentid = 0\n");
+ appendPQExpBufferStr(&buf, "ORDER BY conname");
+ }
+
+ result = PSQLexec(buf.data);
+ if (!result)
+ goto error_return;
+ else
+ tuples = PQntuples(result);
+
+ if (tuples > 0)
+ {
+ int i_sametable = PQfnumber(result, "sametable"),
+ i_conname = PQfnumber(result, "conname"),
+ i_condef = PQfnumber(result, "condef"),
+ i_ontable = PQfnumber(result, "ontable");
+
+ printTableAddFooter(&cont, _("Foreign-key constraints:"));
+ for (i = 0; i < tuples; i++)
{
/*
- * Put the constraints defined in this table first, followed
- * by the constraints defined in ancestor partitioned tables.
+ * Print untranslated constraint name and definition. Use
+ * a "TABLE tab" prefix when the constraint is defined in
+ * a parent partitioned table.
*/
- printfPQExpBuffer(&buf,
- "SELECT conrelid = '%s'::pg_catalog.regclass AS sametable,\n"
- " conname,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef,\n"
- " conrelid::pg_catalog.regclass AS ontable\n"
- " FROM pg_catalog.pg_constraint,\n"
- " pg_catalog.pg_partition_ancestors('%s')\n"
- " WHERE conrelid = relid AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
- "ORDER BY sametable DESC, conname;",
- oid, oid);
- }
- else
- {
- printfPQExpBuffer(&buf,
- "SELECT true as sametable, conname,\n"
- " pg_catalog.pg_get_constraintdef(r.oid, true) as condef,\n"
- " conrelid::pg_catalog.regclass AS ontable\n"
- "FROM pg_catalog.pg_constraint r\n"
- "WHERE r.conrelid = '%s' AND r.contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n",
- oid);
-
- if (pset.sversion >= 120000)
- appendPQExpBufferStr(&buf, " AND conparentid = 0\n");
- appendPQExpBufferStr(&buf, "ORDER BY conname");
- }
-
- result = PSQLexec(buf.data);
- if (!result)
- goto error_return;
- else
- tuples = PQntuples(result);
-
- if (tuples > 0)
- {
- int i_sametable = PQfnumber(result, "sametable"),
- i_conname = PQfnumber(result, "conname"),
- i_condef = PQfnumber(result, "condef"),
- i_ontable = PQfnumber(result, "ontable");
-
- printTableAddFooter(&cont, _("Foreign-key constraints:"));
- for (i = 0; i < tuples; i++)
- {
- /*
- * Print untranslated constraint name and definition. Use
- * a "TABLE tab" prefix when the constraint is defined in
- * a parent partitioned table.
- */
- if (strcmp(PQgetvalue(result, i, i_sametable), "f") == 0)
- printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
- PQgetvalue(result, i, i_ontable),
- PQgetvalue(result, i, i_conname),
- PQgetvalue(result, i, i_condef));
- else
- printfPQExpBuffer(&buf, " \"%s\" %s",
- PQgetvalue(result, i, i_conname),
- PQgetvalue(result, i, i_condef));
-
- printTableAddFooter(&cont, buf.data);
- }
- }
- PQclear(result);
- }
-
- /* print incoming foreign-key references */
- if (tableinfo.hastriggers ||
- tableinfo.relkind == RELKIND_PARTITIONED_TABLE)
- {
- if (pset.sversion >= 120000)
- {
- printfPQExpBuffer(&buf,
- "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
- " FROM pg_catalog.pg_constraint c\n"
- " WHERE confrelid IN (SELECT pg_catalog.pg_partition_ancestors('%s')\n"
- " UNION ALL VALUES ('%s'::pg_catalog.regclass))\n"
- " AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
- "ORDER BY conname;",
- oid, oid);
- }
- else
- {
- printfPQExpBuffer(&buf,
- "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
- " FROM pg_catalog.pg_constraint\n"
- " WHERE confrelid = %s AND contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n"
- "ORDER BY conname;",
- oid);
- }
-
- result = PSQLexec(buf.data);
- if (!result)
- goto error_return;
- else
- tuples = PQntuples(result);
-
- if (tuples > 0)
- {
- int i_conname = PQfnumber(result, "conname"),
- i_ontable = PQfnumber(result, "ontable"),
- i_condef = PQfnumber(result, "condef");
-
- printTableAddFooter(&cont, _("Referenced by:"));
- for (i = 0; i < tuples; i++)
- {
+ if (strcmp(PQgetvalue(result, i, i_sametable), "f") == 0)
printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
PQgetvalue(result, i, i_ontable),
PQgetvalue(result, i, i_conname),
PQgetvalue(result, i, i_condef));
+ else
+ printfPQExpBuffer(&buf, " \"%s\" %s",
+ PQgetvalue(result, i, i_conname),
+ PQgetvalue(result, i, i_condef));
- printTableAddFooter(&cont, buf.data);
- }
+ printTableAddFooter(&cont, buf.data);
}
- PQclear(result);
}
+ PQclear(result);
+
+ /* print incoming foreign-key references */
+ if (pset.sversion >= 120000)
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
+ " FROM pg_catalog.pg_constraint c\n"
+ " WHERE confrelid IN (SELECT pg_catalog.pg_partition_ancestors('%s')\n"
+ " UNION ALL VALUES ('%s'::pg_catalog.regclass))\n"
+ " AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
+ "ORDER BY conname;",
+ oid, oid);
+ }
+ else
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
+ " FROM pg_catalog.pg_constraint\n"
+ " WHERE confrelid = %s AND contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n"
+ "ORDER BY conname;",
+ oid);
+ }
+
+ result = PSQLexec(buf.data);
+ if (!result)
+ goto error_return;
+ else
+ tuples = PQntuples(result);
+
+ if (tuples > 0)
+ {
+ int i_conname = PQfnumber(result, "conname"),
+ i_ontable = PQfnumber(result, "ontable"),
+ i_condef = PQfnumber(result, "condef");
+
+ printTableAddFooter(&cont, _("Referenced by:"));
+ for (i = 0; i < tuples; i++)
+ {
+ printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
+ PQgetvalue(result, i, i_ontable),
+ PQgetvalue(result, i, i_conname),
+ PQgetvalue(result, i, i_condef));
+
+ printTableAddFooter(&cont, buf.data);
+ }
+ }
+ PQclear(result);
/* print any row-level policies */
if (pset.sversion >= 90500)
--
2.43.5
v10-0006-Add-support-for-NOT-ENFORCED-in-foreign-key-cons.patchapplication/octet-stream; name=v10-0006-Add-support-for-NOT-ENFORCED-in-foreign-key-cons.patchDownload
From c965d5894d10968c3b89c0efa41bb8b4de7c9a3a Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 20 Jan 2025 21:45:07 +0530
Subject: [PATCH v10 6/7] Add support for NOT ENFORCED in foreign key
constraints.
Typically, when a foreign key (FK) constraint is created on a table,
action and check triggers are added to maintain data integrity. With
this patch, if a constraint is marked as NOT ENFORCED, integrity
checks are no longer required, making these triggers unnecessary.
Consequently, when creating a NOT ENFORCED FK constraint, triggers
will not be created, and the constraint will be marked as NOT VALID.
Similarly, if an existing FK constraint is changed to NOT ENFORCED,
the associated triggers will be dropped, and the constraint will also
be marked as NOT VALID. Conversely, if a NOT ENFORCED FK constraint is
changed to ENFORCED, the necessary triggers will be created, and the
constraint will remain in the NOT VALID state.
----
NOTE: In this patch, the tryAttachPartitionForeignKey() function will
not merge the constraint if the enforcibility differs. This will be
addressed in the next patch.
----
---
doc/src/sgml/advanced.sgml | 13 +
doc/src/sgml/catalogs.sgml | 2 +-
doc/src/sgml/ddl.sgml | 12 +-
doc/src/sgml/ref/alter_table.sgml | 12 +-
doc/src/sgml/ref/create_table.sgml | 2 +-
src/backend/catalog/pg_constraint.c | 5 +-
src/backend/catalog/sql_features.txt | 2 +-
src/backend/commands/tablecmds.c | 387 ++++++++++++++++------
src/backend/parser/gram.y | 7 +-
src/backend/parser/parse_utilcmd.c | 14 +-
src/backend/utils/cache/relcache.c | 1 +
src/include/utils/rel.h | 3 +
src/test/regress/expected/constraints.out | 8 +-
src/test/regress/expected/foreign_key.out | 130 +++++++-
src/test/regress/sql/foreign_key.sql | 93 +++++-
15 files changed, 562 insertions(+), 129 deletions(-)
diff --git a/doc/src/sgml/advanced.sgml b/doc/src/sgml/advanced.sgml
index 755c9f14850..b7d90de69d7 100644
--- a/doc/src/sgml/advanced.sgml
+++ b/doc/src/sgml/advanced.sgml
@@ -125,6 +125,19 @@ ERROR: insert or update on table "weather" violates foreign key constraint "wea
DETAIL: Key (city)=(Berkeley) is not present in table "cities".
</screen>
</para>
+ <para>
+ If, for some reason, you do not want to enforce this constraint and wish to
+ avoid the error, you can modify the foreign key constraint to <literal>NOT ENFORCED</literal>,
+ or specify it in the <link linkend="sql-createtable"><command>CREATE TABLE</command></link>.
+ </para>
+ <para>
+ Let's change the constraint to <literal>NOT ENFORCED</literal>, and when you
+ attempt to reinsert the same record, it will not result in any error.
+<programlisting>
+ALTER TABLE weather ALTER CONSTRAINT weather_city_fkey NOT ENFORCED;
+INSERT INTO weather VALUES ('Berkeley', 45, 53, 0.0, '1994-11-28');
+</programlisting>
+ </para>
<para>
The behavior of foreign keys can be finely tuned to your
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index d3036c5ba9d..72b68cb8d2d 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2597,7 +2597,7 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l
</para>
<para>
Is the constraint enforced?
- Currently, can be false only for CHECK constraints
+ Currently, can be false only for foreign keys and CHECK constraints
</para></entry>
</row>
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index dea04d64db6..d3377503981 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -1054,11 +1054,13 @@ CREATE TABLE example (
</indexterm>
<para>
- A foreign key constraint specifies that the values in a column (or
- a group of columns) must match the values appearing in some row
- of another table.
- We say this maintains the <firstterm>referential
- integrity</firstterm> between two related tables.
+ A foreign key constraint with <literal>ENFORCED</literal>, the default
+ setting, specifies that the values in a column (or a group of columns)
+ must match the values appearing in some row of another table. We say this
+ maintains the <firstterm>referential integrity</firstterm> between two
+ related tables. If a foreign key constraint is created with <literal>NOT ENFORCED</literal>,
+ the check for aforementioned <firstterm>referential integrity</firstterm>
+ will not be performed.
</para>
<para>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index f9576da435e..8d4d0dec9d2 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -58,7 +58,7 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable>
ALTER [ COLUMN ] <replaceable class="parameter">column_name</replaceable> SET COMPRESSION <replaceable class="parameter">compression_method</replaceable>
ADD <replaceable class="parameter">table_constraint</replaceable> [ NOT VALID ]
ADD <replaceable class="parameter">table_constraint_using_index</replaceable>
- ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+ ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]
VALIDATE CONSTRAINT <replaceable class="parameter">constraint_name</replaceable>
DROP CONSTRAINT [ IF EXISTS ] <replaceable class="parameter">constraint_name</replaceable> [ RESTRICT | CASCADE ]
DISABLE TRIGGER [ <replaceable class="parameter">trigger_name</replaceable> | ALL | USER ]
@@ -553,6 +553,13 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
This form alters the attributes of a constraint that was previously
created. Currently only foreign key constraints may be altered.
</para>
+ <para>
+ Note that when a constraint is changed to <literal>NOT ENFORCED</literal>,
+ it is also marked as <literal>NOT VALID</literal>. While this might seem
+ unnecessary, it is done for consistency and should be set accordingly.
+ This ensures that if the constraint is later changed to <literal>ENFORCED</literal>,
+ it will automatically be in the correct <literal>NOT VALID</literal> state.
+ </para>
</listitem>
</varlistentry>
@@ -563,7 +570,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
This form validates a foreign key or check constraint that was
previously created as <literal>NOT VALID</literal>, by scanning the
table to ensure there are no rows for which the constraint is not
- satisfied. Nothing happens if the constraint is already marked valid.
+ satisfied. If the constraint is not enforced, an error is thrown.
+ Nothing happens if the constraint is already marked valid.
(See <xref linkend="sql-altertable-notes"/> below for an explanation
of the usefulness of this command.)
</para>
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index 2237321cb4f..5071dfeabf9 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -1401,7 +1401,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</para>
<para>
- This is currently only supported for <literal>CHECK</literal>
+ This is currently only supported for foreign key and <literal>CHECK</literal>
constraints.
</para>
</listitem>
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index bbf4742e18c..1d7b29de40d 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -100,8 +100,9 @@ CreateConstraintEntry(const char *constraintName,
ObjectAddresses *addrs_auto;
ObjectAddresses *addrs_normal;
- /* Only CHECK constraint can be not enforced */
- Assert(isEnforced || constraintType == CONSTRAINT_CHECK);
+ /* Only CHECK or FOREIGN KEY constraint can be not enforced */
+ Assert(isEnforced || constraintType == CONSTRAINT_CHECK ||
+ constraintType == CONSTRAINT_FOREIGN);
/* NOT ENFORCED constraint must be NOT VALID */
Assert(isEnforced || !isValidated);
diff --git a/src/backend/catalog/sql_features.txt b/src/backend/catalog/sql_features.txt
index 2f250d2c57b..d49471bbdf2 100644
--- a/src/backend/catalog/sql_features.txt
+++ b/src/backend/catalog/sql_features.txt
@@ -281,7 +281,7 @@ F461 Named character sets NO
F471 Scalar subquery values YES
F481 Expanded NULL predicate YES
F491 Constraint management YES
-F492 Optional table constraint enforcement NO check constraints only
+F492 Optional table constraint enforcement NO foreign key and check constraints only
F501 Features and conformance views YES
F501 Features and conformance views 01 SQL_FEATURES view YES
F501 Features and conformance views 02 SQL_SIZING view YES
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 2164eaf017b..5c1b44c387d 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -391,11 +391,23 @@ static void AlterSeqNamespaces(Relation classRel, Relation rel,
LOCKMODE lockmode);
static ObjectAddress ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd,
bool recurse, bool recursing, LOCKMODE lockmode);
-static bool ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
- const Oid fkrelid, const Oid pkrelid,
- HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode);
-static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
+static bool ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel,
+ Relation tgrel, const Oid fkrelid,
+ const Oid pkrelid, HeapTuple contuple,
+ List **otherrelids, LOCKMODE lockmode,
+ Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger);
+static void ATExecAlterConstrEnforceability(Constraint *cmdcon, Relation conrel,
+ Relation tgrel, const Oid fkrelid,
+ const Oid pkrelid, HeapTuple contuple,
+ List **otherrelids, LOCKMODE lockmode,
+ Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger);
+static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Oid fkrel,
bool deferrable, bool initdeferred,
List **otherrelids);
static void ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
@@ -10455,7 +10467,7 @@ addFkConstraint(addFkConstraintSides fkside,
CONSTRAINT_FOREIGN,
fkconstraint->deferrable,
fkconstraint->initdeferred,
- true, /* Is Enforced */
+ fkconstraint->is_enforced,
fkconstraint->initially_valid,
parentConstr,
RelationGetRelid(rel),
@@ -10573,21 +10585,23 @@ addFkRecurseReferenced(Constraint *fkconstraint, Relation rel,
Oid parentDelTrigger, Oid parentUpdTrigger,
bool with_period)
{
- Oid deleteTriggerOid,
- updateTriggerOid;
+ Oid deleteTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
Assert(CheckRelationLockedByMe(pkrel, ShareRowExclusiveLock, true));
Assert(CheckRelationLockedByMe(rel, ShareRowExclusiveLock, true));
/*
- * Create the action triggers that enforce the constraint.
+ * Create action triggers to enforce the constraint, or skip them if the
+ * constraint is not enforced.
*/
- createForeignKeyActionTriggers(RelationGetRelid(rel),
- RelationGetRelid(pkrel),
- fkconstraint,
- parentConstr, indexOid,
- parentDelTrigger, parentUpdTrigger,
- &deleteTriggerOid, &updateTriggerOid);
+ if (fkconstraint->is_enforced)
+ createForeignKeyActionTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
+ fkconstraint,
+ parentConstr, indexOid,
+ parentDelTrigger, parentUpdTrigger,
+ &deleteTriggerOid, &updateTriggerOid);
/*
* If the referenced table is partitioned, recurse on ourselves to handle
@@ -10708,8 +10722,8 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
Oid parentInsTrigger, Oid parentUpdTrigger,
bool with_period)
{
- Oid insertTriggerOid,
- updateTriggerOid;
+ Oid insertTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
Assert(OidIsValid(parentConstr));
Assert(CheckRelationLockedByMe(rel, ShareRowExclusiveLock, true));
@@ -10721,29 +10735,32 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
errmsg("foreign key constraints are not supported on foreign tables")));
/*
- * Add the check triggers to it and, if necessary, schedule it to be
- * checked in Phase 3.
+ * Add check triggers if the constraint is enforced, and if needed,
+ * schedule them to be checked in Phase 3.
*
* If the relation is partitioned, drill down to do it to its partitions.
*/
- createForeignKeyCheckTriggers(RelationGetRelid(rel),
- RelationGetRelid(pkrel),
- fkconstraint,
- parentConstr,
- indexOid,
- parentInsTrigger, parentUpdTrigger,
- &insertTriggerOid, &updateTriggerOid);
+ if (fkconstraint->is_enforced)
+ createForeignKeyCheckTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
+ fkconstraint,
+ parentConstr,
+ indexOid,
+ parentInsTrigger, parentUpdTrigger,
+ &insertTriggerOid, &updateTriggerOid);
if (rel->rd_rel->relkind == RELKIND_RELATION)
{
/*
* Tell Phase 3 to check that the constraint is satisfied by existing
- * rows. We can skip this during table creation, when requested
- * explicitly by specifying NOT VALID in an ADD FOREIGN KEY command,
- * and when we're recreating a constraint following a SET DATA TYPE
- * operation that did not impugn its validity.
+ * rows. We can skip this during table creation, when constraint is
+ * specified as NOT ENFORCED, when requested explicitly by specifying
+ * NOT VALID in an ADD FOREIGN KEY command, and when we're recreating
+ * a constraint following a SET DATA TYPE operation that did not
+ * impugn its validity.
*/
- if (wqueue && !old_check_ok && !fkconstraint->skip_validation)
+ if (wqueue && !old_check_ok && !fkconstraint->skip_validation &&
+ fkconstraint->is_enforced)
{
NewConstraint *newcon;
AlteredTableInfo *tab;
@@ -10978,8 +10995,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
AttrNumber confdelsetcols[INDEX_MAX_KEYS];
Constraint *fkconstraint;
ObjectAddress address;
- Oid deleteTriggerOid,
- updateTriggerOid;
+ Oid deleteTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(constrOid));
if (!HeapTupleIsValid(tuple))
@@ -11039,8 +11056,9 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
fkconstraint->fk_del_set_cols = NIL;
fkconstraint->old_conpfeqop = NIL;
fkconstraint->old_pktable_oid = InvalidOid;
+ fkconstraint->is_enforced = constrForm->conenforced;
fkconstraint->skip_validation = false;
- fkconstraint->initially_valid = true;
+ fkconstraint->initially_valid = constrForm->convalidated;
/* set up colnames that are used to generate the constraint name */
for (int i = 0; i < numfks; i++)
@@ -11068,9 +11086,10 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
* parent OIDs for similar triggers that will be created on the
* partition in addFkRecurseReferenced().
*/
- GetForeignKeyActionTriggers(trigrel, constrOid,
- constrForm->confrelid, constrForm->conrelid,
- &deleteTriggerOid, &updateTriggerOid);
+ if (constrForm->conenforced)
+ GetForeignKeyActionTriggers(trigrel, constrOid,
+ constrForm->confrelid, constrForm->conrelid,
+ &deleteTriggerOid, &updateTriggerOid);
/* Add this constraint ... */
address = addFkConstraint(addFkReferencedSide,
@@ -11236,17 +11255,18 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
mapped_conkey[i] = attmap->attnums[conkey[i] - 1];
/*
- * Get the "check" triggers belonging to the constraint to pass as
- * parent OIDs for similar triggers that will be created on the
- * partition in addFkRecurseReferencing(). They are also passed to
- * tryAttachPartitionForeignKey() below to simply assign as parents to
- * the partition's existing "check" triggers, that is, if the
- * corresponding constraints is deemed attachable to the parent
- * constraint.
+ * Get the "check" triggers belonging to the constraint, if it is
+ * enforced, to pass as parent OIDs for similar triggers that will be
+ * created on the partition in addFkRecurseReferencing(). They are
+ * also passed to tryAttachPartitionForeignKey() below to simply
+ * assign as parents to the partition's existing "check" triggers,
+ * that is, if the corresponding constraints is deemed attachable to
+ * the parent constraint.
*/
- GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
- constrForm->confrelid, constrForm->conrelid,
- &insertTriggerOid, &updateTriggerOid);
+ if (constrForm->conenforced)
+ GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
+ constrForm->confrelid, constrForm->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
/*
* Before creating a new constraint, see whether any existing FKs are
@@ -11299,6 +11319,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
fkconstraint->fk_del_set_cols = NIL;
fkconstraint->old_conpfeqop = NIL;
fkconstraint->old_pktable_oid = InvalidOid;
+ fkconstraint->is_enforced = constrForm->conenforced;
fkconstraint->skip_validation = false;
fkconstraint->initially_valid = constrForm->convalidated;
for (int i = 0; i < numfks; i++)
@@ -11422,6 +11443,7 @@ tryAttachPartitionForeignKey(List **wqueue,
if (OidIsValid(partConstr->conparentid) ||
partConstr->condeferrable != parentConstr->condeferrable ||
partConstr->condeferred != parentConstr->condeferred ||
+ partConstr->conenforced != parentConstr->conenforced ||
partConstr->confupdtype != parentConstr->confupdtype ||
partConstr->confdeltype != parentConstr->confdeltype ||
partConstr->confmatchtype != parentConstr->confmatchtype)
@@ -11464,6 +11486,7 @@ AttachPartitionForeignKey(List **wqueue,
Form_pg_constraint partConstr;
Oid partConstrFrelid;
Oid partConstrRelid;
+ bool parentConstrIsEnforced;
bool parentConstrIsValid;
bool partConstrIsValid;
Oid insertTriggerOid,
@@ -11475,6 +11498,7 @@ AttachPartitionForeignKey(List **wqueue,
if (!HeapTupleIsValid(parentConstrTup))
elog(ERROR, "cache lookup failed for constraint %u", parentConstrOid);
parentConstr = (Form_pg_constraint) GETSTRUCT(parentConstrTup);
+ parentConstrIsEnforced = parentConstr->conenforced;
parentConstrIsValid = parentConstr->convalidated;
/* Fetch the child constraint tuple */
@@ -11483,7 +11507,7 @@ AttachPartitionForeignKey(List **wqueue,
if (!HeapTupleIsValid(partcontup))
elog(ERROR, "cache lookup failed for constraint %u", partConstrOid);
partConstr = (Form_pg_constraint) GETSTRUCT(partcontup);
- partConstrIsValid = parentConstr->convalidated;
+ partConstrIsValid = partConstr->convalidated;
partConstrFrelid = partConstr->confrelid;
partConstrRelid = partConstr->conrelid;
@@ -11504,17 +11528,21 @@ AttachPartitionForeignKey(List **wqueue,
/*
* Like the constraint, attach partition's "check" triggers to the
- * corresponding parent triggers.
+ * corresponding parent triggers if the constraint is enforced.
+ * Non-enforced constraints do not have these triggers.
*/
- GetForeignKeyCheckTriggers(trigrel,
- partConstrOid, partConstrFrelid, partConstrRelid,
- &insertTriggerOid, &updateTriggerOid);
- Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
- TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
- RelationGetRelid(partition));
- Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
- TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
- RelationGetRelid(partition));
+ if (parentConstrIsEnforced)
+ {
+ GetForeignKeyCheckTriggers(trigrel,
+ partConstrOid, partConstrFrelid, partConstrRelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
+ RelationGetRelid(partition));
+ Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
+ RelationGetRelid(partition));
+ }
/*
* If the referenced table is partitioned, then the partition we're
@@ -11642,6 +11670,10 @@ RemoveInheritedConstraint(Relation conrel, Relation trigrel, Oid conrelid,
*
* The subroutine for tryAttachPartitionForeignKey handles the deletion of
* action triggers for the foreign key constraint.
+ *
+ * If valid confrelid and conrelid values are not provided, the respective
+ * trigger check will be skipped, and the trigger will be considered for
+ * removal.
*/
static void
DropForeignKeyConstraintTriggers(Relation trigrel, Oid conoid, Oid confrelid,
@@ -11662,10 +11694,27 @@ DropForeignKeyConstraintTriggers(Relation trigrel, Oid conoid, Oid confrelid,
Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
ObjectAddress trigger;
- if (trgform->tgconstrrelid != conrelid)
+ /* Invalid if trigger is not for a referential integrity constraint */
+ if (!OidIsValid(trgform->tgconstrrelid))
continue;
- if (trgform->tgrelid != confrelid)
+ if (OidIsValid(conrelid) && trgform->tgconstrrelid != conrelid)
continue;
+ if (OidIsValid(confrelid) && trgform->tgrelid != confrelid)
+ continue;
+
+ /* We should be droping trigger related to foreign ke constraint */
+ Assert(trgform->tgfoid == F_RI_FKEY_CHECK_INS ||
+ trgform->tgfoid == F_RI_FKEY_CHECK_UPD ||
+ trgform->tgfoid == F_RI_FKEY_CASCADE_DEL ||
+ trgform->tgfoid == F_RI_FKEY_CASCADE_UPD ||
+ trgform->tgfoid == F_RI_FKEY_RESTRICT_DEL ||
+ trgform->tgfoid == F_RI_FKEY_RESTRICT_UPD ||
+ trgform->tgfoid == F_RI_FKEY_SETNULL_DEL ||
+ trgform->tgfoid == F_RI_FKEY_SETNULL_UPD ||
+ trgform->tgfoid == F_RI_FKEY_SETDEFAULT_DEL ||
+ trgform->tgfoid == F_RI_FKEY_SETDEFAULT_UPD ||
+ trgform->tgfoid == F_RI_FKEY_NOACTION_DEL ||
+ trgform->tgfoid == F_RI_FKEY_NOACTION_UPD);
/*
* The constraint is originally set up to contain this trigger as an
@@ -11925,11 +11974,13 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
address = InvalidObjectAddress;
if (currcon->condeferrable != cmdcon->deferrable ||
currcon->condeferred != cmdcon->initdeferred ||
+ currcon->conenforced != cmdcon->is_enforced ||
rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
if (ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, currcon->conrelid,
currcon->confrelid, contuple, &otherrelids,
- lockmode))
+ lockmode, InvalidOid, InvalidOid, InvalidOid,
+ InvalidOid))
ObjectAddressSet(address, ConstraintRelationId, currcon->oid);
}
@@ -11964,7 +12015,10 @@ static bool
ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
const Oid fkrelid, const Oid pkrelid,
HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode)
+ LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger)
{
Form_pg_constraint currcon;
Oid conoid;
@@ -11988,7 +12042,8 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
* silently do nothing.
*/
if (currcon->condeferrable != cmdcon->deferrable ||
- currcon->condeferred != cmdcon->initdeferred)
+ currcon->condeferred != cmdcon->initdeferred ||
+ currcon->conenforced != cmdcon->is_enforced)
{
HeapTuple copyTuple;
Form_pg_constraint copy_con;
@@ -11997,6 +12052,18 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
copy_con->condeferrable = cmdcon->deferrable;
copy_con->condeferred = cmdcon->initdeferred;
+ copy_con->conenforced = cmdcon->is_enforced;
+
+ /*
+ * NB: The convalidated status is irrelevant when the constraint is
+ * set to NOT ENFORCED, but for consistency, it should be set
+ * accordingly. This ensures that if the constraint is later changed
+ * to ENFORCED, it will automatically be in the correct NOT VALID
+ * state.
+ */
+ if (!cmdcon->is_enforced)
+ copy_con->convalidated = false;
+
CatalogTupleUpdate(conrel, ©Tuple->t_self, copyTuple);
InvokeObjectPostAlterHook(ConstraintRelationId,
@@ -12007,32 +12074,158 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
/* Make new constraint flags visible to others */
CacheInvalidateRelcache(rel);
+ }
+ if (currcon->conenforced != cmdcon->is_enforced)
+ {
+ ATExecAlterConstrEnforceability(cmdcon, conrel, tgrel, fkrelid, pkrelid,
+ contuple, otherrelids, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
+ }
+ else
+ {
/*
* Now we need to update the multiple entries in pg_trigger that
* implement the constraint.
*/
- AlterConstrTriggerDeferrability(conoid, tgrel, rel, cmdcon->deferrable,
- cmdcon->initdeferred, otherrelids);
+ AlterConstrTriggerDeferrability(conoid, tgrel,
+ RelationGetRelid(rel),
+ cmdcon->deferrable,
+ cmdcon->initdeferred,
+ otherrelids);
+
+ /*
+ * If the table at either end of the constraint is partitioned, we
+ * need to recurse and handle every constraint that is a child of this
+ * one.
+ *
+ * (This assumes that the recurse flag is forcibly set for partitioned
+ * tables, and not set for legacy inheritance, though we don't check
+ * for that here.)
+ */
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)
+ ATExecAlterChildConstr(cmdcon, conrel, tgrel, fkrelid, pkrelid,
+ contuple, otherrelids, lockmode);
}
- /*
- * If the table at either end of the constraint is partitioned, we need to
- * recurse and handle every constraint that is a child of this one.
- *
- * (This assumes that the recurse flag is forcibly set for partitioned
- * tables, and not set for legacy inheritance, though we don't check for
- * that here.)
- */
- if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
- get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)
- ATExecAlterChildConstr(cmdcon, conrel, tgrel, fkrelid, pkrelid,
- contuple, otherrelids, lockmode);
table_close(rel, NoLock);
return changed;
}
+/*
+ * A subroutine of ATExecAlterConstrRecurse that updates the enforceability of
+ * a foreign key constraint. Depending on whether the constraint is being set
+ * to enforced or not enforced, it creates or drops the trigger accordingly.
+ *
+ * The arguments to this function have the same meaning as the arguments to
+ * ATExecAlterConstrRecurse.
+ */
+static void
+ATExecAlterConstrEnforceability(Constraint *cmdcon, Relation conrel,
+ Relation tgrel, const Oid fkrelid,
+ const Oid pkrelid, HeapTuple contuple,
+ List **otherrelids, LOCKMODE lockmode,
+ Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger)
+{
+ Form_pg_constraint currcon;
+ Oid conoid;
+
+ currcon = (Form_pg_constraint) GETSTRUCT(contuple);
+ conoid = currcon->oid;
+
+ /* Drop triggers */
+ if (!cmdcon->is_enforced)
+ {
+ /*
+ * When setting a constraint to NOT ENFORCED, the constraint triggers
+ * need to be dropped. Therefore, we must process the child relations
+ * first, followed by the parent, to account for dependencies.
+ */
+ if (get_rel_relkind(currcon->conrelid) == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE)
+ ATExecAlterChildConstr(cmdcon, conrel, tgrel, fkrelid, pkrelid,
+ contuple, otherrelids, lockmode);
+
+ /* Drop all the triggers */
+ DropForeignKeyConstraintTriggers(tgrel, conoid, InvalidOid, InvalidOid);
+ }
+ else /* Create triggers */
+ {
+ Oid ReferencedDelTriggerOid = InvalidOid,
+ ReferencedUpdTriggerOid = InvalidOid,
+ ReferencingInsTriggerOid = InvalidOid,
+ ReferencingUpdTriggerOid = InvalidOid;
+
+ /* Update the foreign key action required for trigger creation. */
+ cmdcon->fk_matchtype = currcon->confmatchtype;
+ cmdcon->fk_upd_action = currcon->confupdtype;
+ cmdcon->fk_del_action = currcon->confdeltype;
+
+ /* Create referenced triggers */
+ if (currcon->conrelid == fkrelid)
+ createForeignKeyActionTriggers(currcon->conrelid,
+ currcon->confrelid,
+ cmdcon,
+ conoid,
+ currcon->conindid,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ &ReferencedDelTriggerOid,
+ &ReferencedUpdTriggerOid);
+
+ /* Create referencing triggers */
+ if (currcon->confrelid == pkrelid)
+ createForeignKeyCheckTriggers(currcon->conrelid,
+ pkrelid,
+ cmdcon,
+ conoid,
+ currcon->conindid,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger,
+ &ReferencingInsTriggerOid,
+ &ReferencingUpdTriggerOid);
+
+ /*
+ * If the table at either end of the constraint is partitioned, we
+ * need to recurse and create triggers for each constraint that is a
+ * child of this one.
+ */
+ if (get_rel_relkind(currcon->conrelid) == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE)
+ {
+ ScanKeyData pkey;
+ SysScanDesc pscan;
+ HeapTuple childtup;
+
+ ScanKeyInit(&pkey,
+ Anum_pg_constraint_conparentid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+
+ pscan = systable_beginscan(conrel, ConstraintParentIndexId,
+ true, NULL, 1, &pkey);
+
+ while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
+ ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, fkrelid, pkrelid,
+ childtup, otherrelids, lockmode,
+ ReferencedDelTriggerOid,
+ ReferencedUpdTriggerOid,
+ ReferencingInsTriggerOid,
+ ReferencingUpdTriggerOid);
+
+ systable_endscan(pscan);
+ }
+ }
+}
+
/*
* A subroutine of ATExecAlterConstrRecurse that updated constraint trigger's
* deferrability.
@@ -12041,7 +12234,7 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
* ATExecAlterConstrRecurse.
*/
static void
-AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
+AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Oid fkrel,
bool deferrable, bool initdeferred,
List **otherrelids)
{
@@ -12067,7 +12260,7 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
* other rels that don't have a trigger whose properties change, but
* let's be conservative.)
*/
- if (tgform->tgrelid != RelationGetRelid(rel))
+ if (tgform->tgrelid != fkrel)
*otherrelids = list_append_unique_oid(*otherrelids,
tgform->tgrelid);
@@ -12130,7 +12323,8 @@ ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, fkrelid, pkrelid,
- childtup, otherrelids, lockmode);
+ childtup, otherrelids, lockmode, InvalidOid,
+ InvalidOid, InvalidOid, InvalidOid);
systable_endscan(pscan);
}
@@ -20154,8 +20348,6 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
Form_pg_constraint conform;
- Oid insertTriggerOid,
- updateTriggerOid;
contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
if (!HeapTupleIsValid(contup))
@@ -20178,17 +20370,25 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
/*
* Also, look up the partition's "check" triggers corresponding to the
- * constraint being detached and detach them from the parent triggers.
+ * enforced constraint being detached and detach them from the parent
+ * triggers. Non-enforced constraints do not have these triggers;
+ * therefore, this step is not needed.
*/
- GetForeignKeyCheckTriggers(trigrel,
- fk->conoid, fk->confrelid, fk->conrelid,
- &insertTriggerOid, &updateTriggerOid);
- Assert(OidIsValid(insertTriggerOid));
- TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
- RelationGetRelid(partRel));
- Assert(OidIsValid(updateTriggerOid));
- TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
- RelationGetRelid(partRel));
+ if (fk->conenforced)
+ {
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ Assert(OidIsValid(updateTriggerOid));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ }
/*
* Lastly, create the action triggers on the referenced table, using
@@ -20228,8 +20428,9 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
fkconstraint->conname = pstrdup(NameStr(conform->conname));
fkconstraint->deferrable = conform->condeferrable;
fkconstraint->initdeferred = conform->condeferred;
+ fkconstraint->is_enforced = conform->conenforced;
fkconstraint->skip_validation = true;
- fkconstraint->initially_valid = true;
+ fkconstraint->initially_valid = conform->convalidated;
/* a few irrelevant fields omitted here */
fkconstraint->pktable = NULL;
fkconstraint->fk_attrs = NIL;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index d7f9c00c409..37082bc511e 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -2666,7 +2666,7 @@ alter_table_cmd:
processCASbits($4, @4, "ALTER CONSTRAINT statement",
&c->deferrable,
&c->initdeferred,
- NULL, NULL, NULL, yyscanner);
+ &c->is_enforced, NULL, NULL, yyscanner);
$$ = (Node *) n;
}
/* ALTER TABLE <name> VALIDATE CONSTRAINT ... */
@@ -4035,6 +4035,7 @@ ColConstraintElem:
n->is_enforced = true;
n->skip_validation = false;
n->initially_valid = true;
+ n->is_enforced = true;
$$ = (Node *) n;
}
;
@@ -4309,8 +4310,8 @@ ConstraintElem:
n->fk_del_set_cols = ($11)->deleteAction->cols;
processCASbits($12, @12, "FOREIGN KEY",
&n->deferrable, &n->initdeferred,
- NULL, &n->skip_validation, NULL,
- yyscanner);
+ &n->is_enforced, &n->skip_validation,
+ NULL, yyscanner);
n->initially_valid = !n->skip_validation;
$$ = (Node *) n;
}
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index ca028d2a66d..170f8032d59 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -2961,8 +2961,10 @@ transformFKConstraints(CreateStmtContext *cxt,
/*
* If CREATE TABLE or adding a column with NULL default, we can safely
- * skip validation of FK constraints, and nonetheless mark them valid.
- * (This will override any user-supplied NOT VALID flag.)
+ * skip validation of FK constraints, and mark them as valid based on the
+ * constraint enforcement flag, since NOT ENFORCED constraints must always
+ * be marked as NOT VALID. (This will override any user-supplied NOT VALID
+ * flag.)
*/
if (skipValidation)
{
@@ -2971,7 +2973,7 @@ transformFKConstraints(CreateStmtContext *cxt,
Constraint *constraint = (Constraint *) lfirst(fkclist);
constraint->skip_validation = true;
- constraint->initially_valid = true;
+ constraint->initially_valid = constraint->is_enforced;
}
}
@@ -3966,7 +3968,8 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
case CONSTR_ATTR_ENFORCED:
if (lastprimarycon == NULL ||
- lastprimarycon->contype != CONSTR_CHECK)
+ (lastprimarycon->contype != CONSTR_CHECK &&
+ lastprimarycon->contype != CONSTR_FOREIGN))
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("misplaced ENFORCED clause"),
@@ -3982,7 +3985,8 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
case CONSTR_ATTR_NOT_ENFORCED:
if (lastprimarycon == NULL ||
- lastprimarycon->contype != CONSTR_CHECK)
+ (lastprimarycon->contype != CONSTR_CHECK &&
+ lastprimarycon->contype != CONSTR_FOREIGN))
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("misplaced NOT ENFORCED clause"),
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 485cd2178bc..cb3c19c7448 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4689,6 +4689,7 @@ RelationGetFKeyList(Relation relation)
info->conoid = constraint->oid;
info->conrelid = constraint->conrelid;
info->confrelid = constraint->confrelid;
+ info->conenforced = constraint->conenforced;
DeconstructFkConstraintRow(htup, &info->nkeys,
info->conkey,
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 33d1e4a4e2e..c707ebec7b6 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -284,6 +284,9 @@ typedef struct ForeignKeyCacheInfo
/* number of columns in the foreign key */
int nkeys;
+ /* Is enforced ? */
+ bool conenforced;
+
/*
* these arrays each have nkeys valid entries:
*/
diff --git a/src/test/regress/expected/constraints.out b/src/test/regress/expected/constraints.out
index 692a69fe457..f8767c5cacf 100644
--- a/src/test/regress/expected/constraints.out
+++ b/src/test/regress/expected/constraints.out
@@ -746,13 +746,9 @@ LINE 1: CREATE TABLE UNIQUE_NOTEN_TBL(i int UNIQUE NOT ENFORCED);
^
-- XXX: error message is misleading here
ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
-ERROR: ALTER CONSTRAINT statement constraints cannot be marked ENFORCED
-LINE 1: ...TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
- ^
+ERROR: constraint "unique_tbl_i_key" of relation "unique_tbl" is not a foreign key constraint
ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORCED;
-ERROR: ALTER CONSTRAINT statement constraints cannot be marked NOT ENFORCED
-LINE 1: ...ABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORC...
- ^
+ERROR: constraint "unique_tbl_i_key" of relation "unique_tbl" is not a foreign key constraint
DROP TABLE unique_tbl;
--
-- EXCLUDE constraints
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index f617d519aef..9f6f22dd988 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -1,12 +1,44 @@
--
-- FOREIGN KEY
--
--- MATCH FULL
+-- NOT ENFORCED
--
-- First test, check and cascade
--
CREATE TABLE PKTABLE ( ptest1 int PRIMARY KEY, ptest2 text );
-CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE, ftest2 int );
+CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE NOT ENFORCED,
+ ftest2 int );
+-- Inserting into the foreign key table will not result in an error, even if
+-- there is no matching key in the referenced table.
+INSERT INTO FKTABLE VALUES (1, 2);
+INSERT INTO FKTABLE VALUES (2, 3);
+-- Check FKTABLE
+SELECT * FROM FKTABLE;
+ ftest1 | ftest2
+--------+--------
+ 1 | 2
+ 2 | 3
+(2 rows)
+
+-- Changing it back to ENFORCED will leave the constraint in the NOT VALID state
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+-- Which needs to be explicitly validated.
+ALTER TABLE FKTABLE VALIDATE CONSTRAINT fktable_ftest1_fkey;
+ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_fkey"
+DETAIL: Key (ftest1)=(1) is not present in table "pktable".
+-- Remove any existing rows that violate the constraint, then attempt to change
+-- it.
+TRUNCATE FKTABLE;
+ALTER TABLE FKTABLE VALIDATE CONSTRAINT fktable_ftest1_fkey;
+-- Any further inserts will fail due to the enforcement.
+INSERT INTO FKTABLE VALUES (3, 4);
+ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_fkey"
+DETAIL: Key (ftest1)=(3) is not present in table "pktable".
+--
+-- MATCH FULL
+--
+-- First test, check and cascade
+--
-- Insert test data into PKTABLE
INSERT INTO PKTABLE VALUES (1, 'Test1');
INSERT INTO PKTABLE VALUES (2, 'Test2');
@@ -351,6 +383,8 @@ INSERT INTO FKTABLE VALUES (1, NULL);
ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL;
ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_ftest2_fkey"
DETAIL: MATCH FULL does not allow mixing of null and nonnull key values.
+-- NOT ENFORCED will bypass the initial check.
+ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL NOT ENFORCED;
DROP TABLE FKTABLE;
DROP TABLE PKTABLE;
-- MATCH SIMPLE
@@ -1276,13 +1310,28 @@ INSERT INTO fktable VALUES (0, 20);
ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_fk_fkey"
DETAIL: Key (fk)=(20) is not present in table "pktable".
COMMIT;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT ENFORCED;
+BEGIN;
+-- doesn't match FK, no error.
+UPDATE pktable SET id = 10 WHERE id = 5;
+-- doesn't match PK, no error.
+INSERT INTO fktable VALUES (0, 20);
+ROLLBACK;
-- try additional syntax
-ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE ENFORCED;
-- illegal option
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE INITIALLY DEFERRED;
ERROR: constraint declared INITIALLY DEFERRED must be DEFERRABLE
LINE 1: ...e ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE INITIALLY ...
^
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORCED;
+ERROR: conflicting constraint properties
+LINE 1: ...fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORC...
+ ^
+CREATE TEMP TABLE fktable2 (fk int references pktable ENFORCED NOT ENFORCED);
+ERROR: multiple ENFORCED/NOT ENFORCED clauses not allowed
+LINE 1: ...ABLE fktable2 (fk int references pktable ENFORCED NOT ENFORC...
+ ^
-- test order of firing of FK triggers when several RI-induced changes need to
-- be made to the same row. This was broken by subtransaction-related
-- changes in 8.0.
@@ -1650,10 +1699,14 @@ ALTER TABLE fk_partitioned_fk DROP COLUMN fdrop1;
CREATE TABLE fk_partitioned_fk_1 (fdrop1 int, fdrop2 int, a int, fdrop3 int, b int);
ALTER TABLE fk_partitioned_fk_1 DROP COLUMN fdrop1, DROP COLUMN fdrop2, DROP COLUMN fdrop3;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FROM (0,0) TO (1000,1000);
-ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk;
+ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
+ALTER TABLE fk_partitioned_fk_2 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
+-- Changing to ENFORCED does not alter the validation status; it must be explicitly validated.
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+ALTER TABLE fk_partitioned_fk VALIDATE CONSTRAINT fk_partitioned_fk_a_b_fkey ;
CREATE TABLE fk_partitioned_fk_3 (fdrop1 int, fdrop2 int, fdrop3 int, fdrop4 int, b int, a int)
PARTITION BY HASH (a);
ALTER TABLE fk_partitioned_fk_3 DROP COLUMN fdrop1, DROP COLUMN fdrop2,
@@ -1676,10 +1729,10 @@ INSERT INTO fk_partitioned_fk_1 (a,b) VALUES (500, 501);
ERROR: insert or update on table "fk_partitioned_fk_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
DETAIL: Key (a, b)=(500, 501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (1500, 1501);
-ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_2_a_b_fkey"
DETAIL: Key (a, b)=(1500, 1501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk_2 (a,b) VALUES (1500, 1501);
-ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_2_a_b_fkey"
DETAIL: Key (a, b)=(1500, 1501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (2500, 2502);
ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
@@ -1729,6 +1782,37 @@ Indexes:
Referenced by:
TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b)
+-- Check the exsting FK trigger
+CREATE TEMP TABLE tmp_trg_info AS SELECT tgtype, tgrelid::regclass, tgconstraint
+FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+SELECT count(1) FROM tmp_trg_info;
+ count
+-------
+ 14
+(1 row)
+
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+-- No triggers
+SELECT count(1) FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+ count
+-------
+ 0
+(1 row)
+
+-- Changing it back to ENFORCED will recreate the necessary triggers.
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+-- Should be exactly the same number of triggers found as before
+SELECT COUNT(1) FROM pg_trigger pgt JOIN tmp_trg_info tt
+ON (pgt.tgtype = tt.tgtype AND pgt.tgrelid = tt.tgrelid AND pgt.tgconstraint = tt.tgconstraint);
+ count
+-------
+ 14
+(1 row)
+
ALTER TABLE fk_partitioned_fk DROP CONSTRAINT fk_partitioned_fk_a_b_fkey;
-- done.
DROP TABLE fk_notpartitioned_pk, fk_partitioned_fk;
@@ -1932,6 +2016,40 @@ Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
Foreign-key constraints:
TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
+DROP TABLE fk_partitioned_fk_2;
+CREATE TABLE fk_partitioned_fk_2 (b int, a int,
+ FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
+BEGIN;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have two constraints
+\d fk_partitioned_fk_2
+ Table "public.fk_partitioned_fk_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ b | integer | | |
+ a | integer | | |
+Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
+Foreign-key constraints:
+ "fk_partitioned_fk_2_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED
+ TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
+
+DROP TABLE fk_partitioned_fk_2;
+ROLLBACK;
+BEGIN;
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have only one constraint
+\d fk_partitioned_fk_2
+ Table "public.fk_partitioned_fk_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ b | integer | | |
+ a | integer | | |
+Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
+Foreign-key constraints:
+ TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED
+
+ROLLBACK;
DROP TABLE fk_partitioned_fk_2;
CREATE TABLE fk_partitioned_fk_4 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE) PARTITION BY RANGE (b, a);
CREATE TABLE fk_partitioned_fk_4_1 PARTITION OF fk_partitioned_fk_4 FOR VALUES FROM (1,1) TO (100,100);
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index d5f68812ba6..98f47873d41 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -2,13 +2,40 @@
-- FOREIGN KEY
--
--- MATCH FULL
+-- NOT ENFORCED
--
-- First test, check and cascade
--
CREATE TABLE PKTABLE ( ptest1 int PRIMARY KEY, ptest2 text );
-CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE, ftest2 int );
+CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE NOT ENFORCED,
+ ftest2 int );
+-- Inserting into the foreign key table will not result in an error, even if
+-- there is no matching key in the referenced table.
+INSERT INTO FKTABLE VALUES (1, 2);
+INSERT INTO FKTABLE VALUES (2, 3);
+
+-- Check FKTABLE
+SELECT * FROM FKTABLE;
+
+-- Changing it back to ENFORCED will leave the constraint in the NOT VALID state
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+-- Which needs to be explicitly validated.
+ALTER TABLE FKTABLE VALIDATE CONSTRAINT fktable_ftest1_fkey;
+
+-- Remove any existing rows that violate the constraint, then attempt to change
+-- it.
+TRUNCATE FKTABLE;
+ALTER TABLE FKTABLE VALIDATE CONSTRAINT fktable_ftest1_fkey;
+
+-- Any further inserts will fail due to the enforcement.
+INSERT INTO FKTABLE VALUES (3, 4);
+
+--
+-- MATCH FULL
+--
+-- First test, check and cascade
+--
-- Insert test data into PKTABLE
INSERT INTO PKTABLE VALUES (1, 'Test1');
INSERT INTO PKTABLE VALUES (2, 'Test2');
@@ -230,6 +257,9 @@ INSERT INTO FKTABLE VALUES (1, NULL);
ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL;
+-- NOT ENFORCED will bypass the initial check.
+ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL NOT ENFORCED;
+
DROP TABLE FKTABLE;
DROP TABLE PKTABLE;
@@ -968,10 +998,23 @@ INSERT INTO fktable VALUES (0, 20);
COMMIT;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT ENFORCED;
+
+BEGIN;
+
+-- doesn't match FK, no error.
+UPDATE pktable SET id = 10 WHERE id = 5;
+-- doesn't match PK, no error.
+INSERT INTO fktable VALUES (0, 20);
+
+ROLLBACK;
+
-- try additional syntax
-ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE ENFORCED;
-- illegal option
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORCED;
+CREATE TEMP TABLE fktable2 (fk int references pktable ENFORCED NOT ENFORCED);
-- test order of firing of FK triggers when several RI-induced changes need to
-- be made to the same row. This was broken by subtransaction-related
@@ -1236,11 +1279,16 @@ ALTER TABLE fk_partitioned_fk DROP COLUMN fdrop1;
CREATE TABLE fk_partitioned_fk_1 (fdrop1 int, fdrop2 int, a int, fdrop3 int, b int);
ALTER TABLE fk_partitioned_fk_1 DROP COLUMN fdrop1, DROP COLUMN fdrop2, DROP COLUMN fdrop3;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FROM (0,0) TO (1000,1000);
-ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk;
+ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
+ALTER TABLE fk_partitioned_fk_2 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
+-- Changing to ENFORCED does not alter the validation status; it must be explicitly validated.
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+ALTER TABLE fk_partitioned_fk VALIDATE CONSTRAINT fk_partitioned_fk_a_b_fkey ;
+
CREATE TABLE fk_partitioned_fk_3 (fdrop1 int, fdrop2 int, fdrop3 int, fdrop4 int, b int, a int)
PARTITION BY HASH (a);
ALTER TABLE fk_partitioned_fk_3 DROP COLUMN fdrop1, DROP COLUMN fdrop2,
@@ -1286,6 +1334,27 @@ UPDATE fk_notpartitioned_pk SET b = 1502 WHERE a = 1500;
UPDATE fk_notpartitioned_pk SET b = 2504 WHERE a = 2500;
-- check psql behavior
\d fk_notpartitioned_pk
+
+-- Check the exsting FK trigger
+CREATE TEMP TABLE tmp_trg_info AS SELECT tgtype, tgrelid::regclass, tgconstraint
+FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+SELECT count(1) FROM tmp_trg_info;
+
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+-- No triggers
+SELECT count(1) FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+
+-- Changing it back to ENFORCED will recreate the necessary triggers.
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+
+-- Should be exactly the same number of triggers found as before
+SELECT COUNT(1) FROM pg_trigger pgt JOIN tmp_trg_info tt
+ON (pgt.tgtype = tt.tgtype AND pgt.tgrelid = tt.tgrelid AND pgt.tgconstraint = tt.tgconstraint);
+
ALTER TABLE fk_partitioned_fk DROP CONSTRAINT fk_partitioned_fk_a_b_fkey;
-- done.
DROP TABLE fk_notpartitioned_pk, fk_partitioned_fk;
@@ -1422,6 +1491,22 @@ ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN
\d fk_partitioned_fk_2
DROP TABLE fk_partitioned_fk_2;
+CREATE TABLE fk_partitioned_fk_2 (b int, a int,
+ FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
+BEGIN;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have two constraints
+\d fk_partitioned_fk_2
+DROP TABLE fk_partitioned_fk_2;
+ROLLBACK;
+BEGIN;
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have only one constraint
+\d fk_partitioned_fk_2
+ROLLBACK;
+DROP TABLE fk_partitioned_fk_2;
+
CREATE TABLE fk_partitioned_fk_4 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE) PARTITION BY RANGE (b, a);
CREATE TABLE fk_partitioned_fk_4_1 PARTITION OF fk_partitioned_fk_4 FOR VALUES FROM (1,1) TO (100,100);
CREATE TABLE fk_partitioned_fk_4_2 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE SET NULL);
--
2.43.5
v10-0007-Merge-the-parent-and-child-constraints-with-diff.patchapplication/octet-stream; name=v10-0007-Merge-the-parent-and-child-constraints-with-diff.patchDownload
From 3651cbf884f5a459c099fb743cd478803610b53a Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Fri, 17 Jan 2025 17:03:59 +0530
Subject: [PATCH v10 7/7] Merge the parent and child constraints with differing
enforcibility.
If an enforced parent constraint is attached to a non-enforced child
constraint, the child constraint will be made enforced, with
validation applied if the parent constraint is validated as well.
Otherwise, a new enforced constraint (with validation, if the parent
constraint is validated) would need to be created on the child table,
which would be unnecessary if a similar constraint already exists and
can be attached.
On the other hand, having a not-enforced parent constraint with an
enforced child constraint does not cause any issues, and no changes
are required.
----
NOTE: This patch is intended to reduce the diff noise from the main
patch and is not meant to be committed separately. It should be
squashed with the main patch that adds ENFORCED/NOT ENFORCED.
----
---
src/backend/commands/tablecmds.c | 161 ++++++++++++++++++++--
src/test/regress/expected/foreign_key.out | 36 ++++-
src/test/regress/sql/foreign_key.sql | 14 ++
3 files changed, 195 insertions(+), 16 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 5c1b44c387d..d6bf11ae53a 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -11443,7 +11443,6 @@ tryAttachPartitionForeignKey(List **wqueue,
if (OidIsValid(partConstr->conparentid) ||
partConstr->condeferrable != parentConstr->condeferrable ||
partConstr->condeferred != parentConstr->condeferred ||
- partConstr->conenforced != parentConstr->conenforced ||
partConstr->confupdtype != parentConstr->confupdtype ||
partConstr->confdeltype != parentConstr->confdeltype ||
partConstr->confmatchtype != parentConstr->confmatchtype)
@@ -11488,7 +11487,9 @@ AttachPartitionForeignKey(List **wqueue,
Oid partConstrRelid;
bool parentConstrIsEnforced;
bool parentConstrIsValid;
+ bool partConstrIsEnforced;
bool partConstrIsValid;
+ bool partConstrParentIsSet;
Oid insertTriggerOid,
updateTriggerOid;
@@ -11507,10 +11508,80 @@ AttachPartitionForeignKey(List **wqueue,
if (!HeapTupleIsValid(partcontup))
elog(ERROR, "cache lookup failed for constraint %u", partConstrOid);
partConstr = (Form_pg_constraint) GETSTRUCT(partcontup);
+ partConstrIsEnforced = partConstr->conenforced;
partConstrIsValid = partConstr->convalidated;
partConstrFrelid = partConstr->confrelid;
partConstrRelid = partConstr->conrelid;
+ /*
+ * When the enforcibility of both constraints is the same, no special
+ * action is taken, and the flow remains unchanged. However, there are two
+ * scenarios to consider when it differs:
+ *
+ * 1. Parent constraint is not-enforced and child constraint is enforced:
+ *
+ * This is allowed because the not-enforced parent constraint does not
+ * have triggers, so no redundancy issues arise with the child constraint,
+ * even if it is enforced. In this case, we allow the child constraint to
+ * remain enforced and keep its trigger intact. This ensures that
+ * referential integrity checks for the child continue as before, even
+ * with the parent constraint not enforced. The relationship between the
+ * two constraints is maintained by setting the parent constraint, which
+ * enables us to locate the child constraint. This is important if the
+ * parent constraint is later changed to enforced, at which point the
+ * necessary trigger will be created for the parent. Any potential
+ * redundancy from these triggers will be addressed accordingly.
+ *
+ * 2. Parent constraint is enforced and child constraint is not-enforced:
+ *
+ * This is not allowed, as it would violate referential integrity. In such
+ * cases, the child constraint will first be made enforced before merging
+ * it with the enforced parent constraint. Then, action triggers and
+ * constraint redundancy will be handled in the usual manner, similar to
+ * how two enforced constraints are merged.
+ */
+ if (!parentConstrIsEnforced && partConstrIsEnforced)
+ {
+ ReleaseSysCache(partcontup);
+ ReleaseSysCache(parentConstrTup);
+
+ ConstraintSetParentConstraint(partConstrOid, parentConstrOid,
+ RelationGetRelid(partition));
+ CommandCounterIncrement();
+
+ return;
+ }
+ else if (parentConstrIsEnforced && !partConstrIsEnforced)
+ {
+ Constraint *cmdcon = makeNode(Constraint);
+ Relation conrel;
+ List *otherrelids = NIL;
+
+ cmdcon->contype = partConstr->contype;
+ cmdcon->conname = NameStr(partConstr->conname);
+ cmdcon->deferrable = partConstr->condeferrable;
+ cmdcon->initdeferred = partConstr->condeferred;
+ cmdcon->is_enforced = true;
+
+ conrel = table_open(ConstraintRelationId, RowExclusiveLock);
+
+ ATExecAlterConstrRecurse(cmdcon, conrel, trigrel, partConstr->conrelid,
+ partConstr->confrelid, partcontup, &otherrelids,
+ AccessExclusiveLock, InvalidOid, InvalidOid,
+ InvalidOid, InvalidOid);
+
+ table_close(conrel, RowExclusiveLock);
+
+ CommandCounterIncrement();
+ }
+
+ /*
+ * The constraint parent shouldn't be set beforehand, or if it's already
+ * set, it should be the specified parent.
+ */
+ partConstrParentIsSet = OidIsValid(partConstr->conparentid);
+ Assert(!partConstrParentIsSet || partConstr->conparentid == parentConstrOid);
+
ReleaseSysCache(partcontup);
ReleaseSysCache(parentConstrTup);
@@ -11523,8 +11594,10 @@ AttachPartitionForeignKey(List **wqueue,
DropForeignKeyConstraintTriggers(trigrel, partConstrOid, partConstrFrelid,
partConstrRelid);
- ConstraintSetParentConstraint(partConstrOid, parentConstrOid,
- RelationGetRelid(partition));
+ /* Skip if the parent is already set */
+ if (!partConstrParentIsSet)
+ ConstraintSetParentConstraint(partConstrOid, parentConstrOid,
+ RelationGetRelid(partition));
/*
* Like the constraint, attach partition's "check" triggers to the
@@ -11569,7 +11642,7 @@ AttachPartitionForeignKey(List **wqueue,
* validate the child data. Conversely, having an invalid parent
* constraint while the child constraint is valid doesn't cause any harm.
*/
- if (parentConstrIsValid && !partConstrIsValid);
+ if (wqueue && parentConstrIsValid && !partConstrIsValid)
{
Relation conrel;
@@ -12156,6 +12229,17 @@ ATExecAlterConstrEnforceability(Constraint *cmdcon, Relation conrel,
/* Drop all the triggers */
DropForeignKeyConstraintTriggers(tgrel, conoid, InvalidOid, InvalidOid);
+
+ /*
+ * If the referenced table is partitioned, the child constraint we're
+ * changing to not-enforced may have additional pg_constraint rows and
+ * action triggers that remain untouched while this child constraint
+ * is attached to the not-enforced parent. These must now be removed.
+ * For more details, see AttachPartitionForeignKey().
+ */
+ if (OidIsValid(currcon->conparentid) &&
+ get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE)
+ RemoveInheritedConstraint(conrel, tgrel, currcon->conrelid, conoid);
}
else /* Create triggers */
{
@@ -12214,13 +12298,46 @@ ATExecAlterConstrEnforceability(Constraint *cmdcon, Relation conrel,
true, NULL, 1, &pkey);
while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
- ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, fkrelid, pkrelid,
- childtup, otherrelids, lockmode,
- ReferencedDelTriggerOid,
- ReferencedUpdTriggerOid,
- ReferencingInsTriggerOid,
- ReferencingUpdTriggerOid);
+ {
+ Form_pg_constraint childcon;
+ childcon = (Form_pg_constraint) GETSTRUCT(childtup);
+
+ if (childcon->conenforced)
+ {
+
+ /*
+ * The child constraint is attached to the parent
+ * constraint, which is already enforced. Now, as the
+ * parent constraint is being modified to be enforced, some
+ * constraints and action triggers on the child table may
+ * become redundant and need to be removed.
+ */
+ if (currcon->confrelid == pkrelid)
+ {
+ /*
+ * The lock on the relation will be held by upper
+ * callers, so it's fine to open it with no lock here.
+ */
+ Relation rel = table_open(currcon->conrelid, NoLock);
+
+ AttachPartitionForeignKey(NULL, rel, childcon->oid,
+ conoid,
+ ReferencingInsTriggerOid,
+ ReferencingUpdTriggerOid,
+ tgrel);
+
+ table_close(rel, NoLock);
+ }
+ }
+ else
+ ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, childtup, otherrelids,
+ lockmode, ReferencedDelTriggerOid,
+ ReferencedUpdTriggerOid,
+ ReferencingInsTriggerOid,
+ ReferencingUpdTriggerOid);
+ }
systable_endscan(pscan);
}
}
@@ -20347,7 +20464,9 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
{
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
+ HeapTuple parentContup;
Form_pg_constraint conform;
+ Oid parentConstrIsEnforced;
contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
if (!HeapTupleIsValid(contup))
@@ -20362,12 +20481,34 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
continue;
}
+ /* Get the enforcibility of the parent constraint */
+ parentContup = SearchSysCache1(CONSTROID,
+ ObjectIdGetDatum(conform->conparentid));
+ if (!HeapTupleIsValid(parentContup))
+ elog(ERROR, "cache lookup failed for constraint %u",
+ conform->conparentid);
+ parentConstrIsEnforced =
+ ((Form_pg_constraint) GETSTRUCT(parentContup))->conenforced;
+ ReleaseSysCache(parentContup);
+
/*
* The constraint on this table must be marked no longer a child of
* the parent's constraint, as do its check triggers.
*/
ConstraintSetParentConstraint(fk->conoid, InvalidOid, InvalidOid);
+ /*
+ * Unsetting the parent is sufficient when the parent constraint is
+ * not enforced and the child constraint is enforced, as we link them
+ * by setting the constraint parent, while leaving the rest unchanged.
+ * For more details, see AttachPartitionForeignKey().
+ */
+ if (!parentConstrIsEnforced && fk->conenforced)
+ {
+ ReleaseSysCache(contup);
+ continue;
+ }
+
/*
* Also, look up the partition's "check" triggers corresponding to the
* enforced constraint being detached and detach them from the parent
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 9f6f22dd988..d894beaf1c0 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -1713,8 +1713,33 @@ ALTER TABLE fk_partitioned_fk_3 DROP COLUMN fdrop1, DROP COLUMN fdrop2,
DROP COLUMN fdrop3, DROP COLUMN fdrop4;
CREATE TABLE fk_partitioned_fk_3_0 PARTITION OF fk_partitioned_fk_3 FOR VALUES WITH (MODULUS 5, REMAINDER 0);
CREATE TABLE fk_partitioned_fk_3_1 PARTITION OF fk_partitioned_fk_3 FOR VALUES WITH (MODULUS 5, REMAINDER 1);
+-- Merge the not-enforced parent constraint with the enforced and not-enforced child constraints.
+ALTER TABLE fk_partitioned_fk_3_0 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ENFORCED;
+ALTER TABLE fk_partitioned_fk_3_1 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk_3 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+SELECT conname, conenforced, convalidated, conrelid::regclass FROM pg_constraint
+WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+ conname | conenforced | convalidated | conrelid
+--------------------------------+-------------+--------------+-----------------------
+ fk_partitioned_fk_3_0_a_b_fkey | t | t | fk_partitioned_fk_3_0
+ fk_partitioned_fk_3_1_a_b_fkey | f | f | fk_partitioned_fk_3_1
+ fk_partitioned_fk_3_a_b_fkey | f | f | fk_partitioned_fk_3
+(3 rows)
+
+-- Merging an enforced parent constraint (validated) with a not-enforced child
+-- constraint will implicitly change the child constraint to enforced and apply
+-- the validation as well.
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_3
FOR VALUES FROM (2000,2000) TO (3000,3000);
+SELECT conname, conenforced, convalidated, conrelid::regclass FROM pg_constraint
+WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+ conname | conenforced | convalidated | conrelid
+--------------------------------+-------------+--------------+-----------------------
+ fk_partitioned_fk_3_0_a_b_fkey | t | t | fk_partitioned_fk_3_0
+ fk_partitioned_fk_3_1_a_b_fkey | t | t | fk_partitioned_fk_3_1
+ fk_partitioned_fk_3_a_b_fkey | t | t | fk_partitioned_fk_3
+(3 rows)
+
-- Creating a foreign key with ONLY on a partitioned table referencing
-- a non-partitioned table fails.
ALTER TABLE ONLY fk_partitioned_fk ADD FOREIGN KEY (a, b)
@@ -1735,16 +1760,16 @@ INSERT INTO fk_partitioned_fk_2 (a,b) VALUES (1500, 1501);
ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_2_a_b_fkey"
DETAIL: Key (a, b)=(1500, 1501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (2500, 2502);
-ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_3_1_a_b_fkey"
DETAIL: Key (a, b)=(2500, 2502) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk_3 (a,b) VALUES (2500, 2502);
-ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_3_1_a_b_fkey"
DETAIL: Key (a, b)=(2500, 2502) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (2501, 2503);
-ERROR: insert or update on table "fk_partitioned_fk_3_0" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_0" violates foreign key constraint "fk_partitioned_fk_3_0_a_b_fkey"
DETAIL: Key (a, b)=(2501, 2503) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk_3 (a,b) VALUES (2501, 2503);
-ERROR: insert or update on table "fk_partitioned_fk_3_0" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_0" violates foreign key constraint "fk_partitioned_fk_3_0_a_b_fkey"
DETAIL: Key (a, b)=(2501, 2503) is not present in table "fk_notpartitioned_pk".
-- but if we insert the values that make them valid, then they work
INSERT INTO fk_notpartitioned_pk VALUES (500, 501), (1500, 1501),
@@ -1755,7 +1780,7 @@ INSERT INTO fk_partitioned_fk (a,b) VALUES (2500, 2502);
INSERT INTO fk_partitioned_fk (a,b) VALUES (2501, 2503);
-- this update fails because there is no referenced row
UPDATE fk_partitioned_fk SET a = a + 1 WHERE a = 2501;
-ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_3_1_a_b_fkey"
DETAIL: Key (a, b)=(2502, 2503) is not present in table "fk_notpartitioned_pk".
-- but we can fix it thusly:
INSERT INTO fk_notpartitioned_pk (a,b) VALUES (2502, 2503);
@@ -2030,7 +2055,6 @@ ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN
a | integer | | |
Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
Foreign-key constraints:
- "fk_partitioned_fk_2_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED
TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
DROP TABLE fk_partitioned_fk_2;
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index 98f47873d41..8dda6b6bbae 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -1295,9 +1295,23 @@ ALTER TABLE fk_partitioned_fk_3 DROP COLUMN fdrop1, DROP COLUMN fdrop2,
DROP COLUMN fdrop3, DROP COLUMN fdrop4;
CREATE TABLE fk_partitioned_fk_3_0 PARTITION OF fk_partitioned_fk_3 FOR VALUES WITH (MODULUS 5, REMAINDER 0);
CREATE TABLE fk_partitioned_fk_3_1 PARTITION OF fk_partitioned_fk_3 FOR VALUES WITH (MODULUS 5, REMAINDER 1);
+-- Merge the not-enforced parent constraint with the enforced and not-enforced child constraints.
+ALTER TABLE fk_partitioned_fk_3_0 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ENFORCED;
+ALTER TABLE fk_partitioned_fk_3_1 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk_3 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+
+SELECT conname, conenforced, convalidated, conrelid::regclass FROM pg_constraint
+WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+
+-- Merging an enforced parent constraint (validated) with a not-enforced child
+-- constraint will implicitly change the child constraint to enforced and apply
+-- the validation as well.
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_3
FOR VALUES FROM (2000,2000) TO (3000,3000);
+SELECT conname, conenforced, convalidated, conrelid::regclass FROM pg_constraint
+WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+
-- Creating a foreign key with ONLY on a partitioned table referencing
-- a non-partitioned table fails.
ALTER TABLE ONLY fk_partitioned_fk ADD FOREIGN KEY (a, b)
--
2.43.5
On 20.01.25 17:53, Amul Sul wrote:
Attached is a new set of patches. Please ignore patch 0001 here, which
was posted separately [1] -- proposes allowing invalid foreign key
constraints on partitioned tables. Patch 0002 refactors
tryAttachPartitionForeignKey(), primarily needed by the new patch
v9-0006 included in this set. This patch handles merging constraints
with different enforceability. Without it, a new constraint would be
created on the child. However, this patch introduces additional code
that may appear somewhat messy or confusing. I've added plenty of
comments to clarify the logic. While I’ve done some testing, it hasn’t
been extensive. I plan to do more testing in the next week.Please let me know if we should continue with patch 0006 improvement,
or if the feature up to patch 0005, which enforces matching
enforceability before merging constraints, is sufficient.1] /messages/by-id/CAAJ_b96Bp=-ZwihPPtuaNX=SrZ0U6ZsXD3+fgARO0JuKa8v2jQ@mail.gmail.com
I made minor updates to the attached version, particularly ensuring
that the order of relation opening and closing remains the same as
before in ATExecAlterConstrRecurse(). Additionally, I’ve added a
refactoring patch to make createForeignKeyActionTriggers() accept a
relation OID instead of a Relation, making this function consistent
with others like createForeignKeyCheckTriggers().
I think v10-0001 has been committed separately now. I can't
successfully apply the remaining patches though. Could you supply an
updated patch set?
Just from looking at them, the refactoring patches 0002, 0003, 0004 seem ok.
0005 also seems ok.
In 0006, this change in the test output should be improved:
-- XXX: error message is misleading here
ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
-ERROR: ALTER CONSTRAINT statement constraints cannot be marked ENFORCED
-LINE 1: ...TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
- ^
+ERROR: constraint "unique_tbl_i_key" of relation "unique_tbl" is not a
foreign key constraint
Maybe this should be along the lines of "ALTER CONSTRAINT ... ENFORCED
is not supported for %s constraints" or something like that.
This behavior is not correct:
+-- Changing it back to ENFORCED will leave the constraint in the NOT
VALID state
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+-- Which needs to be explicitly validated.
+ALTER TABLE FKTABLE VALIDATE CONSTRAINT fktable_ftest1_fkey;
Setting the constraint to enforced should enforce it immediately. This
SQL statement is covered by the SQL standard. Also, I think it's a
better user experience if you don't require two steps.
On Tue, Jan 28, 2025 at 4:01 PM Peter Eisentraut <peter@eisentraut.org> wrote:
On 20.01.25 17:53, Amul Sul wrote:
Attached is a new set of patches. Please ignore patch 0001 here, which
was posted separately [1] -- proposes allowing invalid foreign key
constraints on partitioned tables. Patch 0002 refactors
tryAttachPartitionForeignKey(), primarily needed by the new patch
v9-0006 included in this set. This patch handles merging constraints
with different enforceability. Without it, a new constraint would be
created on the child. However, this patch introduces additional code
that may appear somewhat messy or confusing. I've added plenty of
comments to clarify the logic. While I’ve done some testing, it hasn’t
been extensive. I plan to do more testing in the next week.Please let me know if we should continue with patch 0006 improvement,
or if the feature up to patch 0005, which enforces matching
enforceability before merging constraints, is sufficient.1] /messages/by-id/CAAJ_b96Bp=-ZwihPPtuaNX=SrZ0U6ZsXD3+fgARO0JuKa8v2jQ@mail.gmail.com
I made minor updates to the attached version, particularly ensuring
that the order of relation opening and closing remains the same as
before in ATExecAlterConstrRecurse(). Additionally, I’ve added a
refactoring patch to make createForeignKeyActionTriggers() accept a
relation OID instead of a Relation, making this function consistent
with others like createForeignKeyCheckTriggers().I think v10-0001 has been committed separately now. I can't
successfully apply the remaining patches though. Could you supply an
updated patch set?
Sure, I plan to work on that tomorrow.
Just from looking at them, the refactoring patches 0002, 0003, 0004 seem ok.
0005 also seems ok.
Thank you.
In 0006, this change in the test output should be improved:
-- XXX: error message is misleading here ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED; -ERROR: ALTER CONSTRAINT statement constraints cannot be marked ENFORCED -LINE 1: ...TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED; - ^ +ERROR: constraint "unique_tbl_i_key" of relation "unique_tbl" is not a foreign key constraintMaybe this should be along the lines of "ALTER CONSTRAINT ... ENFORCED
is not supported for %s constraints" or something like that.
Ok, let me see what can be done here.
This behavior is not correct:
+-- Changing it back to ENFORCED will leave the constraint in the NOT VALID state +ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED; +-- Which needs to be explicitly validated. +ALTER TABLE FKTABLE VALIDATE CONSTRAINT fktable_ftest1_fkey;Setting the constraint to enforced should enforce it immediately. This
SQL statement is covered by the SQL standard. Also, I think it's a
better user experience if you don't require two steps.
Let me clarify: the constraint will be enforced for new inserts and
updates, but it won't be validated against existing data, so those
will remain marked as invalid.
Regards,
On 28.01.25 11:58, Amul Sul wrote:
This behavior is not correct:
+-- Changing it back to ENFORCED will leave the constraint in the NOT VALID state +ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED; +-- Which needs to be explicitly validated. +ALTER TABLE FKTABLE VALIDATE CONSTRAINT fktable_ftest1_fkey;Setting the constraint to enforced should enforce it immediately. This
SQL statement is covered by the SQL standard. Also, I think it's a
better user experience if you don't require two steps.Let me clarify: the constraint will be enforced for new inserts and
updates, but it won't be validated against existing data, so those
will remain marked as invalid.
Yes, I understand, but that is the not the correct behavior of this
command per SQL standard.
On Tue, Jan 28, 2025 at 9:47 PM Peter Eisentraut <peter@eisentraut.org> wrote:
In 0006, this change in the test output should be improved:
-- XXX: error message is misleading here ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED; -ERROR: ALTER CONSTRAINT statement constraints cannot be marked ENFORCED -LINE 1: ...TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED; - ^ +ERROR: constraint "unique_tbl_i_key" of relation "unique_tbl" is not a foreign key constraintMaybe this should be along the lines of "ALTER CONSTRAINT ... ENFORCED
is not supported for %s constraints" or something like that.Ok, let me see what can be done here.
I tried to improve the error message by adding the following details
for this case in the attached version:
+ERROR: cannot alter enforceability of constraint "unique_tbl_i_key"
of relation "unique_tbl"
+DETAIL: Enforceability can only be altered for foreign key constraints.
On 28.01.25 11:58, Amul Sul wrote:
This behavior is not correct:
+-- Changing it back to ENFORCED will leave the constraint in the NOT VALID state +ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED; +-- Which needs to be explicitly validated. +ALTER TABLE FKTABLE VALIDATE CONSTRAINT fktable_ftest1_fkey;Setting the constraint to enforced should enforce it immediately. This
SQL statement is covered by the SQL standard. Also, I think it's a
better user experience if you don't require two steps.Let me clarify: the constraint will be enforced for new inserts and
updates, but it won't be validated against existing data, so those
will remain marked as invalid.Yes, I understand, but that is the not the correct behavior of this
command per SQL standard.
I haven't worked on this yet due to a lack of clarity, and it requires
further discussion. The behavior will remain the same as the previous
version. The attached set is the rebased version on top of the latest
master head and includes the aforementioned error message
improvements.
Regards,
Amul
Attachments:
v11-0005-Remove-hastriggers-flag-check-before-fetching-FK.patchapplication/x-patch; name=v11-0005-Remove-hastriggers-flag-check-before-fetching-FK.patchDownload
From 148cf1f33ccece7d73f400aba4c03f3200158b06 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 7 Oct 2024 12:40:07 +0530
Subject: [PATCH v11 5/7] Remove hastriggers flag check before fetching FK
constraints.
With NOT ENFORCED, foreign key constraints will be created without
triggers. Therefore, the criteria for fetching foreign keys based on
the presence of triggers no longer apply.
----
NOTE: This patch is intended to reduce the diff noise from the main
patch and is not meant to be committed separately. It should be
squashed with the main patch that adds ENFORCED/NOT ENFORCED.
----
---
src/backend/utils/cache/relcache.c | 5 -
src/bin/pg_dump/pg_dump.c | 8 +-
src/bin/psql/describe.c | 226 ++++++++++++++---------------
3 files changed, 108 insertions(+), 131 deletions(-)
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 43219a9629c..485cd2178bc 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4658,11 +4658,6 @@ RelationGetFKeyList(Relation relation)
if (relation->rd_fkeyvalid)
return relation->rd_fkeylist;
- /* Fast path: non-partitioned tables without triggers can't have FKs */
- if (!relation->rd_rel->relhastriggers &&
- relation->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
- return NIL;
-
/*
* We build the list we intend to return (in the caller's context) while
* doing the scan. After successfully completing the scan, we copy that
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 02e1fdf8f78..a7887251834 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -7842,13 +7842,7 @@ getConstraints(Archive *fout, TableInfo tblinfo[], int numTables)
{
TableInfo *tinfo = &tblinfo[i];
- /*
- * For partitioned tables, foreign keys have no triggers so they must
- * be included anyway in case some foreign keys are defined.
- */
- if ((!tinfo->hastriggers &&
- tinfo->relkind != RELKIND_PARTITIONED_TABLE) ||
- !(tinfo->dobj.dump & DUMP_COMPONENT_DEFINITION))
+ if (!(tinfo->dobj.dump & DUMP_COMPONENT_DEFINITION))
continue;
/* OK, we need info for this table */
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index aa4363b200a..c633306b3cb 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2544,136 +2544,124 @@ describeOneTableDetails(const char *schemaname,
PQclear(result);
}
- /*
- * Print foreign-key constraints (there are none if no triggers,
- * except if the table is partitioned, in which case the triggers
- * appear in the partitions)
- */
- if (tableinfo.hastriggers ||
- tableinfo.relkind == RELKIND_PARTITIONED_TABLE)
+ /* Print foreign-key constraints */
+ if (pset.sversion >= 120000 &&
+ (tableinfo.ispartition || tableinfo.relkind == RELKIND_PARTITIONED_TABLE))
{
- if (pset.sversion >= 120000 &&
- (tableinfo.ispartition || tableinfo.relkind == RELKIND_PARTITIONED_TABLE))
+ /*
+ * Put the constraints defined in this table first, followed
+ * by the constraints defined in ancestor partitioned tables.
+ */
+ printfPQExpBuffer(&buf,
+ "SELECT conrelid = '%s'::pg_catalog.regclass AS sametable,\n"
+ " conname,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef,\n"
+ " conrelid::pg_catalog.regclass AS ontable\n"
+ " FROM pg_catalog.pg_constraint,\n"
+ " pg_catalog.pg_partition_ancestors('%s')\n"
+ " WHERE conrelid = relid AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
+ "ORDER BY sametable DESC, conname;",
+ oid, oid);
+ }
+ else
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT true as sametable, conname,\n"
+ " pg_catalog.pg_get_constraintdef(r.oid, true) as condef,\n"
+ " conrelid::pg_catalog.regclass AS ontable\n"
+ "FROM pg_catalog.pg_constraint r\n"
+ "WHERE r.conrelid = '%s' AND r.contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n",
+ oid);
+
+ if (pset.sversion >= 120000)
+ appendPQExpBufferStr(&buf, " AND conparentid = 0\n");
+ appendPQExpBufferStr(&buf, "ORDER BY conname");
+ }
+
+ result = PSQLexec(buf.data);
+ if (!result)
+ goto error_return;
+ else
+ tuples = PQntuples(result);
+
+ if (tuples > 0)
+ {
+ int i_sametable = PQfnumber(result, "sametable"),
+ i_conname = PQfnumber(result, "conname"),
+ i_condef = PQfnumber(result, "condef"),
+ i_ontable = PQfnumber(result, "ontable");
+
+ printTableAddFooter(&cont, _("Foreign-key constraints:"));
+ for (i = 0; i < tuples; i++)
{
/*
- * Put the constraints defined in this table first, followed
- * by the constraints defined in ancestor partitioned tables.
+ * Print untranslated constraint name and definition. Use
+ * a "TABLE tab" prefix when the constraint is defined in
+ * a parent partitioned table.
*/
- printfPQExpBuffer(&buf,
- "SELECT conrelid = '%s'::pg_catalog.regclass AS sametable,\n"
- " conname,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef,\n"
- " conrelid::pg_catalog.regclass AS ontable\n"
- " FROM pg_catalog.pg_constraint,\n"
- " pg_catalog.pg_partition_ancestors('%s')\n"
- " WHERE conrelid = relid AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
- "ORDER BY sametable DESC, conname;",
- oid, oid);
- }
- else
- {
- printfPQExpBuffer(&buf,
- "SELECT true as sametable, conname,\n"
- " pg_catalog.pg_get_constraintdef(r.oid, true) as condef,\n"
- " conrelid::pg_catalog.regclass AS ontable\n"
- "FROM pg_catalog.pg_constraint r\n"
- "WHERE r.conrelid = '%s' AND r.contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n",
- oid);
-
- if (pset.sversion >= 120000)
- appendPQExpBufferStr(&buf, " AND conparentid = 0\n");
- appendPQExpBufferStr(&buf, "ORDER BY conname");
- }
-
- result = PSQLexec(buf.data);
- if (!result)
- goto error_return;
- else
- tuples = PQntuples(result);
-
- if (tuples > 0)
- {
- int i_sametable = PQfnumber(result, "sametable"),
- i_conname = PQfnumber(result, "conname"),
- i_condef = PQfnumber(result, "condef"),
- i_ontable = PQfnumber(result, "ontable");
-
- printTableAddFooter(&cont, _("Foreign-key constraints:"));
- for (i = 0; i < tuples; i++)
- {
- /*
- * Print untranslated constraint name and definition. Use
- * a "TABLE tab" prefix when the constraint is defined in
- * a parent partitioned table.
- */
- if (strcmp(PQgetvalue(result, i, i_sametable), "f") == 0)
- printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
- PQgetvalue(result, i, i_ontable),
- PQgetvalue(result, i, i_conname),
- PQgetvalue(result, i, i_condef));
- else
- printfPQExpBuffer(&buf, " \"%s\" %s",
- PQgetvalue(result, i, i_conname),
- PQgetvalue(result, i, i_condef));
-
- printTableAddFooter(&cont, buf.data);
- }
- }
- PQclear(result);
- }
-
- /* print incoming foreign-key references */
- if (tableinfo.hastriggers ||
- tableinfo.relkind == RELKIND_PARTITIONED_TABLE)
- {
- if (pset.sversion >= 120000)
- {
- printfPQExpBuffer(&buf,
- "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
- " FROM pg_catalog.pg_constraint c\n"
- " WHERE confrelid IN (SELECT pg_catalog.pg_partition_ancestors('%s')\n"
- " UNION ALL VALUES ('%s'::pg_catalog.regclass))\n"
- " AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
- "ORDER BY conname;",
- oid, oid);
- }
- else
- {
- printfPQExpBuffer(&buf,
- "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
- " FROM pg_catalog.pg_constraint\n"
- " WHERE confrelid = %s AND contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n"
- "ORDER BY conname;",
- oid);
- }
-
- result = PSQLexec(buf.data);
- if (!result)
- goto error_return;
- else
- tuples = PQntuples(result);
-
- if (tuples > 0)
- {
- int i_conname = PQfnumber(result, "conname"),
- i_ontable = PQfnumber(result, "ontable"),
- i_condef = PQfnumber(result, "condef");
-
- printTableAddFooter(&cont, _("Referenced by:"));
- for (i = 0; i < tuples; i++)
- {
+ if (strcmp(PQgetvalue(result, i, i_sametable), "f") == 0)
printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
PQgetvalue(result, i, i_ontable),
PQgetvalue(result, i, i_conname),
PQgetvalue(result, i, i_condef));
+ else
+ printfPQExpBuffer(&buf, " \"%s\" %s",
+ PQgetvalue(result, i, i_conname),
+ PQgetvalue(result, i, i_condef));
- printTableAddFooter(&cont, buf.data);
- }
+ printTableAddFooter(&cont, buf.data);
}
- PQclear(result);
}
+ PQclear(result);
+
+ /* print incoming foreign-key references */
+ if (pset.sversion >= 120000)
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
+ " FROM pg_catalog.pg_constraint c\n"
+ " WHERE confrelid IN (SELECT pg_catalog.pg_partition_ancestors('%s')\n"
+ " UNION ALL VALUES ('%s'::pg_catalog.regclass))\n"
+ " AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
+ "ORDER BY conname;",
+ oid, oid);
+ }
+ else
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
+ " FROM pg_catalog.pg_constraint\n"
+ " WHERE confrelid = %s AND contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n"
+ "ORDER BY conname;",
+ oid);
+ }
+
+ result = PSQLexec(buf.data);
+ if (!result)
+ goto error_return;
+ else
+ tuples = PQntuples(result);
+
+ if (tuples > 0)
+ {
+ int i_conname = PQfnumber(result, "conname"),
+ i_ontable = PQfnumber(result, "ontable"),
+ i_condef = PQfnumber(result, "condef");
+
+ printTableAddFooter(&cont, _("Referenced by:"));
+ for (i = 0; i < tuples; i++)
+ {
+ printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
+ PQgetvalue(result, i, i_ontable),
+ PQgetvalue(result, i, i_conname),
+ PQgetvalue(result, i, i_condef));
+
+ printTableAddFooter(&cont, buf.data);
+ }
+ }
+ PQclear(result);
/* print any row-level policies */
if (pset.sversion >= 90500)
--
2.43.5
v11-0006-Add-support-for-NOT-ENFORCED-in-foreign-key-cons.patchapplication/x-patch; name=v11-0006-Add-support-for-NOT-ENFORCED-in-foreign-key-cons.patchDownload
From e57f67fa0bf74c37fee0d9a92dc07b1074e183dc Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 20 Jan 2025 21:45:07 +0530
Subject: [PATCH v11 6/7] Add support for NOT ENFORCED in foreign key
constraints.
Typically, when a foreign key (FK) constraint is created on a table,
action and check triggers are added to maintain data integrity. With
this patch, if a constraint is marked as NOT ENFORCED, integrity
checks are no longer required, making these triggers unnecessary.
Consequently, when creating a NOT ENFORCED FK constraint, triggers
will not be created, and the constraint will be marked as NOT VALID.
Similarly, if an existing FK constraint is changed to NOT ENFORCED,
the associated triggers will be dropped, and the constraint will also
be marked as NOT VALID. Conversely, if a NOT ENFORCED FK constraint is
changed to ENFORCED, the necessary triggers will be created, and the
constraint will remain in the NOT VALID state.
----
NOTE: In this patch, the tryAttachPartitionForeignKey() function will
not merge the constraint if the enforcibility differs. This will be
addressed in the next patch.
----
---
doc/src/sgml/advanced.sgml | 13 +
doc/src/sgml/catalogs.sgml | 2 +-
doc/src/sgml/ddl.sgml | 12 +-
doc/src/sgml/ref/alter_table.sgml | 12 +-
doc/src/sgml/ref/create_table.sgml | 2 +-
src/backend/catalog/pg_constraint.c | 5 +-
src/backend/catalog/sql_features.txt | 2 +-
src/backend/commands/tablecmds.c | 400 +++++++++++++++++-----
src/backend/parser/gram.y | 7 +-
src/backend/parser/parse_utilcmd.c | 14 +-
src/backend/utils/cache/relcache.c | 1 +
src/include/utils/rel.h | 3 +
src/test/regress/expected/constraints.out | 10 +-
src/test/regress/expected/foreign_key.out | 130 ++++++-
src/test/regress/sql/constraints.sql | 1 -
src/test/regress/sql/foreign_key.sql | 93 ++++-
16 files changed, 574 insertions(+), 133 deletions(-)
diff --git a/doc/src/sgml/advanced.sgml b/doc/src/sgml/advanced.sgml
index e15a3323dfb..14ed11e1f4d 100644
--- a/doc/src/sgml/advanced.sgml
+++ b/doc/src/sgml/advanced.sgml
@@ -125,6 +125,19 @@ ERROR: insert or update on table "weather" violates foreign key constraint "wea
DETAIL: Key (city)=(Berkeley) is not present in table "cities".
</screen>
</para>
+ <para>
+ If, for some reason, you do not want to enforce this constraint and wish to
+ avoid the error, you can modify the foreign key constraint to <literal>NOT ENFORCED</literal>,
+ or specify it in the <link linkend="sql-createtable"><command>CREATE TABLE</command></link>.
+ </para>
+ <para>
+ Let's change the constraint to <literal>NOT ENFORCED</literal>, and when you
+ attempt to reinsert the same record, it will not result in any error.
+<programlisting>
+ALTER TABLE weather ALTER CONSTRAINT weather_city_fkey NOT ENFORCED;
+INSERT INTO weather VALUES ('Berkeley', 45, 53, 0.0, '1994-11-28');
+</programlisting>
+ </para>
<para>
The behavior of foreign keys can be finely tuned to your
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 088fb175cce..d8aa52993a8 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2597,7 +2597,7 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l
</para>
<para>
Is the constraint enforced?
- Currently, can be false only for CHECK constraints
+ Currently, can be false only for foreign keys and CHECK constraints
</para></entry>
</row>
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index dea04d64db6..d3377503981 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -1054,11 +1054,13 @@ CREATE TABLE example (
</indexterm>
<para>
- A foreign key constraint specifies that the values in a column (or
- a group of columns) must match the values appearing in some row
- of another table.
- We say this maintains the <firstterm>referential
- integrity</firstterm> between two related tables.
+ A foreign key constraint with <literal>ENFORCED</literal>, the default
+ setting, specifies that the values in a column (or a group of columns)
+ must match the values appearing in some row of another table. We say this
+ maintains the <firstterm>referential integrity</firstterm> between two
+ related tables. If a foreign key constraint is created with <literal>NOT ENFORCED</literal>,
+ the check for aforementioned <firstterm>referential integrity</firstterm>
+ will not be performed.
</para>
<para>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index f9576da435e..8d4d0dec9d2 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -58,7 +58,7 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable>
ALTER [ COLUMN ] <replaceable class="parameter">column_name</replaceable> SET COMPRESSION <replaceable class="parameter">compression_method</replaceable>
ADD <replaceable class="parameter">table_constraint</replaceable> [ NOT VALID ]
ADD <replaceable class="parameter">table_constraint_using_index</replaceable>
- ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+ ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]
VALIDATE CONSTRAINT <replaceable class="parameter">constraint_name</replaceable>
DROP CONSTRAINT [ IF EXISTS ] <replaceable class="parameter">constraint_name</replaceable> [ RESTRICT | CASCADE ]
DISABLE TRIGGER [ <replaceable class="parameter">trigger_name</replaceable> | ALL | USER ]
@@ -553,6 +553,13 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
This form alters the attributes of a constraint that was previously
created. Currently only foreign key constraints may be altered.
</para>
+ <para>
+ Note that when a constraint is changed to <literal>NOT ENFORCED</literal>,
+ it is also marked as <literal>NOT VALID</literal>. While this might seem
+ unnecessary, it is done for consistency and should be set accordingly.
+ This ensures that if the constraint is later changed to <literal>ENFORCED</literal>,
+ it will automatically be in the correct <literal>NOT VALID</literal> state.
+ </para>
</listitem>
</varlistentry>
@@ -563,7 +570,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
This form validates a foreign key or check constraint that was
previously created as <literal>NOT VALID</literal>, by scanning the
table to ensure there are no rows for which the constraint is not
- satisfied. Nothing happens if the constraint is already marked valid.
+ satisfied. If the constraint is not enforced, an error is thrown.
+ Nothing happens if the constraint is already marked valid.
(See <xref linkend="sql-altertable-notes"/> below for an explanation
of the usefulness of this command.)
</para>
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index 2237321cb4f..5071dfeabf9 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -1401,7 +1401,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</para>
<para>
- This is currently only supported for <literal>CHECK</literal>
+ This is currently only supported for foreign key and <literal>CHECK</literal>
constraints.
</para>
</listitem>
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index ac80652baf2..0467e7442ff 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -100,8 +100,9 @@ CreateConstraintEntry(const char *constraintName,
ObjectAddresses *addrs_auto;
ObjectAddresses *addrs_normal;
- /* Only CHECK constraint can be not enforced */
- Assert(isEnforced || constraintType == CONSTRAINT_CHECK);
+ /* Only CHECK or FOREIGN KEY constraint can be not enforced */
+ Assert(isEnforced || constraintType == CONSTRAINT_CHECK ||
+ constraintType == CONSTRAINT_FOREIGN);
/* NOT ENFORCED constraint must be NOT VALID */
Assert(isEnforced || !isValidated);
diff --git a/src/backend/catalog/sql_features.txt b/src/backend/catalog/sql_features.txt
index 2f250d2c57b..d49471bbdf2 100644
--- a/src/backend/catalog/sql_features.txt
+++ b/src/backend/catalog/sql_features.txt
@@ -281,7 +281,7 @@ F461 Named character sets NO
F471 Scalar subquery values YES
F481 Expanded NULL predicate YES
F491 Constraint management YES
-F492 Optional table constraint enforcement NO check constraints only
+F492 Optional table constraint enforcement NO foreign key and check constraints only
F501 Features and conformance views YES
F501 Features and conformance views 01 SQL_FEATURES view YES
F501 Features and conformance views 02 SQL_SIZING view YES
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 56233eea319..00cf56d3fc9 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -391,11 +391,23 @@ static void AlterSeqNamespaces(Relation classRel, Relation rel,
LOCKMODE lockmode);
static ObjectAddress ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd,
bool recurse, bool recursing, LOCKMODE lockmode);
-static bool ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
- const Oid fkrelid, const Oid pkrelid,
- HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode);
-static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
+static bool ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel,
+ Relation tgrel, const Oid fkrelid,
+ const Oid pkrelid, HeapTuple contuple,
+ List **otherrelids, LOCKMODE lockmode,
+ Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger);
+static void ATExecAlterConstrEnforceability(Constraint *cmdcon, Relation conrel,
+ Relation tgrel, const Oid fkrelid,
+ const Oid pkrelid, HeapTuple contuple,
+ List **otherrelids, LOCKMODE lockmode,
+ Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger);
+static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Oid fkrel,
bool deferrable, bool initdeferred,
List **otherrelids);
static void ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
@@ -10457,7 +10469,7 @@ addFkConstraint(addFkConstraintSides fkside,
CONSTRAINT_FOREIGN,
fkconstraint->deferrable,
fkconstraint->initdeferred,
- true, /* Is Enforced */
+ fkconstraint->is_enforced,
fkconstraint->initially_valid,
parentConstr,
RelationGetRelid(rel),
@@ -10575,21 +10587,23 @@ addFkRecurseReferenced(Constraint *fkconstraint, Relation rel,
Oid parentDelTrigger, Oid parentUpdTrigger,
bool with_period)
{
- Oid deleteTriggerOid,
- updateTriggerOid;
+ Oid deleteTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
Assert(CheckRelationLockedByMe(pkrel, ShareRowExclusiveLock, true));
Assert(CheckRelationLockedByMe(rel, ShareRowExclusiveLock, true));
/*
- * Create the action triggers that enforce the constraint.
+ * Create action triggers to enforce the constraint, or skip them if the
+ * constraint is not enforced.
*/
- createForeignKeyActionTriggers(RelationGetRelid(rel),
- RelationGetRelid(pkrel),
- fkconstraint,
- parentConstr, indexOid,
- parentDelTrigger, parentUpdTrigger,
- &deleteTriggerOid, &updateTriggerOid);
+ if (fkconstraint->is_enforced)
+ createForeignKeyActionTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
+ fkconstraint,
+ parentConstr, indexOid,
+ parentDelTrigger, parentUpdTrigger,
+ &deleteTriggerOid, &updateTriggerOid);
/*
* If the referenced table is partitioned, recurse on ourselves to handle
@@ -10710,8 +10724,8 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
Oid parentInsTrigger, Oid parentUpdTrigger,
bool with_period)
{
- Oid insertTriggerOid,
- updateTriggerOid;
+ Oid insertTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
Assert(OidIsValid(parentConstr));
Assert(CheckRelationLockedByMe(rel, ShareRowExclusiveLock, true));
@@ -10723,29 +10737,32 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
errmsg("foreign key constraints are not supported on foreign tables")));
/*
- * Add the check triggers to it and, if necessary, schedule it to be
- * checked in Phase 3.
+ * Add check triggers if the constraint is enforced, and if needed,
+ * schedule them to be checked in Phase 3.
*
* If the relation is partitioned, drill down to do it to its partitions.
*/
- createForeignKeyCheckTriggers(RelationGetRelid(rel),
- RelationGetRelid(pkrel),
- fkconstraint,
- parentConstr,
- indexOid,
- parentInsTrigger, parentUpdTrigger,
- &insertTriggerOid, &updateTriggerOid);
+ if (fkconstraint->is_enforced)
+ createForeignKeyCheckTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
+ fkconstraint,
+ parentConstr,
+ indexOid,
+ parentInsTrigger, parentUpdTrigger,
+ &insertTriggerOid, &updateTriggerOid);
if (rel->rd_rel->relkind == RELKIND_RELATION)
{
/*
* Tell Phase 3 to check that the constraint is satisfied by existing
- * rows. We can skip this during table creation, when requested
- * explicitly by specifying NOT VALID in an ADD FOREIGN KEY command,
- * and when we're recreating a constraint following a SET DATA TYPE
- * operation that did not impugn its validity.
+ * rows. We can skip this during table creation, when constraint is
+ * specified as NOT ENFORCED, when requested explicitly by specifying
+ * NOT VALID in an ADD FOREIGN KEY command, and when we're recreating
+ * a constraint following a SET DATA TYPE operation that did not
+ * impugn its validity.
*/
- if (wqueue && !old_check_ok && !fkconstraint->skip_validation)
+ if (wqueue && !old_check_ok && !fkconstraint->skip_validation &&
+ fkconstraint->is_enforced)
{
NewConstraint *newcon;
AlteredTableInfo *tab;
@@ -10976,8 +10993,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
AttrNumber confdelsetcols[INDEX_MAX_KEYS];
Constraint *fkconstraint;
ObjectAddress address;
- Oid deleteTriggerOid,
- updateTriggerOid;
+ Oid deleteTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(constrOid));
if (!HeapTupleIsValid(tuple))
@@ -11037,8 +11054,9 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
fkconstraint->fk_del_set_cols = NIL;
fkconstraint->old_conpfeqop = NIL;
fkconstraint->old_pktable_oid = InvalidOid;
+ fkconstraint->is_enforced = constrForm->conenforced;
fkconstraint->skip_validation = false;
- fkconstraint->initially_valid = true;
+ fkconstraint->initially_valid = constrForm->convalidated;
/* set up colnames that are used to generate the constraint name */
for (int i = 0; i < numfks; i++)
@@ -11066,9 +11084,10 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
* parent OIDs for similar triggers that will be created on the
* partition in addFkRecurseReferenced().
*/
- GetForeignKeyActionTriggers(trigrel, constrOid,
- constrForm->confrelid, constrForm->conrelid,
- &deleteTriggerOid, &updateTriggerOid);
+ if (constrForm->conenforced)
+ GetForeignKeyActionTriggers(trigrel, constrOid,
+ constrForm->confrelid, constrForm->conrelid,
+ &deleteTriggerOid, &updateTriggerOid);
/* Add this constraint ... */
address = addFkConstraint(addFkReferencedSide,
@@ -11234,17 +11253,18 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
mapped_conkey[i] = attmap->attnums[conkey[i] - 1];
/*
- * Get the "check" triggers belonging to the constraint to pass as
- * parent OIDs for similar triggers that will be created on the
- * partition in addFkRecurseReferencing(). They are also passed to
- * tryAttachPartitionForeignKey() below to simply assign as parents to
- * the partition's existing "check" triggers, that is, if the
- * corresponding constraints is deemed attachable to the parent
- * constraint.
+ * Get the "check" triggers belonging to the constraint, if it is
+ * enforced, to pass as parent OIDs for similar triggers that will be
+ * created on the partition in addFkRecurseReferencing(). They are
+ * also passed to tryAttachPartitionForeignKey() below to simply
+ * assign as parents to the partition's existing "check" triggers,
+ * that is, if the corresponding constraints is deemed attachable to
+ * the parent constraint.
*/
- GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
- constrForm->confrelid, constrForm->conrelid,
- &insertTriggerOid, &updateTriggerOid);
+ if (constrForm->conenforced)
+ GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
+ constrForm->confrelid, constrForm->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
/*
* Before creating a new constraint, see whether any existing FKs are
@@ -11297,6 +11317,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
fkconstraint->fk_del_set_cols = NIL;
fkconstraint->old_conpfeqop = NIL;
fkconstraint->old_pktable_oid = InvalidOid;
+ fkconstraint->is_enforced = constrForm->conenforced;
fkconstraint->skip_validation = false;
fkconstraint->initially_valid = constrForm->convalidated;
for (int i = 0; i < numfks; i++)
@@ -11414,6 +11435,7 @@ tryAttachPartitionForeignKey(List **wqueue,
if (OidIsValid(partConstr->conparentid) ||
partConstr->condeferrable != parentConstr->condeferrable ||
partConstr->condeferred != parentConstr->condeferred ||
+ partConstr->conenforced != parentConstr->conenforced ||
partConstr->confupdtype != parentConstr->confupdtype ||
partConstr->confdeltype != parentConstr->confdeltype ||
partConstr->confmatchtype != parentConstr->confmatchtype)
@@ -11457,6 +11479,7 @@ AttachPartitionForeignKey(List **wqueue,
bool queueValidation;
Oid partConstrFrelid;
Oid partConstrRelid;
+ bool parentConstrIsEnforced;
Oid insertTriggerOid,
updateTriggerOid;
@@ -11466,6 +11489,7 @@ AttachPartitionForeignKey(List **wqueue,
if (!HeapTupleIsValid(parentConstrTup))
elog(ERROR, "cache lookup failed for constraint %u", parentConstrOid);
parentConstr = (Form_pg_constraint) GETSTRUCT(parentConstrTup);
+ parentConstrIsEnforced = parentConstr->conenforced;
/* Fetch the child constraint tuple */
partcontup = SearchSysCache1(CONSTROID,
@@ -11500,17 +11524,21 @@ AttachPartitionForeignKey(List **wqueue,
/*
* Like the constraint, attach partition's "check" triggers to the
- * corresponding parent triggers.
+ * corresponding parent triggers if the constraint is enforced.
+ * Non-enforced constraints do not have these triggers.
*/
- GetForeignKeyCheckTriggers(trigrel,
- partConstrOid, partConstrFrelid, partConstrRelid,
- &insertTriggerOid, &updateTriggerOid);
- Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
- TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
- RelationGetRelid(partition));
- Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
- TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
- RelationGetRelid(partition));
+ if (parentConstrIsEnforced)
+ {
+ GetForeignKeyCheckTriggers(trigrel,
+ partConstrOid, partConstrFrelid, partConstrRelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
+ RelationGetRelid(partition));
+ Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
+ RelationGetRelid(partition));
+ }
/*
* If the referenced table is partitioned, then the partition we're
@@ -11639,6 +11667,10 @@ RemoveInheritedConstraint(Relation conrel, Relation trigrel, Oid conoid,
*
* The subroutine for tryAttachPartitionForeignKey handles the deletion of
* action triggers for the foreign key constraint.
+ *
+ * If valid confrelid and conrelid values are not provided, the respective
+ * trigger check will be skipped, and the trigger will be considered for
+ * removal.
*/
static void
DropForeignKeyConstraintTriggers(Relation trigrel, Oid conoid, Oid confrelid,
@@ -11659,10 +11691,27 @@ DropForeignKeyConstraintTriggers(Relation trigrel, Oid conoid, Oid confrelid,
Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
ObjectAddress trigger;
- if (trgform->tgconstrrelid != conrelid)
+ /* Invalid if trigger is not for a referential integrity constraint */
+ if (!OidIsValid(trgform->tgconstrrelid))
continue;
- if (trgform->tgrelid != confrelid)
+ if (OidIsValid(conrelid) && trgform->tgconstrrelid != conrelid)
continue;
+ if (OidIsValid(confrelid) && trgform->tgrelid != confrelid)
+ continue;
+
+ /* We should be droping trigger related to foreign ke constraint */
+ Assert(trgform->tgfoid == F_RI_FKEY_CHECK_INS ||
+ trgform->tgfoid == F_RI_FKEY_CHECK_UPD ||
+ trgform->tgfoid == F_RI_FKEY_CASCADE_DEL ||
+ trgform->tgfoid == F_RI_FKEY_CASCADE_UPD ||
+ trgform->tgfoid == F_RI_FKEY_RESTRICT_DEL ||
+ trgform->tgfoid == F_RI_FKEY_RESTRICT_UPD ||
+ trgform->tgfoid == F_RI_FKEY_SETNULL_DEL ||
+ trgform->tgfoid == F_RI_FKEY_SETNULL_UPD ||
+ trgform->tgfoid == F_RI_FKEY_SETDEFAULT_DEL ||
+ trgform->tgfoid == F_RI_FKEY_SETDEFAULT_UPD ||
+ trgform->tgfoid == F_RI_FKEY_NOACTION_DEL ||
+ trgform->tgfoid == F_RI_FKEY_NOACTION_UPD);
/*
* The constraint is originally set up to contain this trigger as an
@@ -11865,10 +11914,19 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
currcon = (Form_pg_constraint) GETSTRUCT(contuple);
if (currcon->contype != CONSTRAINT_FOREIGN)
- ereport(ERROR,
- (errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("constraint \"%s\" of relation \"%s\" is not a foreign key constraint",
+ {
+ if (currcon->conenforced != cmdcon->is_enforced)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot alter enforceability of constraint \"%s\" of relation \"%s\"",
+ cmdcon->conname, RelationGetRelationName(rel)),
+ errdetail("Enforceability can only be altered for foreign key constraints.")));
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("constraint \"%s\" of relation \"%s\" is not a foreign key constraint",
cmdcon->conname, RelationGetRelationName(rel))));
+ }
/*
* If it's not the topmost constraint, raise an error.
@@ -11922,11 +11980,13 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
address = InvalidObjectAddress;
if (currcon->condeferrable != cmdcon->deferrable ||
currcon->condeferred != cmdcon->initdeferred ||
+ currcon->conenforced != cmdcon->is_enforced ||
rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
if (ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, currcon->conrelid,
currcon->confrelid, contuple, &otherrelids,
- lockmode))
+ lockmode, InvalidOid, InvalidOid, InvalidOid,
+ InvalidOid))
ObjectAddressSet(address, ConstraintRelationId, currcon->oid);
}
@@ -11961,7 +12021,10 @@ static bool
ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
const Oid fkrelid, const Oid pkrelid,
HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode)
+ LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger)
{
Form_pg_constraint currcon;
Oid conoid;
@@ -11985,7 +12048,8 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
* silently do nothing.
*/
if (currcon->condeferrable != cmdcon->deferrable ||
- currcon->condeferred != cmdcon->initdeferred)
+ currcon->condeferred != cmdcon->initdeferred ||
+ currcon->conenforced != cmdcon->is_enforced)
{
HeapTuple copyTuple;
Form_pg_constraint copy_con;
@@ -11994,6 +12058,18 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
copy_con->condeferrable = cmdcon->deferrable;
copy_con->condeferred = cmdcon->initdeferred;
+ copy_con->conenforced = cmdcon->is_enforced;
+
+ /*
+ * NB: The convalidated status is irrelevant when the constraint is
+ * set to NOT ENFORCED, but for consistency, it should be set
+ * accordingly. This ensures that if the constraint is later changed
+ * to ENFORCED, it will automatically be in the correct NOT VALID
+ * state.
+ */
+ if (!cmdcon->is_enforced)
+ copy_con->convalidated = false;
+
CatalogTupleUpdate(conrel, ©Tuple->t_self, copyTuple);
InvokeObjectPostAlterHook(ConstraintRelationId,
@@ -12004,32 +12080,158 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
/* Make new constraint flags visible to others */
CacheInvalidateRelcache(rel);
+ }
+ if (currcon->conenforced != cmdcon->is_enforced)
+ {
+ ATExecAlterConstrEnforceability(cmdcon, conrel, tgrel, fkrelid, pkrelid,
+ contuple, otherrelids, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
+ }
+ else
+ {
/*
* Now we need to update the multiple entries in pg_trigger that
* implement the constraint.
*/
- AlterConstrTriggerDeferrability(conoid, tgrel, rel, cmdcon->deferrable,
- cmdcon->initdeferred, otherrelids);
+ AlterConstrTriggerDeferrability(conoid, tgrel,
+ RelationGetRelid(rel),
+ cmdcon->deferrable,
+ cmdcon->initdeferred,
+ otherrelids);
+
+ /*
+ * If the table at either end of the constraint is partitioned, we
+ * need to recurse and handle every constraint that is a child of this
+ * one.
+ *
+ * (This assumes that the recurse flag is forcibly set for partitioned
+ * tables, and not set for legacy inheritance, though we don't check
+ * for that here.)
+ */
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)
+ ATExecAlterChildConstr(cmdcon, conrel, tgrel, fkrelid, pkrelid,
+ contuple, otherrelids, lockmode);
}
- /*
- * If the table at either end of the constraint is partitioned, we need to
- * recurse and handle every constraint that is a child of this one.
- *
- * (This assumes that the recurse flag is forcibly set for partitioned
- * tables, and not set for legacy inheritance, though we don't check for
- * that here.)
- */
- if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
- get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)
- ATExecAlterChildConstr(cmdcon, conrel, tgrel, fkrelid, pkrelid,
- contuple, otherrelids, lockmode);
table_close(rel, NoLock);
return changed;
}
+/*
+ * A subroutine of ATExecAlterConstrRecurse that updates the enforceability of
+ * a foreign key constraint. Depending on whether the constraint is being set
+ * to enforced or not enforced, it creates or drops the trigger accordingly.
+ *
+ * The arguments to this function have the same meaning as the arguments to
+ * ATExecAlterConstrRecurse.
+ */
+static void
+ATExecAlterConstrEnforceability(Constraint *cmdcon, Relation conrel,
+ Relation tgrel, const Oid fkrelid,
+ const Oid pkrelid, HeapTuple contuple,
+ List **otherrelids, LOCKMODE lockmode,
+ Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger)
+{
+ Form_pg_constraint currcon;
+ Oid conoid;
+
+ currcon = (Form_pg_constraint) GETSTRUCT(contuple);
+ conoid = currcon->oid;
+
+ /* Drop triggers */
+ if (!cmdcon->is_enforced)
+ {
+ /*
+ * When setting a constraint to NOT ENFORCED, the constraint triggers
+ * need to be dropped. Therefore, we must process the child relations
+ * first, followed by the parent, to account for dependencies.
+ */
+ if (get_rel_relkind(currcon->conrelid) == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE)
+ ATExecAlterChildConstr(cmdcon, conrel, tgrel, fkrelid, pkrelid,
+ contuple, otherrelids, lockmode);
+
+ /* Drop all the triggers */
+ DropForeignKeyConstraintTriggers(tgrel, conoid, InvalidOid, InvalidOid);
+ }
+ else /* Create triggers */
+ {
+ Oid ReferencedDelTriggerOid = InvalidOid,
+ ReferencedUpdTriggerOid = InvalidOid,
+ ReferencingInsTriggerOid = InvalidOid,
+ ReferencingUpdTriggerOid = InvalidOid;
+
+ /* Update the foreign key action required for trigger creation. */
+ cmdcon->fk_matchtype = currcon->confmatchtype;
+ cmdcon->fk_upd_action = currcon->confupdtype;
+ cmdcon->fk_del_action = currcon->confdeltype;
+
+ /* Create referenced triggers */
+ if (currcon->conrelid == fkrelid)
+ createForeignKeyActionTriggers(currcon->conrelid,
+ currcon->confrelid,
+ cmdcon,
+ conoid,
+ currcon->conindid,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ &ReferencedDelTriggerOid,
+ &ReferencedUpdTriggerOid);
+
+ /* Create referencing triggers */
+ if (currcon->confrelid == pkrelid)
+ createForeignKeyCheckTriggers(currcon->conrelid,
+ pkrelid,
+ cmdcon,
+ conoid,
+ currcon->conindid,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger,
+ &ReferencingInsTriggerOid,
+ &ReferencingUpdTriggerOid);
+
+ /*
+ * If the table at either end of the constraint is partitioned, we
+ * need to recurse and create triggers for each constraint that is a
+ * child of this one.
+ */
+ if (get_rel_relkind(currcon->conrelid) == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE)
+ {
+ ScanKeyData pkey;
+ SysScanDesc pscan;
+ HeapTuple childtup;
+
+ ScanKeyInit(&pkey,
+ Anum_pg_constraint_conparentid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+
+ pscan = systable_beginscan(conrel, ConstraintParentIndexId,
+ true, NULL, 1, &pkey);
+
+ while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
+ ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, fkrelid, pkrelid,
+ childtup, otherrelids, lockmode,
+ ReferencedDelTriggerOid,
+ ReferencedUpdTriggerOid,
+ ReferencingInsTriggerOid,
+ ReferencingUpdTriggerOid);
+
+ systable_endscan(pscan);
+ }
+ }
+}
+
/*
* A subroutine of ATExecAlterConstrRecurse that updated constraint trigger's
* deferrability.
@@ -12038,7 +12240,7 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
* ATExecAlterConstrRecurse.
*/
static void
-AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
+AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Oid fkrel,
bool deferrable, bool initdeferred,
List **otherrelids)
{
@@ -12064,7 +12266,7 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
* other rels that don't have a trigger whose properties change, but
* let's be conservative.)
*/
- if (tgform->tgrelid != RelationGetRelid(rel))
+ if (tgform->tgrelid != fkrel)
*otherrelids = list_append_unique_oid(*otherrelids,
tgform->tgrelid);
@@ -12127,7 +12329,8 @@ ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, fkrelid, pkrelid,
- childtup, otherrelids, lockmode);
+ childtup, otherrelids, lockmode, InvalidOid,
+ InvalidOid, InvalidOid, InvalidOid);
systable_endscan(pscan);
}
@@ -20185,8 +20388,6 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
Form_pg_constraint conform;
- Oid insertTriggerOid,
- updateTriggerOid;
contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
if (!HeapTupleIsValid(contup))
@@ -20213,17 +20414,25 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
/*
* Also, look up the partition's "check" triggers corresponding to the
- * constraint being detached and detach them from the parent triggers.
+ * enforced constraint being detached and detach them from the parent
+ * triggers. Non-enforced constraints do not have these triggers;
+ * therefore, this step is not needed.
*/
- GetForeignKeyCheckTriggers(trigrel,
- fk->conoid, fk->confrelid, fk->conrelid,
- &insertTriggerOid, &updateTriggerOid);
- Assert(OidIsValid(insertTriggerOid));
- TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
- RelationGetRelid(partRel));
- Assert(OidIsValid(updateTriggerOid));
- TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
- RelationGetRelid(partRel));
+ if (fk->conenforced)
+ {
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ Assert(OidIsValid(updateTriggerOid));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ }
/*
* Lastly, create the action triggers on the referenced table, using
@@ -20263,8 +20472,9 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
fkconstraint->conname = pstrdup(NameStr(conform->conname));
fkconstraint->deferrable = conform->condeferrable;
fkconstraint->initdeferred = conform->condeferred;
+ fkconstraint->is_enforced = conform->conenforced;
fkconstraint->skip_validation = true;
- fkconstraint->initially_valid = true;
+ fkconstraint->initially_valid = conform->convalidated;
/* a few irrelevant fields omitted here */
fkconstraint->pktable = NULL;
fkconstraint->fk_attrs = NIL;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index d7f9c00c409..37082bc511e 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -2666,7 +2666,7 @@ alter_table_cmd:
processCASbits($4, @4, "ALTER CONSTRAINT statement",
&c->deferrable,
&c->initdeferred,
- NULL, NULL, NULL, yyscanner);
+ &c->is_enforced, NULL, NULL, yyscanner);
$$ = (Node *) n;
}
/* ALTER TABLE <name> VALIDATE CONSTRAINT ... */
@@ -4035,6 +4035,7 @@ ColConstraintElem:
n->is_enforced = true;
n->skip_validation = false;
n->initially_valid = true;
+ n->is_enforced = true;
$$ = (Node *) n;
}
;
@@ -4309,8 +4310,8 @@ ConstraintElem:
n->fk_del_set_cols = ($11)->deleteAction->cols;
processCASbits($12, @12, "FOREIGN KEY",
&n->deferrable, &n->initdeferred,
- NULL, &n->skip_validation, NULL,
- yyscanner);
+ &n->is_enforced, &n->skip_validation,
+ NULL, yyscanner);
n->initially_valid = !n->skip_validation;
$$ = (Node *) n;
}
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index ca028d2a66d..170f8032d59 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -2961,8 +2961,10 @@ transformFKConstraints(CreateStmtContext *cxt,
/*
* If CREATE TABLE or adding a column with NULL default, we can safely
- * skip validation of FK constraints, and nonetheless mark them valid.
- * (This will override any user-supplied NOT VALID flag.)
+ * skip validation of FK constraints, and mark them as valid based on the
+ * constraint enforcement flag, since NOT ENFORCED constraints must always
+ * be marked as NOT VALID. (This will override any user-supplied NOT VALID
+ * flag.)
*/
if (skipValidation)
{
@@ -2971,7 +2973,7 @@ transformFKConstraints(CreateStmtContext *cxt,
Constraint *constraint = (Constraint *) lfirst(fkclist);
constraint->skip_validation = true;
- constraint->initially_valid = true;
+ constraint->initially_valid = constraint->is_enforced;
}
}
@@ -3966,7 +3968,8 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
case CONSTR_ATTR_ENFORCED:
if (lastprimarycon == NULL ||
- lastprimarycon->contype != CONSTR_CHECK)
+ (lastprimarycon->contype != CONSTR_CHECK &&
+ lastprimarycon->contype != CONSTR_FOREIGN))
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("misplaced ENFORCED clause"),
@@ -3982,7 +3985,8 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
case CONSTR_ATTR_NOT_ENFORCED:
if (lastprimarycon == NULL ||
- lastprimarycon->contype != CONSTR_CHECK)
+ (lastprimarycon->contype != CONSTR_CHECK &&
+ lastprimarycon->contype != CONSTR_FOREIGN))
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("misplaced NOT ENFORCED clause"),
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 485cd2178bc..cb3c19c7448 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4689,6 +4689,7 @@ RelationGetFKeyList(Relation relation)
info->conoid = constraint->oid;
info->conrelid = constraint->conrelid;
info->confrelid = constraint->confrelid;
+ info->conenforced = constraint->conenforced;
DeconstructFkConstraintRow(htup, &info->nkeys,
info->conkey,
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 33d1e4a4e2e..c707ebec7b6 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -284,6 +284,9 @@ typedef struct ForeignKeyCacheInfo
/* number of columns in the foreign key */
int nkeys;
+ /* Is enforced ? */
+ bool conenforced;
+
/*
* these arrays each have nkeys valid entries:
*/
diff --git a/src/test/regress/expected/constraints.out b/src/test/regress/expected/constraints.out
index 692a69fe457..6d81a755a42 100644
--- a/src/test/regress/expected/constraints.out
+++ b/src/test/regress/expected/constraints.out
@@ -744,15 +744,11 @@ CREATE TABLE UNIQUE_NOTEN_TBL(i int UNIQUE NOT ENFORCED);
ERROR: misplaced NOT ENFORCED clause
LINE 1: CREATE TABLE UNIQUE_NOTEN_TBL(i int UNIQUE NOT ENFORCED);
^
--- XXX: error message is misleading here
ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
-ERROR: ALTER CONSTRAINT statement constraints cannot be marked ENFORCED
-LINE 1: ...TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
- ^
+ERROR: constraint "unique_tbl_i_key" of relation "unique_tbl" is not a foreign key constraint
ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORCED;
-ERROR: ALTER CONSTRAINT statement constraints cannot be marked NOT ENFORCED
-LINE 1: ...ABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORC...
- ^
+ERROR: cannot alter enforceability of constraint "unique_tbl_i_key" of relation "unique_tbl"
+DETAIL: Enforceability can only be altered for foreign key constraints.
DROP TABLE unique_tbl;
--
-- EXCLUDE constraints
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 374dcb266e7..c8b91f45146 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -1,12 +1,44 @@
--
-- FOREIGN KEY
--
--- MATCH FULL
+-- NOT ENFORCED
--
-- First test, check and cascade
--
CREATE TABLE PKTABLE ( ptest1 int PRIMARY KEY, ptest2 text );
-CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE, ftest2 int );
+CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE NOT ENFORCED,
+ ftest2 int );
+-- Inserting into the foreign key table will not result in an error, even if
+-- there is no matching key in the referenced table.
+INSERT INTO FKTABLE VALUES (1, 2);
+INSERT INTO FKTABLE VALUES (2, 3);
+-- Check FKTABLE
+SELECT * FROM FKTABLE;
+ ftest1 | ftest2
+--------+--------
+ 1 | 2
+ 2 | 3
+(2 rows)
+
+-- Changing it back to ENFORCED will leave the constraint in the NOT VALID state
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+-- Which needs to be explicitly validated.
+ALTER TABLE FKTABLE VALIDATE CONSTRAINT fktable_ftest1_fkey;
+ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_fkey"
+DETAIL: Key (ftest1)=(1) is not present in table "pktable".
+-- Remove any existing rows that violate the constraint, then attempt to change
+-- it.
+TRUNCATE FKTABLE;
+ALTER TABLE FKTABLE VALIDATE CONSTRAINT fktable_ftest1_fkey;
+-- Any further inserts will fail due to the enforcement.
+INSERT INTO FKTABLE VALUES (3, 4);
+ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_fkey"
+DETAIL: Key (ftest1)=(3) is not present in table "pktable".
+--
+-- MATCH FULL
+--
+-- First test, check and cascade
+--
-- Insert test data into PKTABLE
INSERT INTO PKTABLE VALUES (1, 'Test1');
INSERT INTO PKTABLE VALUES (2, 'Test2');
@@ -351,6 +383,8 @@ INSERT INTO FKTABLE VALUES (1, NULL);
ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL;
ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_ftest2_fkey"
DETAIL: MATCH FULL does not allow mixing of null and nonnull key values.
+-- NOT ENFORCED will bypass the initial check.
+ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL NOT ENFORCED;
DROP TABLE FKTABLE;
DROP TABLE PKTABLE;
-- MATCH SIMPLE
@@ -1276,13 +1310,28 @@ INSERT INTO fktable VALUES (0, 20);
ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_fk_fkey"
DETAIL: Key (fk)=(20) is not present in table "pktable".
COMMIT;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT ENFORCED;
+BEGIN;
+-- doesn't match FK, no error.
+UPDATE pktable SET id = 10 WHERE id = 5;
+-- doesn't match PK, no error.
+INSERT INTO fktable VALUES (0, 20);
+ROLLBACK;
-- try additional syntax
-ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE ENFORCED;
-- illegal option
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE INITIALLY DEFERRED;
ERROR: constraint declared INITIALLY DEFERRED must be DEFERRABLE
LINE 1: ...e ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE INITIALLY ...
^
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORCED;
+ERROR: conflicting constraint properties
+LINE 1: ...fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORC...
+ ^
+CREATE TEMP TABLE fktable2 (fk int references pktable ENFORCED NOT ENFORCED);
+ERROR: multiple ENFORCED/NOT ENFORCED clauses not allowed
+LINE 1: ...ABLE fktable2 (fk int references pktable ENFORCED NOT ENFORC...
+ ^
-- test order of firing of FK triggers when several RI-induced changes need to
-- be made to the same row. This was broken by subtransaction-related
-- changes in 8.0.
@@ -1580,10 +1629,14 @@ ALTER TABLE fk_partitioned_fk DROP COLUMN fdrop1;
CREATE TABLE fk_partitioned_fk_1 (fdrop1 int, fdrop2 int, a int, fdrop3 int, b int);
ALTER TABLE fk_partitioned_fk_1 DROP COLUMN fdrop1, DROP COLUMN fdrop2, DROP COLUMN fdrop3;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FROM (0,0) TO (1000,1000);
-ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk;
+ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
+ALTER TABLE fk_partitioned_fk_2 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
+-- Changing to ENFORCED does not alter the validation status; it must be explicitly validated.
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+ALTER TABLE fk_partitioned_fk VALIDATE CONSTRAINT fk_partitioned_fk_a_b_fkey ;
CREATE TABLE fk_partitioned_fk_3 (fdrop1 int, fdrop2 int, fdrop3 int, fdrop4 int, b int, a int)
PARTITION BY HASH (a);
ALTER TABLE fk_partitioned_fk_3 DROP COLUMN fdrop1, DROP COLUMN fdrop2,
@@ -1606,10 +1659,10 @@ INSERT INTO fk_partitioned_fk_1 (a,b) VALUES (500, 501);
ERROR: insert or update on table "fk_partitioned_fk_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
DETAIL: Key (a, b)=(500, 501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (1500, 1501);
-ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_2_a_b_fkey"
DETAIL: Key (a, b)=(1500, 1501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk_2 (a,b) VALUES (1500, 1501);
-ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_2_a_b_fkey"
DETAIL: Key (a, b)=(1500, 1501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (2500, 2502);
ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
@@ -1659,6 +1712,37 @@ Indexes:
Referenced by:
TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b)
+-- Check the exsting FK trigger
+CREATE TEMP TABLE tmp_trg_info AS SELECT tgtype, tgrelid::regclass, tgconstraint
+FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+SELECT count(1) FROM tmp_trg_info;
+ count
+-------
+ 14
+(1 row)
+
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+-- No triggers
+SELECT count(1) FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+ count
+-------
+ 0
+(1 row)
+
+-- Changing it back to ENFORCED will recreate the necessary triggers.
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+-- Should be exactly the same number of triggers found as before
+SELECT COUNT(1) FROM pg_trigger pgt JOIN tmp_trg_info tt
+ON (pgt.tgtype = tt.tgtype AND pgt.tgrelid = tt.tgrelid AND pgt.tgconstraint = tt.tgconstraint);
+ count
+-------
+ 14
+(1 row)
+
ALTER TABLE fk_partitioned_fk DROP CONSTRAINT fk_partitioned_fk_a_b_fkey;
-- done.
DROP TABLE fk_notpartitioned_pk, fk_partitioned_fk;
@@ -1956,6 +2040,40 @@ Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
Foreign-key constraints:
TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
+DROP TABLE fk_partitioned_fk_2;
+CREATE TABLE fk_partitioned_fk_2 (b int, a int,
+ FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
+BEGIN;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have two constraints
+\d fk_partitioned_fk_2
+ Table "public.fk_partitioned_fk_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ b | integer | | |
+ a | integer | | |
+Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
+Foreign-key constraints:
+ "fk_partitioned_fk_2_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED
+ TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
+
+DROP TABLE fk_partitioned_fk_2;
+ROLLBACK;
+BEGIN;
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have only one constraint
+\d fk_partitioned_fk_2
+ Table "public.fk_partitioned_fk_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ b | integer | | |
+ a | integer | | |
+Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
+Foreign-key constraints:
+ TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED
+
+ROLLBACK;
DROP TABLE fk_partitioned_fk_2;
CREATE TABLE fk_partitioned_fk_4 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE) PARTITION BY RANGE (b, a);
CREATE TABLE fk_partitioned_fk_4_1 PARTITION OF fk_partitioned_fk_4 FOR VALUES FROM (1,1) TO (100,100);
diff --git a/src/test/regress/sql/constraints.sql b/src/test/regress/sql/constraints.sql
index d6742f83fb9..e61e7324768 100644
--- a/src/test/regress/sql/constraints.sql
+++ b/src/test/regress/sql/constraints.sql
@@ -534,7 +534,6 @@ SELECT * FROM unique_tbl;
-- enforcibility cannot be specified or set for unique constrain
CREATE TABLE UNIQUE_EN_TBL(i int UNIQUE ENFORCED);
CREATE TABLE UNIQUE_NOTEN_TBL(i int UNIQUE NOT ENFORCED);
--- XXX: error message is misleading here
ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORCED;
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index bc0adb8cfe9..f3971c620c3 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -2,13 +2,40 @@
-- FOREIGN KEY
--
--- MATCH FULL
+-- NOT ENFORCED
--
-- First test, check and cascade
--
CREATE TABLE PKTABLE ( ptest1 int PRIMARY KEY, ptest2 text );
-CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE, ftest2 int );
+CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE NOT ENFORCED,
+ ftest2 int );
+-- Inserting into the foreign key table will not result in an error, even if
+-- there is no matching key in the referenced table.
+INSERT INTO FKTABLE VALUES (1, 2);
+INSERT INTO FKTABLE VALUES (2, 3);
+
+-- Check FKTABLE
+SELECT * FROM FKTABLE;
+
+-- Changing it back to ENFORCED will leave the constraint in the NOT VALID state
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+-- Which needs to be explicitly validated.
+ALTER TABLE FKTABLE VALIDATE CONSTRAINT fktable_ftest1_fkey;
+
+-- Remove any existing rows that violate the constraint, then attempt to change
+-- it.
+TRUNCATE FKTABLE;
+ALTER TABLE FKTABLE VALIDATE CONSTRAINT fktable_ftest1_fkey;
+
+-- Any further inserts will fail due to the enforcement.
+INSERT INTO FKTABLE VALUES (3, 4);
+
+--
+-- MATCH FULL
+--
+-- First test, check and cascade
+--
-- Insert test data into PKTABLE
INSERT INTO PKTABLE VALUES (1, 'Test1');
INSERT INTO PKTABLE VALUES (2, 'Test2');
@@ -230,6 +257,9 @@ INSERT INTO FKTABLE VALUES (1, NULL);
ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL;
+-- NOT ENFORCED will bypass the initial check.
+ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL NOT ENFORCED;
+
DROP TABLE FKTABLE;
DROP TABLE PKTABLE;
@@ -968,10 +998,23 @@ INSERT INTO fktable VALUES (0, 20);
COMMIT;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT ENFORCED;
+
+BEGIN;
+
+-- doesn't match FK, no error.
+UPDATE pktable SET id = 10 WHERE id = 5;
+-- doesn't match PK, no error.
+INSERT INTO fktable VALUES (0, 20);
+
+ROLLBACK;
+
-- try additional syntax
-ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE ENFORCED;
-- illegal option
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORCED;
+CREATE TEMP TABLE fktable2 (fk int references pktable ENFORCED NOT ENFORCED);
-- test order of firing of FK triggers when several RI-induced changes need to
-- be made to the same row. This was broken by subtransaction-related
@@ -1182,11 +1225,16 @@ ALTER TABLE fk_partitioned_fk DROP COLUMN fdrop1;
CREATE TABLE fk_partitioned_fk_1 (fdrop1 int, fdrop2 int, a int, fdrop3 int, b int);
ALTER TABLE fk_partitioned_fk_1 DROP COLUMN fdrop1, DROP COLUMN fdrop2, DROP COLUMN fdrop3;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FROM (0,0) TO (1000,1000);
-ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk;
+ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
+ALTER TABLE fk_partitioned_fk_2 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
+-- Changing to ENFORCED does not alter the validation status; it must be explicitly validated.
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+ALTER TABLE fk_partitioned_fk VALIDATE CONSTRAINT fk_partitioned_fk_a_b_fkey ;
+
CREATE TABLE fk_partitioned_fk_3 (fdrop1 int, fdrop2 int, fdrop3 int, fdrop4 int, b int, a int)
PARTITION BY HASH (a);
ALTER TABLE fk_partitioned_fk_3 DROP COLUMN fdrop1, DROP COLUMN fdrop2,
@@ -1232,6 +1280,27 @@ UPDATE fk_notpartitioned_pk SET b = 1502 WHERE a = 1500;
UPDATE fk_notpartitioned_pk SET b = 2504 WHERE a = 2500;
-- check psql behavior
\d fk_notpartitioned_pk
+
+-- Check the exsting FK trigger
+CREATE TEMP TABLE tmp_trg_info AS SELECT tgtype, tgrelid::regclass, tgconstraint
+FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+SELECT count(1) FROM tmp_trg_info;
+
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+-- No triggers
+SELECT count(1) FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+
+-- Changing it back to ENFORCED will recreate the necessary triggers.
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+
+-- Should be exactly the same number of triggers found as before
+SELECT COUNT(1) FROM pg_trigger pgt JOIN tmp_trg_info tt
+ON (pgt.tgtype = tt.tgtype AND pgt.tgrelid = tt.tgrelid AND pgt.tgconstraint = tt.tgconstraint);
+
ALTER TABLE fk_partitioned_fk DROP CONSTRAINT fk_partitioned_fk_a_b_fkey;
-- done.
DROP TABLE fk_notpartitioned_pk, fk_partitioned_fk;
@@ -1439,6 +1508,22 @@ ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN
\d fk_partitioned_fk_2
DROP TABLE fk_partitioned_fk_2;
+CREATE TABLE fk_partitioned_fk_2 (b int, a int,
+ FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
+BEGIN;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have two constraints
+\d fk_partitioned_fk_2
+DROP TABLE fk_partitioned_fk_2;
+ROLLBACK;
+BEGIN;
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have only one constraint
+\d fk_partitioned_fk_2
+ROLLBACK;
+DROP TABLE fk_partitioned_fk_2;
+
CREATE TABLE fk_partitioned_fk_4 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE) PARTITION BY RANGE (b, a);
CREATE TABLE fk_partitioned_fk_4_1 PARTITION OF fk_partitioned_fk_4 FOR VALUES FROM (1,1) TO (100,100);
CREATE TABLE fk_partitioned_fk_4_2 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE SET NULL);
--
2.43.5
v11-0007-Merge-the-parent-and-child-constraints-with-diff.patchapplication/x-patch; name=v11-0007-Merge-the-parent-and-child-constraints-with-diff.patchDownload
From 9271dc59ce3f03bf285f6d136220dad4f8b10a11 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Fri, 17 Jan 2025 17:03:59 +0530
Subject: [PATCH v11 7/7] Merge the parent and child constraints with differing
enforcibility.
If an enforced parent constraint is attached to a non-enforced child
constraint, the child constraint will be made enforced, with
validation applied if the parent constraint is validated as well.
Otherwise, a new enforced constraint (with validation, if the parent
constraint is validated) would need to be created on the child table,
which would be unnecessary if a similar constraint already exists and
can be attached.
On the other hand, having a not-enforced parent constraint with an
enforced child constraint does not cause any issues, and no changes
are required.
----
NOTE: This patch is intended to reduce the diff noise from the main
patch and is not meant to be committed separately. It should be
squashed with the main patch that adds ENFORCED/NOT ENFORCED.
----
---
src/backend/commands/tablecmds.c | 157 ++++++++++++++++++++--
src/test/regress/expected/foreign_key.out | 36 ++++-
src/test/regress/sql/foreign_key.sql | 14 ++
3 files changed, 191 insertions(+), 16 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 00cf56d3fc9..0ba8e5422c3 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -11435,7 +11435,6 @@ tryAttachPartitionForeignKey(List **wqueue,
if (OidIsValid(partConstr->conparentid) ||
partConstr->condeferrable != parentConstr->condeferrable ||
partConstr->condeferred != parentConstr->condeferred ||
- partConstr->conenforced != parentConstr->conenforced ||
partConstr->confupdtype != parentConstr->confupdtype ||
partConstr->confdeltype != parentConstr->confdeltype ||
partConstr->confmatchtype != parentConstr->confmatchtype)
@@ -11480,6 +11479,8 @@ AttachPartitionForeignKey(List **wqueue,
Oid partConstrFrelid;
Oid partConstrRelid;
bool parentConstrIsEnforced;
+ bool partConstrIsEnforced;
+ bool partConstrParentIsSet;
Oid insertTriggerOid,
updateTriggerOid;
@@ -11497,9 +11498,79 @@ AttachPartitionForeignKey(List **wqueue,
if (!HeapTupleIsValid(partcontup))
elog(ERROR, "cache lookup failed for constraint %u", partConstrOid);
partConstr = (Form_pg_constraint) GETSTRUCT(partcontup);
+ partConstrIsEnforced = partConstr->conenforced;
partConstrFrelid = partConstr->confrelid;
partConstrRelid = partConstr->conrelid;
+ /*
+ * When the enforcibility of both constraints is the same, no special
+ * action is taken, and the flow remains unchanged. However, there are two
+ * scenarios to consider when it differs:
+ *
+ * 1. Parent constraint is not-enforced and child constraint is enforced:
+ *
+ * This is allowed because the not-enforced parent constraint does not
+ * have triggers, so no redundancy issues arise with the child constraint,
+ * even if it is enforced. In this case, we allow the child constraint to
+ * remain enforced and keep its trigger intact. This ensures that
+ * referential integrity checks for the child continue as before, even
+ * with the parent constraint not enforced. The relationship between the
+ * two constraints is maintained by setting the parent constraint, which
+ * enables us to locate the child constraint. This is important if the
+ * parent constraint is later changed to enforced, at which point the
+ * necessary trigger will be created for the parent. Any potential
+ * redundancy from these triggers will be addressed accordingly.
+ *
+ * 2. Parent constraint is enforced and child constraint is not-enforced:
+ *
+ * This is not allowed, as it would violate referential integrity. In such
+ * cases, the child constraint will first be made enforced before merging
+ * it with the enforced parent constraint. Then, action triggers and
+ * constraint redundancy will be handled in the usual manner, similar to
+ * how two enforced constraints are merged.
+ */
+ if (!parentConstrIsEnforced && partConstrIsEnforced)
+ {
+ ReleaseSysCache(partcontup);
+ ReleaseSysCache(parentConstrTup);
+
+ ConstraintSetParentConstraint(partConstrOid, parentConstrOid,
+ RelationGetRelid(partition));
+ CommandCounterIncrement();
+
+ return;
+ }
+ else if (parentConstrIsEnforced && !partConstrIsEnforced)
+ {
+ Constraint *cmdcon = makeNode(Constraint);
+ Relation conrel;
+ List *otherrelids = NIL;
+
+ cmdcon->contype = partConstr->contype;
+ cmdcon->conname = NameStr(partConstr->conname);
+ cmdcon->deferrable = partConstr->condeferrable;
+ cmdcon->initdeferred = partConstr->condeferred;
+ cmdcon->is_enforced = true;
+
+ conrel = table_open(ConstraintRelationId, RowExclusiveLock);
+
+ ATExecAlterConstrRecurse(cmdcon, conrel, trigrel, partConstr->conrelid,
+ partConstr->confrelid, partcontup, &otherrelids,
+ AccessExclusiveLock, InvalidOid, InvalidOid,
+ InvalidOid, InvalidOid);
+
+ table_close(conrel, RowExclusiveLock);
+
+ CommandCounterIncrement();
+ }
+
+ /*
+ * The constraint parent shouldn't be set beforehand, or if it's already
+ * set, it should be the specified parent.
+ */
+ partConstrParentIsSet = OidIsValid(partConstr->conparentid);
+ Assert(!partConstrParentIsSet || partConstr->conparentid == parentConstrOid);
+
/*
* Will we need to validate this constraint? A valid parent constraint
* implies that all child constraints have been validated, so if this one
@@ -11519,8 +11590,10 @@ AttachPartitionForeignKey(List **wqueue,
DropForeignKeyConstraintTriggers(trigrel, partConstrOid, partConstrFrelid,
partConstrRelid);
- ConstraintSetParentConstraint(partConstrOid, parentConstrOid,
- RelationGetRelid(partition));
+ /* Skip if the parent is already set */
+ if (!partConstrParentIsSet)
+ ConstraintSetParentConstraint(partConstrOid, parentConstrOid,
+ RelationGetRelid(partition));
/*
* Like the constraint, attach partition's "check" triggers to the
@@ -11566,7 +11639,7 @@ AttachPartitionForeignKey(List **wqueue,
CommandCounterIncrement();
/* If validation is needed, put it in the queue now. */
- if (queueValidation)
+ if (wqueue && queueValidation)
{
Relation conrel;
@@ -12162,6 +12235,17 @@ ATExecAlterConstrEnforceability(Constraint *cmdcon, Relation conrel,
/* Drop all the triggers */
DropForeignKeyConstraintTriggers(tgrel, conoid, InvalidOid, InvalidOid);
+
+ /*
+ * If the referenced table is partitioned, the child constraint we're
+ * changing to not-enforced may have additional pg_constraint rows and
+ * action triggers that remain untouched while this child constraint
+ * is attached to the not-enforced parent. These must now be removed.
+ * For more details, see AttachPartitionForeignKey().
+ */
+ if (OidIsValid(currcon->conparentid) &&
+ get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE)
+ RemoveInheritedConstraint(conrel, tgrel, currcon->conrelid, conoid);
}
else /* Create triggers */
{
@@ -12220,13 +12304,42 @@ ATExecAlterConstrEnforceability(Constraint *cmdcon, Relation conrel,
true, NULL, 1, &pkey);
while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
- ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, fkrelid, pkrelid,
- childtup, otherrelids, lockmode,
- ReferencedDelTriggerOid,
- ReferencedUpdTriggerOid,
- ReferencingInsTriggerOid,
- ReferencingUpdTriggerOid);
+ {
+ Form_pg_constraint childcon;
+ childcon = (Form_pg_constraint) GETSTRUCT(childtup);
+
+ if (childcon->conenforced)
+ {
+
+ /*
+ * The child constraint is attached to the parent
+ * constraint, which is already enforced. Now, as the
+ * parent constraint is being modified to be enforced, some
+ * constraints and action triggers on the child table may
+ * become redundant and need to be removed.
+ */
+ if (currcon->confrelid == pkrelid)
+ {
+ Relation rel = table_open(childcon->conrelid, lockmode);
+
+ AttachPartitionForeignKey(NULL, rel, childcon->oid,
+ conoid,
+ ReferencingInsTriggerOid,
+ ReferencingUpdTriggerOid,
+ tgrel);
+
+ table_close(rel, NoLock);
+ }
+ }
+ else
+ ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, childtup, otherrelids,
+ lockmode, ReferencedDelTriggerOid,
+ ReferencedUpdTriggerOid,
+ ReferencingInsTriggerOid,
+ ReferencingUpdTriggerOid);
+ }
systable_endscan(pscan);
}
}
@@ -20387,7 +20500,9 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
{
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
+ HeapTuple parentContup;
Form_pg_constraint conform;
+ Oid parentConstrIsEnforced;
contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
if (!HeapTupleIsValid(contup))
@@ -20406,12 +20521,34 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
continue;
}
+ /* Get the enforcibility of the parent constraint */
+ parentContup = SearchSysCache1(CONSTROID,
+ ObjectIdGetDatum(conform->conparentid));
+ if (!HeapTupleIsValid(parentContup))
+ elog(ERROR, "cache lookup failed for constraint %u",
+ conform->conparentid);
+ parentConstrIsEnforced =
+ ((Form_pg_constraint) GETSTRUCT(parentContup))->conenforced;
+ ReleaseSysCache(parentContup);
+
/*
* The constraint on this table must be marked no longer a child of
* the parent's constraint, as do its check triggers.
*/
ConstraintSetParentConstraint(fk->conoid, InvalidOid, InvalidOid);
+ /*
+ * Unsetting the parent is sufficient when the parent constraint is
+ * not enforced and the child constraint is enforced, as we link them
+ * by setting the constraint parent, while leaving the rest unchanged.
+ * For more details, see AttachPartitionForeignKey().
+ */
+ if (!parentConstrIsEnforced && fk->conenforced)
+ {
+ ReleaseSysCache(contup);
+ continue;
+ }
+
/*
* Also, look up the partition's "check" triggers corresponding to the
* enforced constraint being detached and detach them from the parent
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index c8b91f45146..ec2058c06d5 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -1643,8 +1643,33 @@ ALTER TABLE fk_partitioned_fk_3 DROP COLUMN fdrop1, DROP COLUMN fdrop2,
DROP COLUMN fdrop3, DROP COLUMN fdrop4;
CREATE TABLE fk_partitioned_fk_3_0 PARTITION OF fk_partitioned_fk_3 FOR VALUES WITH (MODULUS 5, REMAINDER 0);
CREATE TABLE fk_partitioned_fk_3_1 PARTITION OF fk_partitioned_fk_3 FOR VALUES WITH (MODULUS 5, REMAINDER 1);
+-- Merge the not-enforced parent constraint with the enforced and not-enforced child constraints.
+ALTER TABLE fk_partitioned_fk_3_0 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ENFORCED;
+ALTER TABLE fk_partitioned_fk_3_1 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk_3 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+SELECT conname, conenforced, convalidated, conrelid::regclass FROM pg_constraint
+WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+ conname | conenforced | convalidated | conrelid
+--------------------------------+-------------+--------------+-----------------------
+ fk_partitioned_fk_3_0_a_b_fkey | t | t | fk_partitioned_fk_3_0
+ fk_partitioned_fk_3_1_a_b_fkey | f | f | fk_partitioned_fk_3_1
+ fk_partitioned_fk_3_a_b_fkey | f | f | fk_partitioned_fk_3
+(3 rows)
+
+-- Merging an enforced parent constraint (validated) with a not-enforced child
+-- constraint will implicitly change the child constraint to enforced and apply
+-- the validation as well.
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_3
FOR VALUES FROM (2000,2000) TO (3000,3000);
+SELECT conname, conenforced, convalidated, conrelid::regclass FROM pg_constraint
+WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+ conname | conenforced | convalidated | conrelid
+--------------------------------+-------------+--------------+-----------------------
+ fk_partitioned_fk_3_0_a_b_fkey | t | t | fk_partitioned_fk_3_0
+ fk_partitioned_fk_3_1_a_b_fkey | t | t | fk_partitioned_fk_3_1
+ fk_partitioned_fk_3_a_b_fkey | t | t | fk_partitioned_fk_3
+(3 rows)
+
-- Creating a foreign key with ONLY on a partitioned table referencing
-- a non-partitioned table fails.
ALTER TABLE ONLY fk_partitioned_fk ADD FOREIGN KEY (a, b)
@@ -1665,16 +1690,16 @@ INSERT INTO fk_partitioned_fk_2 (a,b) VALUES (1500, 1501);
ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_2_a_b_fkey"
DETAIL: Key (a, b)=(1500, 1501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (2500, 2502);
-ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_3_1_a_b_fkey"
DETAIL: Key (a, b)=(2500, 2502) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk_3 (a,b) VALUES (2500, 2502);
-ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_3_1_a_b_fkey"
DETAIL: Key (a, b)=(2500, 2502) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (2501, 2503);
-ERROR: insert or update on table "fk_partitioned_fk_3_0" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_0" violates foreign key constraint "fk_partitioned_fk_3_0_a_b_fkey"
DETAIL: Key (a, b)=(2501, 2503) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk_3 (a,b) VALUES (2501, 2503);
-ERROR: insert or update on table "fk_partitioned_fk_3_0" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_0" violates foreign key constraint "fk_partitioned_fk_3_0_a_b_fkey"
DETAIL: Key (a, b)=(2501, 2503) is not present in table "fk_notpartitioned_pk".
-- but if we insert the values that make them valid, then they work
INSERT INTO fk_notpartitioned_pk VALUES (500, 501), (1500, 1501),
@@ -1685,7 +1710,7 @@ INSERT INTO fk_partitioned_fk (a,b) VALUES (2500, 2502);
INSERT INTO fk_partitioned_fk (a,b) VALUES (2501, 2503);
-- this update fails because there is no referenced row
UPDATE fk_partitioned_fk SET a = a + 1 WHERE a = 2501;
-ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_3_1_a_b_fkey"
DETAIL: Key (a, b)=(2502, 2503) is not present in table "fk_notpartitioned_pk".
-- but we can fix it thusly:
INSERT INTO fk_notpartitioned_pk (a,b) VALUES (2502, 2503);
@@ -2054,7 +2079,6 @@ ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN
a | integer | | |
Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
Foreign-key constraints:
- "fk_partitioned_fk_2_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED
TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
DROP TABLE fk_partitioned_fk_2;
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index f3971c620c3..baf0fb86296 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -1241,9 +1241,23 @@ ALTER TABLE fk_partitioned_fk_3 DROP COLUMN fdrop1, DROP COLUMN fdrop2,
DROP COLUMN fdrop3, DROP COLUMN fdrop4;
CREATE TABLE fk_partitioned_fk_3_0 PARTITION OF fk_partitioned_fk_3 FOR VALUES WITH (MODULUS 5, REMAINDER 0);
CREATE TABLE fk_partitioned_fk_3_1 PARTITION OF fk_partitioned_fk_3 FOR VALUES WITH (MODULUS 5, REMAINDER 1);
+-- Merge the not-enforced parent constraint with the enforced and not-enforced child constraints.
+ALTER TABLE fk_partitioned_fk_3_0 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ENFORCED;
+ALTER TABLE fk_partitioned_fk_3_1 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk_3 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+
+SELECT conname, conenforced, convalidated, conrelid::regclass FROM pg_constraint
+WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+
+-- Merging an enforced parent constraint (validated) with a not-enforced child
+-- constraint will implicitly change the child constraint to enforced and apply
+-- the validation as well.
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_3
FOR VALUES FROM (2000,2000) TO (3000,3000);
+SELECT conname, conenforced, convalidated, conrelid::regclass FROM pg_constraint
+WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+
-- Creating a foreign key with ONLY on a partitioned table referencing
-- a non-partitioned table fails.
ALTER TABLE ONLY fk_partitioned_fk ADD FOREIGN KEY (a, b)
--
2.43.5
v11-0002-refactor-Split-tryAttachPartitionForeignKey.patchapplication/x-patch; name=v11-0002-refactor-Split-tryAttachPartitionForeignKey.patchDownload
From 25cee55e365f431701d60db58de5c985a6577710 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Wed, 29 Jan 2025 11:35:27 +0530
Subject: [PATCH v11 2/7] refactor: Split tryAttachPartitionForeignKey()
Split tryAttachPartitionForeignKey() into three functions:
AttachPartitionForeignKey(), RemoveInheritedConstraint() and
DropForeignKeyConstraintTriggers(), so they can be reused in the next
patch.
---
src/backend/commands/tablecmds.c | 319 ++++++++++++++++++++-----------
1 file changed, 204 insertions(+), 115 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d617c4bc63d..ae22bdb13be 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -583,6 +583,14 @@ static bool tryAttachPartitionForeignKey(List **wqueue,
Oid parentInsTrigger,
Oid parentUpdTrigger,
Relation trigrel);
+static void AttachPartitionForeignKey(List **wqueue, Relation partition,
+ Oid partConstrOid, Oid parentConstrOid,
+ Oid parentInsTrigger, Oid parentUpdTrigger,
+ Relation trigrel);
+static void RemoveInheritedConstraint(Relation conrel, Relation trigrel,
+ Oid conoid, Oid conrelid);
+static void DropForeignKeyConstraintTriggers(Relation trigrel, Oid conoid,
+ Oid confrelid, Oid conrelid);
static void GetForeignKeyActionTriggers(Relation trigrel,
Oid conoid, Oid confrelid, Oid conrelid,
Oid *deleteTriggerOid,
@@ -11368,12 +11376,6 @@ tryAttachPartitionForeignKey(List **wqueue,
Form_pg_constraint parentConstr;
HeapTuple partcontup;
Form_pg_constraint partConstr;
- bool queueValidation;
- ScanKeyData key;
- SysScanDesc scan;
- HeapTuple trigtup;
- Oid insertTriggerOid,
- updateTriggerOid;
parentConstrTup = SearchSysCache1(CONSTROID,
ObjectIdGetDatum(parentConstrOid));
@@ -11418,6 +11420,59 @@ tryAttachPartitionForeignKey(List **wqueue,
return false;
}
+ ReleaseSysCache(parentConstrTup);
+ ReleaseSysCache(partcontup);
+
+ /* Looks good! Attach this constraint. */
+ AttachPartitionForeignKey(wqueue, partition, fk->conoid,
+ parentConstrOid, parentInsTrigger,
+ parentUpdTrigger, trigrel);
+
+ return true;
+}
+
+/*
+ * AttachPartitionForeignKey
+ *
+ * The subroutine for tryAttachPartitionForeignKey performs the final tasks of
+ * attaching the constraint, removing redundant triggers and entries from
+ * pg_constraint, and setting the constraint's parent.
+ */
+static void
+AttachPartitionForeignKey(List **wqueue,
+ Relation partition,
+ Oid partConstrOid,
+ Oid parentConstrOid,
+ Oid parentInsTrigger,
+ Oid parentUpdTrigger,
+ Relation trigrel)
+{
+ HeapTuple parentConstrTup;
+ Form_pg_constraint parentConstr;
+ HeapTuple partcontup;
+ Form_pg_constraint partConstr;
+ bool queueValidation;
+ Oid partConstrFrelid;
+ Oid partConstrRelid;
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
+ /* Fetch the parent constraint tuple */
+ parentConstrTup = SearchSysCache1(CONSTROID,
+ ObjectIdGetDatum(parentConstrOid));
+ if (!HeapTupleIsValid(parentConstrTup))
+ elog(ERROR, "cache lookup failed for constraint %u", parentConstrOid);
+ parentConstr = (Form_pg_constraint) GETSTRUCT(parentConstrTup);
+
+ /* Fetch the child constraint tuple */
+ partcontup = SearchSysCache1(CONSTROID,
+ ObjectIdGetDatum(partConstrOid));
+ if (!HeapTupleIsValid(partcontup))
+ elog(ERROR, "cache lookup failed for constraint %u", partConstrOid);
+ partConstr = (Form_pg_constraint) GETSTRUCT(partcontup);
+ partConstrFrelid = partConstr->confrelid;
+ partConstrRelid = partConstr->conrelid;
+
/*
* Will we need to validate this constraint? A valid parent constraint
* implies that all child constraints have been validated, so if this one
@@ -11429,50 +11484,15 @@ tryAttachPartitionForeignKey(List **wqueue,
ReleaseSysCache(parentConstrTup);
/*
- * Looks good! Attach this constraint. The action triggers in the new
- * partition become redundant -- the parent table already has equivalent
- * ones, and those will be able to reach the partition. Remove the ones
- * in the partition. We identify them because they have our constraint
- * OID, as well as being on the referenced rel.
+ * The action triggers in the new partition become redundant -- the parent
+ * table already has equivalent ones, and those will be able to reach the
+ * partition. Remove the ones in the partition. We identify them because
+ * they have our constraint OID, as well as being on the referenced rel.
*/
- ScanKeyInit(&key,
- Anum_pg_trigger_tgconstraint,
- BTEqualStrategyNumber, F_OIDEQ,
- ObjectIdGetDatum(fk->conoid));
- scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
- NULL, 1, &key);
- while ((trigtup = systable_getnext(scan)) != NULL)
- {
- Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
- ObjectAddress trigger;
+ DropForeignKeyConstraintTriggers(trigrel, partConstrOid, partConstrFrelid,
+ partConstrRelid);
- if (trgform->tgconstrrelid != fk->conrelid)
- continue;
- if (trgform->tgrelid != fk->confrelid)
- continue;
-
- /*
- * The constraint is originally set up to contain this trigger as an
- * implementation object, so there's a dependency record that links
- * the two; however, since the trigger is no longer needed, we remove
- * the dependency link in order to be able to drop the trigger while
- * keeping the constraint intact.
- */
- deleteDependencyRecordsFor(TriggerRelationId,
- trgform->oid,
- false);
- /* make dependency deletion visible to performDeletion */
- CommandCounterIncrement();
- ObjectAddressSet(trigger, TriggerRelationId,
- trgform->oid);
- performDeletion(&trigger, DROP_RESTRICT, 0);
- /* make trigger drop visible, in case the loop iterates */
- CommandCounterIncrement();
- }
-
- systable_endscan(scan);
-
- ConstraintSetParentConstraint(fk->conoid, parentConstrOid,
+ ConstraintSetParentConstraint(partConstrOid, parentConstrOid,
RelationGetRelid(partition));
/*
@@ -11480,7 +11500,7 @@ tryAttachPartitionForeignKey(List **wqueue,
* corresponding parent triggers.
*/
GetForeignKeyCheckTriggers(trigrel,
- fk->conoid, fk->confrelid, fk->conrelid,
+ partConstrOid, partConstrFrelid, partConstrRelid,
&insertTriggerOid, &updateTriggerOid);
Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
@@ -11494,72 +11514,12 @@ tryAttachPartitionForeignKey(List **wqueue,
* attaching now has extra pg_constraint rows and action triggers that are
* no longer needed. Remove those.
*/
- if (get_rel_relkind(fk->confrelid) == RELKIND_PARTITIONED_TABLE)
+ if (get_rel_relkind(partConstrFrelid) == RELKIND_PARTITIONED_TABLE)
{
Relation pg_constraint = table_open(ConstraintRelationId, RowShareLock);
- ObjectAddresses *objs;
- HeapTuple consttup;
- ScanKeyInit(&key,
- Anum_pg_constraint_conrelid,
- BTEqualStrategyNumber, F_OIDEQ,
- ObjectIdGetDatum(fk->conrelid));
-
- scan = systable_beginscan(pg_constraint,
- ConstraintRelidTypidNameIndexId,
- true, NULL, 1, &key);
- objs = new_object_addresses();
- while ((consttup = systable_getnext(scan)) != NULL)
- {
- Form_pg_constraint conform = (Form_pg_constraint) GETSTRUCT(consttup);
-
- if (conform->conparentid != fk->conoid)
- continue;
- else
- {
- ObjectAddress addr;
- SysScanDesc scan2;
- ScanKeyData key2;
- int n PG_USED_FOR_ASSERTS_ONLY;
-
- ObjectAddressSet(addr, ConstraintRelationId, conform->oid);
- add_exact_object_address(&addr, objs);
-
- /*
- * First we must delete the dependency record that binds the
- * constraint records together.
- */
- n = deleteDependencyRecordsForSpecific(ConstraintRelationId,
- conform->oid,
- DEPENDENCY_INTERNAL,
- ConstraintRelationId,
- fk->conoid);
- Assert(n == 1); /* actually only one is expected */
-
- /*
- * Now search for the triggers for this constraint and set
- * them up for deletion too
- */
- ScanKeyInit(&key2,
- Anum_pg_trigger_tgconstraint,
- BTEqualStrategyNumber, F_OIDEQ,
- ObjectIdGetDatum(conform->oid));
- scan2 = systable_beginscan(trigrel, TriggerConstraintIndexId,
- true, NULL, 1, &key2);
- while ((trigtup = systable_getnext(scan2)) != NULL)
- {
- ObjectAddressSet(addr, TriggerRelationId,
- ((Form_pg_trigger) GETSTRUCT(trigtup))->oid);
- add_exact_object_address(&addr, objs);
- }
- systable_endscan(scan2);
- }
- }
- /* make the dependency deletions visible */
- CommandCounterIncrement();
- performMultipleDeletions(objs, DROP_RESTRICT,
- PERFORM_DELETION_INTERNAL);
- systable_endscan(scan);
+ RemoveInheritedConstraint(pg_constraint, trigrel, partConstrOid,
+ partConstrRelid);
table_close(pg_constraint, RowShareLock);
}
@@ -11580,9 +11540,10 @@ tryAttachPartitionForeignKey(List **wqueue,
Relation conrel;
conrel = table_open(ConstraintRelationId, RowExclusiveLock);
- partcontup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
+
+ partcontup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(partConstrOid));
if (!HeapTupleIsValid(partcontup))
- elog(ERROR, "cache lookup failed for constraint %u", fk->conoid);
+ elog(ERROR, "cache lookup failed for constraint %u", partConstrOid);
/* Use the same lock as for AT_ValidateConstraint */
QueueFKConstraintValidation(wqueue, conrel, partition, partcontup,
@@ -11590,8 +11551,136 @@ tryAttachPartitionForeignKey(List **wqueue,
ReleaseSysCache(partcontup);
table_close(conrel, RowExclusiveLock);
}
+}
- return true;
+/*
+ * RemoveInheritedConstraint
+ *
+ * Removes the constraint and its associated trigger from the specified
+ * relation, which inherited the given constraint.
+ */
+static void
+RemoveInheritedConstraint(Relation conrel, Relation trigrel, Oid conoid,
+ Oid conrelid)
+{
+ ObjectAddresses *objs;
+ HeapTuple consttup;
+ ScanKeyData key;
+ SysScanDesc scan;
+ HeapTuple trigtup;
+
+ ScanKeyInit(&key,
+ Anum_pg_constraint_conrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conrelid));
+
+ scan = systable_beginscan(conrel,
+ ConstraintRelidTypidNameIndexId,
+ true, NULL, 1, &key);
+ objs = new_object_addresses();
+ while ((consttup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_constraint conform = (Form_pg_constraint) GETSTRUCT(consttup);
+
+ if (conform->conparentid != conoid)
+ continue;
+ else
+ {
+ ObjectAddress addr;
+ SysScanDesc scan2;
+ ScanKeyData key2;
+ int n PG_USED_FOR_ASSERTS_ONLY;
+
+ ObjectAddressSet(addr, ConstraintRelationId, conform->oid);
+ add_exact_object_address(&addr, objs);
+
+ /*
+ * First we must delete the dependency record that binds the
+ * constraint records together.
+ */
+ n = deleteDependencyRecordsForSpecific(ConstraintRelationId,
+ conform->oid,
+ DEPENDENCY_INTERNAL,
+ ConstraintRelationId,
+ conoid);
+ Assert(n == 1); /* actually only one is expected */
+
+ /*
+ * Now search for the triggers for this constraint and set them up
+ * for deletion too
+ */
+ ScanKeyInit(&key2,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conform->oid));
+ scan2 = systable_beginscan(trigrel, TriggerConstraintIndexId,
+ true, NULL, 1, &key2);
+ while ((trigtup = systable_getnext(scan2)) != NULL)
+ {
+ ObjectAddressSet(addr, TriggerRelationId,
+ ((Form_pg_trigger) GETSTRUCT(trigtup))->oid);
+ add_exact_object_address(&addr, objs);
+ }
+ systable_endscan(scan2);
+ }
+ }
+ /* make the dependency deletions visible */
+ CommandCounterIncrement();
+ performMultipleDeletions(objs, DROP_RESTRICT,
+ PERFORM_DELETION_INTERNAL);
+ systable_endscan(scan);
+}
+
+/*
+ * DropForeignKeyConstraintTriggers
+ *
+ * The subroutine for tryAttachPartitionForeignKey handles the deletion of
+ * action triggers for the foreign key constraint.
+ */
+static void
+DropForeignKeyConstraintTriggers(Relation trigrel, Oid conoid, Oid confrelid,
+ Oid conrelid)
+{
+ ScanKeyData key;
+ SysScanDesc scan;
+ HeapTuple trigtup;
+
+ ScanKeyInit(&key,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+ scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
+ NULL, 1, &key);
+ while ((trigtup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
+ ObjectAddress trigger;
+
+ if (trgform->tgconstrrelid != conrelid)
+ continue;
+ if (trgform->tgrelid != confrelid)
+ continue;
+
+ /*
+ * The constraint is originally set up to contain this trigger as an
+ * implementation object, so there's a dependency record that links
+ * the two; however, since the trigger is no longer needed, we remove
+ * the dependency link in order to be able to drop the trigger while
+ * keeping the constraint intact.
+ */
+ deleteDependencyRecordsFor(TriggerRelationId,
+ trgform->oid,
+ false);
+ /* make dependency deletion visible to performDeletion */
+ CommandCounterIncrement();
+ ObjectAddressSet(trigger, TriggerRelationId,
+ trgform->oid);
+ performDeletion(&trigger, DROP_RESTRICT, 0);
+ /* make trigger drop visible, in case the loop iterates */
+ CommandCounterIncrement();
+ }
+
+ systable_endscan(scan);
}
/*
--
2.43.5
v11-0003-refactor-Pass-Relid-instead-of-Relation-to-creat.patchapplication/x-patch; name=v11-0003-refactor-Pass-Relid-instead-of-Relation-to-creat.patchDownload
From 20751f4c1cb80c79ae2e4894fcaa138f966c385f Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 20 Jan 2025 18:49:41 +0530
Subject: [PATCH v11 3/7] refactor: Pass Relid instead of Relation to
createForeignKeyCheckTriggers().
Currently, createForeignKeyCheckTriggers() takes a Relation type as
its first argument, but it doesn't use that argument directly.
Instead, it fetches the Relid by calling RelationGetRelid().
Therefore, it would be more consistent with other functions (e.g.,
createForeignKeyCheckTriggers()) to pass the Relid directly instead of
the whole Relation.
---
src/backend/commands/tablecmds.c | 20 ++++++++++----------
1 file changed, 10 insertions(+), 10 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index ae22bdb13be..7741b559818 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -569,7 +569,7 @@ static void createForeignKeyCheckTriggers(Oid myRelOid, Oid refRelOid,
Oid indexOid,
Oid parentInsTrigger, Oid parentUpdTrigger,
Oid *insertTrigOid, Oid *updateTrigOid);
-static void createForeignKeyActionTriggers(Relation rel, Oid refRelOid,
+static void createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid,
Constraint *fkconstraint, Oid constraintOid,
Oid indexOid,
Oid parentDelTrigger, Oid parentUpdTrigger,
@@ -10582,7 +10582,8 @@ addFkRecurseReferenced(Constraint *fkconstraint, Relation rel,
/*
* Create the action triggers that enforce the constraint.
*/
- createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
+ createForeignKeyActionTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
fkconstraint,
parentConstr, indexOid,
parentDelTrigger, parentUpdTrigger,
@@ -12960,10 +12961,11 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
* *updateTrigOid.
*/
static void
-createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstraint,
- Oid constraintOid, Oid indexOid,
- Oid parentDelTrigger, Oid parentUpdTrigger,
- Oid *deleteTrigOid, Oid *updateTrigOid)
+createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid,
+ Constraint *fkconstraint, Oid constraintOid,
+ Oid indexOid, Oid parentDelTrigger,
+ Oid parentUpdTrigger, Oid *deleteTrigOid,
+ Oid *updateTrigOid)
{
CreateTrigStmt *fk_trigger;
ObjectAddress trigAddress;
@@ -13019,8 +13021,7 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
break;
}
- trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid,
- RelationGetRelid(rel),
+ trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid, myRelOid,
constraintOid, indexOid, InvalidOid,
parentDelTrigger, NULL, true, false);
if (deleteTrigOid)
@@ -13080,8 +13081,7 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
break;
}
- trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid,
- RelationGetRelid(rel),
+ trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid, myRelOid,
constraintOid, indexOid, InvalidOid,
parentUpdTrigger, NULL, true, false);
if (updateTrigOid)
--
2.43.5
v11-0004-refactor-Change-ATExecAlterConstrRecurse-argumen.patchapplication/x-patch; name=v11-0004-refactor-Change-ATExecAlterConstrRecurse-argumen.patchDownload
From db1a0ca2fc1bc1e6fe7f8025038ac5688db27798 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Tue, 14 Jan 2025 22:35:32 +0530
Subject: [PATCH v11 4/7] refactor: Change ATExecAlterConstrRecurse() argument.
Instead of passing the Relation of the referencing relation, we will
pass a relid. Opening the relation using the relid again should not
cause significant issues. However, this approach makes recursion more
efficient, especially in the later patch where we will be recreating
the referencing and referred triggers. In addition to that passing
relid of the referred relation too.
----
NOTE: This patch is not meant to be committed separately. It should
be squashed with the main patch that adds ENFORCED/NOT ENFORCED.
----
---
src/backend/commands/tablecmds.c | 36 +++++++++++++++++---------------
1 file changed, 19 insertions(+), 17 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 7741b559818..56233eea319 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -392,13 +392,15 @@ static void AlterSeqNamespaces(Relation classRel, Relation rel,
static ObjectAddress ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd,
bool recurse, bool recursing, LOCKMODE lockmode);
static bool ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
LOCKMODE lockmode);
static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
bool deferrable, bool initdeferred,
List **otherrelids);
static void ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
LOCKMODE lockmode);
static ObjectAddress ATExecValidateConstraint(List **wqueue,
Relation rel, char *constrName,
@@ -11922,8 +11924,9 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
currcon->condeferred != cmdcon->initdeferred ||
rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
- if (ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, rel, contuple,
- &otherrelids, lockmode))
+ if (ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, currcon->conrelid,
+ currcon->confrelid, contuple, &otherrelids,
+ lockmode))
ObjectAddressSet(address, ConstraintRelationId, currcon->oid);
}
@@ -11956,13 +11959,15 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
*/
static bool
ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
LOCKMODE lockmode)
{
Form_pg_constraint currcon;
Oid conoid;
Oid refrelid;
bool changed = false;
+ Relation rel;
/* since this function recurses, it could be driven to stack overflow */
check_stack_depth();
@@ -11971,6 +11976,8 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
conoid = currcon->oid;
refrelid = currcon->confrelid;
+ rel = table_open(currcon->conrelid, lockmode);
+
/*
* Update pg_constraint with the flags from cmdcon.
*
@@ -12016,8 +12023,9 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
*/
if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)
- ATExecAlterChildConstr(cmdcon, conrel, tgrel, rel, contuple,
- otherrelids, lockmode);
+ ATExecAlterChildConstr(cmdcon, conrel, tgrel, fkrelid, pkrelid,
+ contuple, otherrelids, lockmode);
+ table_close(rel, NoLock);
return changed;
}
@@ -12096,7 +12104,8 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
*/
static void
ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
LOCKMODE lockmode)
{
Form_pg_constraint currcon;
@@ -12117,15 +12126,8 @@ ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
true, NULL, 1, &pkey);
while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
- {
- Form_pg_constraint childcon = (Form_pg_constraint) GETSTRUCT(childtup);
- Relation childrel;
-
- childrel = table_open(childcon->conrelid, lockmode);
- ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, childrel, childtup,
- otherrelids, lockmode);
- table_close(childrel, NoLock);
- }
+ ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, fkrelid, pkrelid,
+ childtup, otherrelids, lockmode);
systable_endscan(pscan);
}
--
2.43.5
On Wed, Jan 29, 2025 at 6:18 PM Amul Sul <sulamul@gmail.com> wrote:
On Tue, Jan 28, 2025 at 9:47 PM Peter Eisentraut <peter@eisentraut.org> wrote:
In 0006, this change in the test output should be improved:
-- XXX: error message is misleading here ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED; -ERROR: ALTER CONSTRAINT statement constraints cannot be marked ENFORCED -LINE 1: ...TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED; - ^ +ERROR: constraint "unique_tbl_i_key" of relation "unique_tbl" is not a foreign key constraintMaybe this should be along the lines of "ALTER CONSTRAINT ... ENFORCED
is not supported for %s constraints" or something like that.Ok, let me see what can be done here.
I tried to improve the error message by adding the following details
for this case in the attached version:+ERROR: cannot alter enforceability of constraint "unique_tbl_i_key" of relation "unique_tbl" +DETAIL: Enforceability can only be altered for foreign key constraints.On 28.01.25 11:58, Amul Sul wrote:
This behavior is not correct:
+-- Changing it back to ENFORCED will leave the constraint in the NOT VALID state +ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED; +-- Which needs to be explicitly validated. +ALTER TABLE FKTABLE VALIDATE CONSTRAINT fktable_ftest1_fkey;Setting the constraint to enforced should enforce it immediately. This
SQL statement is covered by the SQL standard. Also, I think it's a
better user experience if you don't require two steps.Let me clarify: the constraint will be enforced for new inserts and
updates, but it won't be validated against existing data, so those
will remain marked as invalid.Yes, I understand, but that is the not the correct behavior of this
command per SQL standard.
If the constraint is VALID and later marked as NOT ENFORCED, changing
it to ENFORCED should also keep it VALID. But if the constraint is NOT
VALID and later marked as NOT ENFORCED, what is expected behaviour
while changing it to ENFORCED? Should it be kept NOT VALID or it
should turn into VALID? I think a user would expect it to be NOT
VALID. When we didn't support the ENFORCED/NOT ENFORCED option, we had
NOT VALID + ENFORCED behaviour.
Now the problem I see is when we set NOT ENFORCED, the constraint is
also set to NOT VALID, which is arguable. When a user sets a
constraint as NOT ENFORCED, it's their responsibility to make sure
that the data still fits the constraint. In that sense the constraint
is VALID and we shouldn't convert it to NOT VALID. We should validate
all the data when changing a NOT ENFORCED, VALID constraint to
ENFORCED, VALID so that the VALID status is reliable.
--
Best Wishes,
Ashutosh Bapat
On 2025-Jan-31, Ashutosh Bapat wrote:
But if the constraint is NOT VALID and later marked as NOT ENFORCED,
what is expected behaviour while changing it to ENFORCED?
I think what you want is a different mode that would be ENFORCED NOT
VALID, which would be an extension of the standard, because the standard
does not support the concept of NOT VALID. So while I think what you
want is nice, I'm not sure that this patch necessarily must implement
it.
--
Álvaro Herrera 48°01'N 7°57'E — https://www.EnterpriseDB.com/
hi.
after applying the v11-0002 to v11-0006.
there is a bug in ATExecAlterConstrRecurse, i think.
in ATExecAlterConstrRecurse, after applying the patch, the code is
if (currcon->condeferrable != cmdcon->deferrable ||
currcon->condeferred != cmdcon->initdeferred ||
currcon->conenforced != cmdcon->is_enforced)
{
}
if (currcon->conenforced != cmdcon->is_enforced)
{
ATExecAlterConstrEnforceability
}
else
{
AlterConstrTriggerDeferrability...
if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)
ATExecAlterChildConstr(cmdcon, conrel, tgrel, fkrelid, pkrelid,
contuple, otherrelids, lockmode);
}
drop table if exists PKTABLE, fktable cascade;
CREATE TABLE PKTABLE (ptest1 int PRIMARY KEY, ptest2 text);
CREATE TABLE FKTABLE (ftest1 int REFERENCES PKTABLE MATCH FULL ON
DELETE CASCADE ON UPDATE CASCADE NOT ENFORCED,
ftest2 int);
ALTER TABLE fktable ALTER CONSTRAINT fktable_ftest1_fkey deferrable;
\d fktable
Table "public.fktable"
Column | Type | Collation | Nullable | Default
--------+---------+-----------+----------+---------
ftest1 | integer | | |
ftest2 | integer | | |
Foreign-key constraints:
"fktable_ftest1_fkey" FOREIGN KEY (ftest1) REFERENCES
pktable(ptest1) MATCH FULL ON UPDATE CASCADE ON DELETE CASCADE
DEFERRABLE NOT VALID
Currently "ALTER TABLE fktable ALTER CONSTRAINT fktable_ftest1_fkey deferrable;"
imply the constraint fktable_ftest1_fkey is changing from "not
enforced" to "enforced".
but here we didn't explicitly mean to change the "enforced" status.
We only want to change the deferriability.
So the code should only call AlterConstrTriggerDeferrability,
not call ATExecAlterConstrEnforceability?
On Sat, Feb 1, 2025 at 8:31 PM jian he <jian.universality@gmail.com> wrote:
[...]
So the code should only call AlterConstrTriggerDeferrability,
not call ATExecAlterConstrEnforceability?
Right. Thank you for the report. We need to know whether the
enforceability and/or deferability has actually been set or not before
catalog update.
Have you started working on the ALTER ... CONSTRAINT for the check
constraint? I am thinking to start that. To fix this bug, we have two
options: we could either throw an error as we don’t currently support
altering enforceability and deferability together (which I’m not a fan
of), or we could refactor the ALTER ... CONSTRAINT code to include
more information which would allow us to perform the appropriate
update action during the execution stage, and it would also help with
altering check constraints.
Regards,
Amul
On Fri, Jan 31, 2025 at 7:10 PM Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
On 2025-Jan-31, Ashutosh Bapat wrote:
But if the constraint is NOT VALID and later marked as NOT ENFORCED,
what is expected behaviour while changing it to ENFORCED?I think what you want is a different mode that would be ENFORCED NOT
VALID, which would be an extension of the standard, because the standard
does not support the concept of NOT VALID. So while I think what you
want is nice, I'm not sure that this patch necessarily must implement
it.
Here is my understanding behind this feature implementation -- I am
not claiming to be 100% correct, I am confident I am not entirely
wrong either. Let me explain with an example: imagine a user adds a
VALID constraint to a table that already has data, and the user is
completely sure that all the data complies with the constraint. Even
in this case, the system still runs a validation check. This is
expected behavior because the system can't just take the user's word
for it -- it needs to explicitly confirm that the data is valid
through validation.
Now, with a NOT ENFORCED constraint, it's almost like the constraint
doesn't exist, because no checks are being performed and there is no
visible effect for the user, even though the constraint is technically
still there. So when the constraint is switched to ENFORCED, we should
be careful not to automatically mark it as validated (regardless of
its previous validate status) unless the data is actually checked
against the constraint -- treat as adding a new VALID constraint. Even
if the user is absolutely sure the data complies, we should still run
the validation to ensure reliability.
In response to Ashutosh’s point about the VALID/NOT ENFORCED scenario:
if a constraint is initially VALID, then marked as NOT ENFORCED, and
later switched back to ENFORCED -- IMO, it shouldn't automatically be
considered VALID. I had similar thoughts when working on a patch
before v5, but after further discussion, I now agree that it makes
more sense for the system to keep it as NOT VALID until the data has
been validated against the constraint. This is especially important
when a user adds the constraint, is confident the data complies, and
then needs to enforce it. Validating the data ensures the integrity
and consistency of the constraint.
In short, even if the user is 100% confident, skipping validation is
not an option. We need to make sure the constraint’s VALID status is
accurate and reliable before marking it as validated.
Regards,
Amul
On Mon, Feb 3, 2025 at 9:57 AM Amul Sul <sulamul@gmail.com> wrote:
On Fri, Jan 31, 2025 at 7:10 PM Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
On 2025-Jan-31, Ashutosh Bapat wrote:
But if the constraint is NOT VALID and later marked as NOT ENFORCED,
what is expected behaviour while changing it to ENFORCED?I think what you want is a different mode that would be ENFORCED NOT
VALID, which would be an extension of the standard, because the standard
does not support the concept of NOT VALID. So while I think what you
want is nice, I'm not sure that this patch necessarily must implement
it.
This way allows VALID/NOT VALID and ENFORCED/NOT ENFORCED states to
work together and also implement behaviour specified by the standard
(ref. Peter's email). If there's some other way to implement the
behaviour, that's fine too.
Here is my understanding behind this feature implementation -- I am
not claiming to be 100% correct, I am confident I am not entirely
wrong either. Let me explain with an example: imagine a user adds a
VALID constraint to a table that already has data, and the user is
completely sure that all the data complies with the constraint. Even
in this case, the system still runs a validation check. This is
expected behavior because the system can't just take the user's word
for it -- it needs to explicitly confirm that the data is valid
through validation.Now, with a NOT ENFORCED constraint, it's almost like the constraint
doesn't exist, because no checks are being performed and there is no
visible effect for the user, even though the constraint is technically
still there. So when the constraint is switched to ENFORCED, we should
be careful not to automatically mark it as validated (regardless of
its previous validate status) unless the data is actually checked
against the constraint -- treat as adding a new VALID constraint. Even
if the user is absolutely sure the data complies, we should still run
the validation to ensure reliability.In response to Ashutosh’s point about the VALID/NOT ENFORCED scenario:
if a constraint is initially VALID, then marked as NOT ENFORCED, and
later switched back to ENFORCED -- IMO, it shouldn't automatically be
considered VALID.
I am suggesting that when a constraint is changed from NOT ENFORCED to
ENFORCED, if it's marked VALID - we run validation checks.
Here's how I see the state conversions happening.
NOT VALID, NOT ENFORCED changed to NOT_VALID, ENFORCED - no data
validation required, constraint is enforced on the new tuples/changes
NOT VALID, ENFORCED changed to NOT VALID, NOT ENFORCED - no data
validation, constraint isn't enforced anymore
VALID, NOT ENFORCED changed to VALID, ENFORCED - data validation
required, constraint is enforced
VALID, ENFORCED changed to VALID, NOT ENFORCED - no data validation
required, constrain isn't enforced anymore, we rely on user to enforce
the constraint on their side
--
Best Wishes,
Ashutosh Bapat
On Mon, Feb 3, 2025 at 10:49 AM Ashutosh Bapat
<ashutosh.bapat.oss@gmail.com> wrote:
On Mon, Feb 3, 2025 at 9:57 AM Amul Sul <sulamul@gmail.com> wrote:
On Fri, Jan 31, 2025 at 7:10 PM Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
On 2025-Jan-31, Ashutosh Bapat wrote:
But if the constraint is NOT VALID and later marked as NOT ENFORCED,
what is expected behaviour while changing it to ENFORCED?I think what you want is a different mode that would be ENFORCED NOT
VALID, which would be an extension of the standard, because the standard
does not support the concept of NOT VALID. So while I think what you
want is nice, I'm not sure that this patch necessarily must implement
it.This way allows VALID/NOT VALID and ENFORCED/NOT ENFORCED states to
work together and also implement behaviour specified by the standard
(ref. Peter's email). If there's some other way to implement the
behaviour, that's fine too.Here is my understanding behind this feature implementation -- I am
not claiming to be 100% correct, I am confident I am not entirely
wrong either. Let me explain with an example: imagine a user adds a
VALID constraint to a table that already has data, and the user is
completely sure that all the data complies with the constraint. Even
in this case, the system still runs a validation check. This is
expected behavior because the system can't just take the user's word
for it -- it needs to explicitly confirm that the data is valid
through validation.Now, with a NOT ENFORCED constraint, it's almost like the constraint
doesn't exist, because no checks are being performed and there is no
visible effect for the user, even though the constraint is technically
still there. So when the constraint is switched to ENFORCED, we should
be careful not to automatically mark it as validated (regardless of
its previous validate status) unless the data is actually checked
against the constraint -- treat as adding a new VALID constraint. Even
if the user is absolutely sure the data complies, we should still run
the validation to ensure reliability.In response to Ashutosh’s point about the VALID/NOT ENFORCED scenario:
if a constraint is initially VALID, then marked as NOT ENFORCED, and
later switched back to ENFORCED -- IMO, it shouldn't automatically be
considered VALID.I am suggesting that when a constraint is changed from NOT ENFORCED to
ENFORCED, if it's marked VALID - we run validation checks.
Ok.
Here's how I see the state conversions happening.
NOT VALID, NOT ENFORCED changed to NOT_VALID, ENFORCED - no data
validation required, constraint is enforced on the new tuples/changes
NOT VALID, ENFORCED changed to NOT VALID, NOT ENFORCED - no data
validation, constraint isn't enforced anymore
VALID, NOT ENFORCED changed to VALID, ENFORCED - data validation
required, constraint is enforced
VALID, ENFORCED changed to VALID, NOT ENFORCED - no data validation
required, constrain isn't enforced anymore, we rely on user to enforce
the constraint on their side
Understood, thanks for the detailed explanation. This is what I had
implemented in the v4 patch, and I agree with this. If we decide to go
with this, I can revert the behavior to the v4 patch set.
Regards,
Amul
On 2025-Feb-03, Ashutosh Bapat wrote:
VALID, NOT ENFORCED changed to VALID, ENFORCED - data validation
required, constraint is enforced
There's no such thing as a VALID NOT ENFORCED constraint. It just
cannot exist.
NOT VALID, NOT ENFORCED changed to NOT_VALID, ENFORCED - no data
validation required, constraint is enforced on the new tuples/changes
This may make sense, but it needs special nonstandard syntax. If you
start with a NOT VALID NOT ENFORCED constraint (which is the only way to
have a NOT ENFORCED constraint) and apply ALTER TABLE ALTER CONSTRAINT
ENFORCE, you will end up with a VALID ENFORCED constraint, therefore
validation must be run.
If you wanted to add a nonstandard command
ALTER TABLE ALTER CONSTRAINT ENFORCE NO VALIDATE
then maybe the transition you suggest could be made.
It should be a separate patch from regular ALTER CONSTRAINT ENFORCE
though, just in case some problems with it emerge later and we're forced
to revert it, we can still keep the standard command.
--
Álvaro Herrera Breisgau, Deutschland — https://www.EnterpriseDB.com/
"Use it up, wear it out, make it do, or do without"
On Mon, Feb 3, 2025 at 1:20 PM Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
On 2025-Feb-03, Ashutosh Bapat wrote:
VALID, NOT ENFORCED changed to VALID, ENFORCED - data validation
required, constraint is enforcedThere's no such thing as a VALID NOT ENFORCED constraint. It just
cannot exist.
The document in the patch says
```
If the
constraint is <literal>NOT ENFORCED</literal>, the database system will
not check the constraint. It is then up to the application code to
ensure that the constraints are satisfied. The database system might
still assume that the data actually satisfies the constraint for
optimization decisions where this does not affect the correctness of the
result.
```
If a constraint is NOT VALID, NOT ENFORCED it can't be used for
optimization. Constraints which are VALID, NOT ENFORCED can be used
for optimizatin. That's a correct state if the application is
faithfully making sure that the constraint is satisfied, as suggested
in our documentation. Otherwise, I don't see how NOT ENFORCED
constraints would be useful.
NOT VALID, NOT ENFORCED changed to NOT_VALID, ENFORCED - no data
validation required, constraint is enforced on the new tuples/changesThis may make sense, but it needs special nonstandard syntax. If you
start with a NOT VALID NOT ENFORCED constraint (which is the only way to
have a NOT ENFORCED constraint) and apply ALTER TABLE ALTER CONSTRAINT
ENFORCE, you will end up with a VALID ENFORCED constraint, therefore
validation must be run.If you wanted to add a nonstandard command
ALTER TABLE ALTER CONSTRAINT ENFORCE NO VALIDATE
Which state transition needs it? ALTER TABLE ALTER CONSTRAINT ENFORCE
is enough to change NOT VALID, NOT ENFORCED constraint to NOT VALID,
ENFORCED constraint; it does not need NO VALIDATE.
--
Best Wishes,
Ashutosh Bapat
On 2025-Feb-03, Ashutosh Bapat wrote:
```
If the
constraint is <literal>NOT ENFORCED</literal>, the database system will
not check the constraint. It is then up to the application code to
ensure that the constraints are satisfied. The database system might
still assume that the data actually satisfies the constraint for
optimization decisions where this does not affect the correctness of the
result.
```
IMO the third sentence should be removed because it is bogus. There's
no situation in which a not-enforced constraint can be used for any
query optimizations -- you cannot know if a constraint remains valid
after it's been turned NOT ENFORCED, because anyone could insert data
that violates it milliseconds after it stops being enforced. I think
the expectation that the application is going to correctly enforce the
constraint after it's told the database server not to enforce it, is
going to be problematic. As I recall, we already do this in FDWs for
instance and it's already a problem.
The second sentence is also somewhat bogus, but not as much, because it
doesn't have any implications for the database system. I think we
should simply say that no assumptions can be made about not enforced
constraints from the server side and that if the user wants to enforce
these at the application side, it's up to them to keep it all straight.
IMO if the patch is letting constraints that are marked NOT ENFORCED
continue to be used for query optimization, the patch is bogus and
should be fixed. For instance, I think CheckConstraintFetch() should
skip adding to TupleDesc->constr any constraints that are marked as not
enforced.
--
Álvaro Herrera 48°01'N 7°57'E — https://www.EnterpriseDB.com/
On Mon, Feb 3, 2025 at 5:55 PM Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
On 2025-Feb-03, Ashutosh Bapat wrote:
```
If the
constraint is <literal>NOT ENFORCED</literal>, the database system will
not check the constraint. It is then up to the application code to
ensure that the constraints are satisfied. The database system might
still assume that the data actually satisfies the constraint for
optimization decisions where this does not affect the correctness of the
result.
```IMO the third sentence should be removed because it is bogus. There's
no situation in which a not-enforced constraint can be used for any
query optimizations -- you cannot know if a constraint remains valid
after it's been turned NOT ENFORCED, because anyone could insert data
that violates it milliseconds after it stops being enforced. I think
the expectation that the application is going to correctly enforce the
constraint after it's told the database server not to enforce it, is
going to be problematic. As I recall, we already do this in FDWs for
instance and it's already a problem.
What's the use of NOT ENFORCED constraint then?
To me NOT ENFORCED looks similar to informational constraints of IBM
DB2 [1]https://www.ibm.com/docs/en/db2/11.1?topic=constraints-informational. It seems that they have TRUSTED/NOT TRUSTED similar to
PostgreSQL's VALID/NOT VALID [2]https://www.ibm.com/docs/en/db2/11.1?topic=statements-create-table.
[1]: https://www.ibm.com/docs/en/db2/11.1?topic=constraints-informational
[2]: https://www.ibm.com/docs/en/db2/11.1?topic=statements-create-table
--
Best Wishes,
Ashutosh Bapat
On Mon, Feb 3, 2025 at 9:39 AM Amul Sul <sulamul@gmail.com> wrote:
On Sat, Feb 1, 2025 at 8:31 PM jian he <jian.universality@gmail.com> wrote:
[...]
So the code should only call AlterConstrTriggerDeferrability,
not call ATExecAlterConstrEnforceability?Right. Thank you for the report. We need to know whether the
enforceability and/or deferability has actually been set or not before
catalog update.
Fixed in the attached version. A new patch, 0001, introduces a new
struct, AlterConstraintStmt, which carries the necessary information
for enforceability and deferrability modifications of a constraint, as
implemented in patch 0006.
Regards,
Amul
Attachments:
v12-0001-Add-AlterConstraintStmt-struct-for-ALTER-.-CONST.patchapplication/octet-stream; name=v12-0001-Add-AlterConstraintStmt-struct-for-ALTER-.-CONST.patchDownload
From d9f8118d6d89a50d5618d9f346b6a6c20c11d59a Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Tue, 4 Feb 2025 09:55:18 +0530
Subject: [PATCH v12 1/7] Add AlterConstraintStmt struct for ALTER ..
CONSTRAINT.
Replace the use of Constraint with the new AlterConstraintStmt struct,
which allows us to pass additional information, such as whether the
constraint attribute is set. This is necessary for the feature patches
that involve altering the enforceability of foreign key and check
constraints as part of the implementation.
---
src/backend/commands/tablecmds.c | 36 ++++++++++++++++----------------
src/backend/parser/gram.y | 3 +--
src/include/nodes/parsenodes.h | 14 +++++++++++++
src/tools/pgindent/typedefs.list | 1 +
4 files changed, 34 insertions(+), 20 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 18f64db6e39..2158b3d6c66 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -389,17 +389,17 @@ static void AlterIndexNamespaces(Relation classRel, Relation rel,
static void AlterSeqNamespaces(Relation classRel, Relation rel,
Oid oldNspOid, Oid newNspOid, ObjectAddresses *objsMoved,
LOCKMODE lockmode);
-static ObjectAddress ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd,
+static ObjectAddress ATExecAlterConstraint(Relation rel, AlterConstraintStmt *cmdcon,
bool recurse, bool recursing, LOCKMODE lockmode);
-static bool ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode);
+static bool ATExecAlterConstrRecurse(AlterConstraintStmt *cmdcon, Relation conrel,
+ Relation tgrel, Relation rel, HeapTuple contuple,
+ List **otherrelids, LOCKMODE lockmode);
static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
bool deferrable, bool initdeferred,
List **otherrelids);
-static void ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode);
+static void ATExecAlterChildConstr(AlterConstraintStmt *cmdcon, Relation conrel,
+ Relation tgrel, Relation rel, HeapTuple contuple,
+ List **otherrelids, LOCKMODE lockmode);
static ObjectAddress ATExecValidateConstraint(List **wqueue,
Relation rel, char *constrName,
bool recurse, bool recursing, LOCKMODE lockmode);
@@ -5432,7 +5432,10 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
lockmode);
break;
case AT_AlterConstraint: /* ALTER CONSTRAINT */
- address = ATExecAlterConstraint(rel, cmd, false, false, lockmode);
+ address =
+ ATExecAlterConstraint(rel,
+ castNode(AlterConstraintStmt, cmd->def),
+ false, false, lockmode);
break;
case AT_ValidateConstraint: /* VALIDATE CONSTRAINT */
address = ATExecValidateConstraint(wqueue, rel, cmd->name, cmd->recurse,
@@ -11711,10 +11714,9 @@ GetForeignKeyCheckTriggers(Relation trigrel,
* InvalidObjectAddress.
*/
static ObjectAddress
-ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
+ATExecAlterConstraint(Relation rel, AlterConstraintStmt *cmdcon, bool recurse,
bool recursing, LOCKMODE lockmode)
{
- Constraint *cmdcon;
Relation conrel;
Relation tgrel;
SysScanDesc scan;
@@ -11725,8 +11727,6 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
List *otherrelids = NIL;
ListCell *lc;
- cmdcon = castNode(Constraint, cmd->def);
-
conrel = table_open(ConstraintRelationId, RowExclusiveLock);
tgrel = table_open(TriggerRelationId, RowExclusiveLock);
@@ -11849,9 +11849,9 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
* but existing releases don't do that.)
*/
static bool
-ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode)
+ATExecAlterConstrRecurse(AlterConstraintStmt *cmdcon, Relation conrel,
+ Relation tgrel, Relation rel, HeapTuple contuple,
+ List **otherrelids, LOCKMODE lockmode)
{
Form_pg_constraint currcon;
Oid conoid;
@@ -11989,9 +11989,9 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
* ATExecAlterConstrRecurse.
*/
static void
-ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode)
+ATExecAlterChildConstr(AlterConstraintStmt *cmdcon, Relation conrel,
+ Relation tgrel, Relation rel, HeapTuple contuple,
+ List **otherrelids, LOCKMODE lockmode)
{
Form_pg_constraint currcon;
Oid conoid;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index d7f9c00c409..e845e888b06 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -2657,11 +2657,10 @@ alter_table_cmd:
| ALTER CONSTRAINT name ConstraintAttributeSpec
{
AlterTableCmd *n = makeNode(AlterTableCmd);
- Constraint *c = makeNode(Constraint);
+ AlterConstraintStmt *c = makeNode(AlterConstraintStmt);
n->subtype = AT_AlterConstraint;
n->def = (Node *) c;
- c->contype = CONSTR_FOREIGN; /* others not supported, yet */
c->conname = $3;
processCASbits($4, @4, "ALTER CONSTRAINT statement",
&c->deferrable,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index ffe155ee20e..4ae9906a674 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2503,6 +2503,20 @@ typedef struct AlterCollationStmt
List *collname;
} AlterCollationStmt;
+/* ----------------------
+ * Alter Constraint
+ * ----------------------
+ */
+typedef struct AlterConstraintStmt
+{
+ NodeTag type;
+
+ char *conname; /* Constraint name */
+
+ bool deferrable; /* DEFERRABLE? */
+ bool initdeferred; /* INITIALLY DEFERRED? */
+} AlterConstraintStmt;
+
/* ----------------------
* Alter Domain
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9a3bee93dec..b7780ad57a3 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -66,6 +66,7 @@ AllocSetFreeList
AllocateDesc
AllocateDescKind
AlterCollationStmt
+AlterConstraintStmt
AlterDatabaseRefreshCollStmt
AlterDatabaseSetStmt
AlterDatabaseStmt
--
2.43.5
v12-0002-refactor-Split-tryAttachPartitionForeignKey.patchapplication/octet-stream; name=v12-0002-refactor-Split-tryAttachPartitionForeignKey.patchDownload
From 83f0960cfa72caf2cc165064855ff313ba42e0ce Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Wed, 29 Jan 2025 11:35:27 +0530
Subject: [PATCH v12 2/7] refactor: Split tryAttachPartitionForeignKey()
Split tryAttachPartitionForeignKey() into three functions:
AttachPartitionForeignKey(), RemoveInheritedConstraint() and
DropForeignKeyConstraintTriggers(), so they can be reused in the next
patch.
---
src/backend/commands/tablecmds.c | 319 ++++++++++++++++++++-----------
1 file changed, 204 insertions(+), 115 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 2158b3d6c66..c577f5100af 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -583,6 +583,14 @@ static bool tryAttachPartitionForeignKey(List **wqueue,
Oid parentInsTrigger,
Oid parentUpdTrigger,
Relation trigrel);
+static void AttachPartitionForeignKey(List **wqueue, Relation partition,
+ Oid partConstrOid, Oid parentConstrOid,
+ Oid parentInsTrigger, Oid parentUpdTrigger,
+ Relation trigrel);
+static void RemoveInheritedConstraint(Relation conrel, Relation trigrel,
+ Oid conoid, Oid conrelid);
+static void DropForeignKeyConstraintTriggers(Relation trigrel, Oid conoid,
+ Oid confrelid, Oid conrelid);
static void GetForeignKeyActionTriggers(Relation trigrel,
Oid conoid, Oid confrelid, Oid conrelid,
Oid *deleteTriggerOid,
@@ -11355,12 +11363,6 @@ tryAttachPartitionForeignKey(List **wqueue,
Form_pg_constraint parentConstr;
HeapTuple partcontup;
Form_pg_constraint partConstr;
- bool queueValidation;
- ScanKeyData key;
- SysScanDesc scan;
- HeapTuple trigtup;
- Oid insertTriggerOid,
- updateTriggerOid;
parentConstrTup = SearchSysCache1(CONSTROID,
ObjectIdGetDatum(parentConstrOid));
@@ -11405,6 +11407,59 @@ tryAttachPartitionForeignKey(List **wqueue,
return false;
}
+ ReleaseSysCache(parentConstrTup);
+ ReleaseSysCache(partcontup);
+
+ /* Looks good! Attach this constraint. */
+ AttachPartitionForeignKey(wqueue, partition, fk->conoid,
+ parentConstrOid, parentInsTrigger,
+ parentUpdTrigger, trigrel);
+
+ return true;
+}
+
+/*
+ * AttachPartitionForeignKey
+ *
+ * The subroutine for tryAttachPartitionForeignKey performs the final tasks of
+ * attaching the constraint, removing redundant triggers and entries from
+ * pg_constraint, and setting the constraint's parent.
+ */
+static void
+AttachPartitionForeignKey(List **wqueue,
+ Relation partition,
+ Oid partConstrOid,
+ Oid parentConstrOid,
+ Oid parentInsTrigger,
+ Oid parentUpdTrigger,
+ Relation trigrel)
+{
+ HeapTuple parentConstrTup;
+ Form_pg_constraint parentConstr;
+ HeapTuple partcontup;
+ Form_pg_constraint partConstr;
+ bool queueValidation;
+ Oid partConstrFrelid;
+ Oid partConstrRelid;
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
+ /* Fetch the parent constraint tuple */
+ parentConstrTup = SearchSysCache1(CONSTROID,
+ ObjectIdGetDatum(parentConstrOid));
+ if (!HeapTupleIsValid(parentConstrTup))
+ elog(ERROR, "cache lookup failed for constraint %u", parentConstrOid);
+ parentConstr = (Form_pg_constraint) GETSTRUCT(parentConstrTup);
+
+ /* Fetch the child constraint tuple */
+ partcontup = SearchSysCache1(CONSTROID,
+ ObjectIdGetDatum(partConstrOid));
+ if (!HeapTupleIsValid(partcontup))
+ elog(ERROR, "cache lookup failed for constraint %u", partConstrOid);
+ partConstr = (Form_pg_constraint) GETSTRUCT(partcontup);
+ partConstrFrelid = partConstr->confrelid;
+ partConstrRelid = partConstr->conrelid;
+
/*
* Will we need to validate this constraint? A valid parent constraint
* implies that all child constraints have been validated, so if this one
@@ -11416,50 +11471,15 @@ tryAttachPartitionForeignKey(List **wqueue,
ReleaseSysCache(parentConstrTup);
/*
- * Looks good! Attach this constraint. The action triggers in the new
- * partition become redundant -- the parent table already has equivalent
- * ones, and those will be able to reach the partition. Remove the ones
- * in the partition. We identify them because they have our constraint
- * OID, as well as being on the referenced rel.
+ * The action triggers in the new partition become redundant -- the parent
+ * table already has equivalent ones, and those will be able to reach the
+ * partition. Remove the ones in the partition. We identify them because
+ * they have our constraint OID, as well as being on the referenced rel.
*/
- ScanKeyInit(&key,
- Anum_pg_trigger_tgconstraint,
- BTEqualStrategyNumber, F_OIDEQ,
- ObjectIdGetDatum(fk->conoid));
- scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
- NULL, 1, &key);
- while ((trigtup = systable_getnext(scan)) != NULL)
- {
- Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
- ObjectAddress trigger;
+ DropForeignKeyConstraintTriggers(trigrel, partConstrOid, partConstrFrelid,
+ partConstrRelid);
- if (trgform->tgconstrrelid != fk->conrelid)
- continue;
- if (trgform->tgrelid != fk->confrelid)
- continue;
-
- /*
- * The constraint is originally set up to contain this trigger as an
- * implementation object, so there's a dependency record that links
- * the two; however, since the trigger is no longer needed, we remove
- * the dependency link in order to be able to drop the trigger while
- * keeping the constraint intact.
- */
- deleteDependencyRecordsFor(TriggerRelationId,
- trgform->oid,
- false);
- /* make dependency deletion visible to performDeletion */
- CommandCounterIncrement();
- ObjectAddressSet(trigger, TriggerRelationId,
- trgform->oid);
- performDeletion(&trigger, DROP_RESTRICT, 0);
- /* make trigger drop visible, in case the loop iterates */
- CommandCounterIncrement();
- }
-
- systable_endscan(scan);
-
- ConstraintSetParentConstraint(fk->conoid, parentConstrOid,
+ ConstraintSetParentConstraint(partConstrOid, parentConstrOid,
RelationGetRelid(partition));
/*
@@ -11467,7 +11487,7 @@ tryAttachPartitionForeignKey(List **wqueue,
* corresponding parent triggers.
*/
GetForeignKeyCheckTriggers(trigrel,
- fk->conoid, fk->confrelid, fk->conrelid,
+ partConstrOid, partConstrFrelid, partConstrRelid,
&insertTriggerOid, &updateTriggerOid);
Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
@@ -11481,72 +11501,12 @@ tryAttachPartitionForeignKey(List **wqueue,
* attaching now has extra pg_constraint rows and action triggers that are
* no longer needed. Remove those.
*/
- if (get_rel_relkind(fk->confrelid) == RELKIND_PARTITIONED_TABLE)
+ if (get_rel_relkind(partConstrFrelid) == RELKIND_PARTITIONED_TABLE)
{
Relation pg_constraint = table_open(ConstraintRelationId, RowShareLock);
- ObjectAddresses *objs;
- HeapTuple consttup;
- ScanKeyInit(&key,
- Anum_pg_constraint_conrelid,
- BTEqualStrategyNumber, F_OIDEQ,
- ObjectIdGetDatum(fk->conrelid));
-
- scan = systable_beginscan(pg_constraint,
- ConstraintRelidTypidNameIndexId,
- true, NULL, 1, &key);
- objs = new_object_addresses();
- while ((consttup = systable_getnext(scan)) != NULL)
- {
- Form_pg_constraint conform = (Form_pg_constraint) GETSTRUCT(consttup);
-
- if (conform->conparentid != fk->conoid)
- continue;
- else
- {
- ObjectAddress addr;
- SysScanDesc scan2;
- ScanKeyData key2;
- int n PG_USED_FOR_ASSERTS_ONLY;
-
- ObjectAddressSet(addr, ConstraintRelationId, conform->oid);
- add_exact_object_address(&addr, objs);
-
- /*
- * First we must delete the dependency record that binds the
- * constraint records together.
- */
- n = deleteDependencyRecordsForSpecific(ConstraintRelationId,
- conform->oid,
- DEPENDENCY_INTERNAL,
- ConstraintRelationId,
- fk->conoid);
- Assert(n == 1); /* actually only one is expected */
-
- /*
- * Now search for the triggers for this constraint and set
- * them up for deletion too
- */
- ScanKeyInit(&key2,
- Anum_pg_trigger_tgconstraint,
- BTEqualStrategyNumber, F_OIDEQ,
- ObjectIdGetDatum(conform->oid));
- scan2 = systable_beginscan(trigrel, TriggerConstraintIndexId,
- true, NULL, 1, &key2);
- while ((trigtup = systable_getnext(scan2)) != NULL)
- {
- ObjectAddressSet(addr, TriggerRelationId,
- ((Form_pg_trigger) GETSTRUCT(trigtup))->oid);
- add_exact_object_address(&addr, objs);
- }
- systable_endscan(scan2);
- }
- }
- /* make the dependency deletions visible */
- CommandCounterIncrement();
- performMultipleDeletions(objs, DROP_RESTRICT,
- PERFORM_DELETION_INTERNAL);
- systable_endscan(scan);
+ RemoveInheritedConstraint(pg_constraint, trigrel, partConstrOid,
+ partConstrRelid);
table_close(pg_constraint, RowShareLock);
}
@@ -11567,9 +11527,10 @@ tryAttachPartitionForeignKey(List **wqueue,
Relation conrel;
conrel = table_open(ConstraintRelationId, RowExclusiveLock);
- partcontup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
+
+ partcontup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(partConstrOid));
if (!HeapTupleIsValid(partcontup))
- elog(ERROR, "cache lookup failed for constraint %u", fk->conoid);
+ elog(ERROR, "cache lookup failed for constraint %u", partConstrOid);
/* Use the same lock as for AT_ValidateConstraint */
QueueFKConstraintValidation(wqueue, conrel, partition, partcontup,
@@ -11577,8 +11538,136 @@ tryAttachPartitionForeignKey(List **wqueue,
ReleaseSysCache(partcontup);
table_close(conrel, RowExclusiveLock);
}
+}
- return true;
+/*
+ * RemoveInheritedConstraint
+ *
+ * Removes the constraint and its associated trigger from the specified
+ * relation, which inherited the given constraint.
+ */
+static void
+RemoveInheritedConstraint(Relation conrel, Relation trigrel, Oid conoid,
+ Oid conrelid)
+{
+ ObjectAddresses *objs;
+ HeapTuple consttup;
+ ScanKeyData key;
+ SysScanDesc scan;
+ HeapTuple trigtup;
+
+ ScanKeyInit(&key,
+ Anum_pg_constraint_conrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conrelid));
+
+ scan = systable_beginscan(conrel,
+ ConstraintRelidTypidNameIndexId,
+ true, NULL, 1, &key);
+ objs = new_object_addresses();
+ while ((consttup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_constraint conform = (Form_pg_constraint) GETSTRUCT(consttup);
+
+ if (conform->conparentid != conoid)
+ continue;
+ else
+ {
+ ObjectAddress addr;
+ SysScanDesc scan2;
+ ScanKeyData key2;
+ int n PG_USED_FOR_ASSERTS_ONLY;
+
+ ObjectAddressSet(addr, ConstraintRelationId, conform->oid);
+ add_exact_object_address(&addr, objs);
+
+ /*
+ * First we must delete the dependency record that binds the
+ * constraint records together.
+ */
+ n = deleteDependencyRecordsForSpecific(ConstraintRelationId,
+ conform->oid,
+ DEPENDENCY_INTERNAL,
+ ConstraintRelationId,
+ conoid);
+ Assert(n == 1); /* actually only one is expected */
+
+ /*
+ * Now search for the triggers for this constraint and set them up
+ * for deletion too
+ */
+ ScanKeyInit(&key2,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conform->oid));
+ scan2 = systable_beginscan(trigrel, TriggerConstraintIndexId,
+ true, NULL, 1, &key2);
+ while ((trigtup = systable_getnext(scan2)) != NULL)
+ {
+ ObjectAddressSet(addr, TriggerRelationId,
+ ((Form_pg_trigger) GETSTRUCT(trigtup))->oid);
+ add_exact_object_address(&addr, objs);
+ }
+ systable_endscan(scan2);
+ }
+ }
+ /* make the dependency deletions visible */
+ CommandCounterIncrement();
+ performMultipleDeletions(objs, DROP_RESTRICT,
+ PERFORM_DELETION_INTERNAL);
+ systable_endscan(scan);
+}
+
+/*
+ * DropForeignKeyConstraintTriggers
+ *
+ * The subroutine for tryAttachPartitionForeignKey handles the deletion of
+ * action triggers for the foreign key constraint.
+ */
+static void
+DropForeignKeyConstraintTriggers(Relation trigrel, Oid conoid, Oid confrelid,
+ Oid conrelid)
+{
+ ScanKeyData key;
+ SysScanDesc scan;
+ HeapTuple trigtup;
+
+ ScanKeyInit(&key,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+ scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
+ NULL, 1, &key);
+ while ((trigtup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
+ ObjectAddress trigger;
+
+ if (trgform->tgconstrrelid != conrelid)
+ continue;
+ if (trgform->tgrelid != confrelid)
+ continue;
+
+ /*
+ * The constraint is originally set up to contain this trigger as an
+ * implementation object, so there's a dependency record that links
+ * the two; however, since the trigger is no longer needed, we remove
+ * the dependency link in order to be able to drop the trigger while
+ * keeping the constraint intact.
+ */
+ deleteDependencyRecordsFor(TriggerRelationId,
+ trgform->oid,
+ false);
+ /* make dependency deletion visible to performDeletion */
+ CommandCounterIncrement();
+ ObjectAddressSet(trigger, TriggerRelationId,
+ trgform->oid);
+ performDeletion(&trigger, DROP_RESTRICT, 0);
+ /* make trigger drop visible, in case the loop iterates */
+ CommandCounterIncrement();
+ }
+
+ systable_endscan(scan);
}
/*
--
2.43.5
v12-0003-refactor-Pass-Relid-instead-of-Relation-to-creat.patchapplication/octet-stream; name=v12-0003-refactor-Pass-Relid-instead-of-Relation-to-creat.patchDownload
From 89a14dc874388b13ef98967ec28a33e1fdec0425 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 20 Jan 2025 18:49:41 +0530
Subject: [PATCH v12 3/7] refactor: Pass Relid instead of Relation to
createForeignKeyCheckTriggers().
Currently, createForeignKeyCheckTriggers() takes a Relation type as
its first argument, but it doesn't use that argument directly.
Instead, it fetches the Relid by calling RelationGetRelid().
Therefore, it would be more consistent with other functions (e.g.,
createForeignKeyCheckTriggers()) to pass the Relid directly instead of
the whole Relation.
---
src/backend/commands/tablecmds.c | 20 ++++++++++----------
1 file changed, 10 insertions(+), 10 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c577f5100af..bee18819290 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -569,7 +569,7 @@ static void createForeignKeyCheckTriggers(Oid myRelOid, Oid refRelOid,
Oid indexOid,
Oid parentInsTrigger, Oid parentUpdTrigger,
Oid *insertTrigOid, Oid *updateTrigOid);
-static void createForeignKeyActionTriggers(Relation rel, Oid refRelOid,
+static void createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid,
Constraint *fkconstraint, Oid constraintOid,
Oid indexOid,
Oid parentDelTrigger, Oid parentUpdTrigger,
@@ -10569,7 +10569,8 @@ addFkRecurseReferenced(Constraint *fkconstraint, Relation rel,
/*
* Create the action triggers that enforce the constraint.
*/
- createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
+ createForeignKeyActionTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
fkconstraint,
parentConstr, indexOid,
parentDelTrigger, parentUpdTrigger,
@@ -12944,10 +12945,11 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
* *updateTrigOid.
*/
static void
-createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstraint,
- Oid constraintOid, Oid indexOid,
- Oid parentDelTrigger, Oid parentUpdTrigger,
- Oid *deleteTrigOid, Oid *updateTrigOid)
+createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid,
+ Constraint *fkconstraint, Oid constraintOid,
+ Oid indexOid, Oid parentDelTrigger,
+ Oid parentUpdTrigger, Oid *deleteTrigOid,
+ Oid *updateTrigOid)
{
CreateTrigStmt *fk_trigger;
ObjectAddress trigAddress;
@@ -13003,8 +13005,7 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
break;
}
- trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid,
- RelationGetRelid(rel),
+ trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid, myRelOid,
constraintOid, indexOid, InvalidOid,
parentDelTrigger, NULL, true, false);
if (deleteTrigOid)
@@ -13064,8 +13065,7 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
break;
}
- trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid,
- RelationGetRelid(rel),
+ trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid, myRelOid,
constraintOid, indexOid, InvalidOid,
parentUpdTrigger, NULL, true, false);
if (updateTrigOid)
--
2.43.5
v12-0004-refactor-Change-ATExecAlterConstrRecurse-argumen.patchapplication/octet-stream; name=v12-0004-refactor-Change-ATExecAlterConstrRecurse-argumen.patchDownload
From 2bbd328cc35fdf2594587db1f13fcfcca7bee575 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Tue, 14 Jan 2025 22:35:32 +0530
Subject: [PATCH v12 4/7] refactor: Change ATExecAlterConstrRecurse() argument.
Instead of passing the Relation of the referencing relation, we will
pass a relid. Opening the relation using the relid again should not
cause significant issues. However, this approach makes recursion more
efficient, especially in the later patch where we will be recreating
the referencing and referred triggers. In addition to that passing
relid of the referred relation too.
----
NOTE: This patch is not meant to be committed separately. It should
be squashed with the main patch that adds ENFORCED/NOT ENFORCED.
----
---
src/backend/commands/tablecmds.c | 44 +++++++++++++++++---------------
1 file changed, 23 insertions(+), 21 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index bee18819290..c5447837fa9 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -392,14 +392,16 @@ static void AlterSeqNamespaces(Relation classRel, Relation rel,
static ObjectAddress ATExecAlterConstraint(Relation rel, AlterConstraintStmt *cmdcon,
bool recurse, bool recursing, LOCKMODE lockmode);
static bool ATExecAlterConstrRecurse(AlterConstraintStmt *cmdcon, Relation conrel,
- Relation tgrel, Relation rel, HeapTuple contuple,
- List **otherrelids, LOCKMODE lockmode);
+ Relation tgrel, const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode);
static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
bool deferrable, bool initdeferred,
List **otherrelids);
static void ATExecAlterChildConstr(AlterConstraintStmt *cmdcon, Relation conrel,
- Relation tgrel, Relation rel, HeapTuple contuple,
- List **otherrelids, LOCKMODE lockmode);
+ Relation tgrel, const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode);
static ObjectAddress ATExecValidateConstraint(List **wqueue,
Relation rel, char *constrName,
bool recurse, bool recursing, LOCKMODE lockmode);
@@ -11906,8 +11908,9 @@ ATExecAlterConstraint(Relation rel, AlterConstraintStmt *cmdcon, bool recurse,
currcon->condeferred != cmdcon->initdeferred ||
rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
- if (ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, rel, contuple,
- &otherrelids, lockmode))
+ if (ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, currcon->conrelid,
+ currcon->confrelid, contuple, &otherrelids,
+ lockmode))
ObjectAddressSet(address, ConstraintRelationId, currcon->oid);
}
@@ -11940,13 +11943,15 @@ ATExecAlterConstraint(Relation rel, AlterConstraintStmt *cmdcon, bool recurse,
*/
static bool
ATExecAlterConstrRecurse(AlterConstraintStmt *cmdcon, Relation conrel,
- Relation tgrel, Relation rel, HeapTuple contuple,
- List **otherrelids, LOCKMODE lockmode)
+ Relation tgrel, const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode)
{
Form_pg_constraint currcon;
Oid conoid;
Oid refrelid;
bool changed = false;
+ Relation rel;
/* since this function recurses, it could be driven to stack overflow */
check_stack_depth();
@@ -11955,6 +11960,8 @@ ATExecAlterConstrRecurse(AlterConstraintStmt *cmdcon, Relation conrel,
conoid = currcon->oid;
refrelid = currcon->confrelid;
+ rel = table_open(currcon->conrelid, lockmode);
+
/*
* Update pg_constraint with the flags from cmdcon.
*
@@ -12000,8 +12007,9 @@ ATExecAlterConstrRecurse(AlterConstraintStmt *cmdcon, Relation conrel,
*/
if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)
- ATExecAlterChildConstr(cmdcon, conrel, tgrel, rel, contuple,
- otherrelids, lockmode);
+ ATExecAlterChildConstr(cmdcon, conrel, tgrel, fkrelid, pkrelid,
+ contuple, otherrelids, lockmode);
+ table_close(rel, NoLock);
return changed;
}
@@ -12080,8 +12088,9 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
*/
static void
ATExecAlterChildConstr(AlterConstraintStmt *cmdcon, Relation conrel,
- Relation tgrel, Relation rel, HeapTuple contuple,
- List **otherrelids, LOCKMODE lockmode)
+ Relation tgrel, const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode)
{
Form_pg_constraint currcon;
Oid conoid;
@@ -12101,15 +12110,8 @@ ATExecAlterChildConstr(AlterConstraintStmt *cmdcon, Relation conrel,
true, NULL, 1, &pkey);
while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
- {
- Form_pg_constraint childcon = (Form_pg_constraint) GETSTRUCT(childtup);
- Relation childrel;
-
- childrel = table_open(childcon->conrelid, lockmode);
- ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, childrel, childtup,
- otherrelids, lockmode);
- table_close(childrel, NoLock);
- }
+ ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, fkrelid, pkrelid,
+ childtup, otherrelids, lockmode);
systable_endscan(pscan);
}
--
2.43.5
v12-0005-Remove-hastriggers-flag-check-before-fetching-FK.patchapplication/octet-stream; name=v12-0005-Remove-hastriggers-flag-check-before-fetching-FK.patchDownload
From f20b600805427d056394a03e799c556954173170 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 7 Oct 2024 12:40:07 +0530
Subject: [PATCH v12 5/7] Remove hastriggers flag check before fetching FK
constraints.
With NOT ENFORCED, foreign key constraints will be created without
triggers. Therefore, the criteria for fetching foreign keys based on
the presence of triggers no longer apply.
----
NOTE: This patch is intended to reduce the diff noise from the main
patch and is not meant to be committed separately. It should be
squashed with the main patch that adds ENFORCED/NOT ENFORCED.
----
---
src/backend/utils/cache/relcache.c | 5 -
src/bin/pg_dump/pg_dump.c | 8 +-
src/bin/psql/describe.c | 226 ++++++++++++++---------------
3 files changed, 108 insertions(+), 131 deletions(-)
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 43219a9629c..485cd2178bc 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4658,11 +4658,6 @@ RelationGetFKeyList(Relation relation)
if (relation->rd_fkeyvalid)
return relation->rd_fkeylist;
- /* Fast path: non-partitioned tables without triggers can't have FKs */
- if (!relation->rd_rel->relhastriggers &&
- relation->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
- return NIL;
-
/*
* We build the list we intend to return (in the caller's context) while
* doing the scan. After successfully completing the scan, we copy that
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 02e1fdf8f78..a7887251834 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -7842,13 +7842,7 @@ getConstraints(Archive *fout, TableInfo tblinfo[], int numTables)
{
TableInfo *tinfo = &tblinfo[i];
- /*
- * For partitioned tables, foreign keys have no triggers so they must
- * be included anyway in case some foreign keys are defined.
- */
- if ((!tinfo->hastriggers &&
- tinfo->relkind != RELKIND_PARTITIONED_TABLE) ||
- !(tinfo->dobj.dump & DUMP_COMPONENT_DEFINITION))
+ if (!(tinfo->dobj.dump & DUMP_COMPONENT_DEFINITION))
continue;
/* OK, we need info for this table */
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index aa4363b200a..c633306b3cb 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2544,136 +2544,124 @@ describeOneTableDetails(const char *schemaname,
PQclear(result);
}
- /*
- * Print foreign-key constraints (there are none if no triggers,
- * except if the table is partitioned, in which case the triggers
- * appear in the partitions)
- */
- if (tableinfo.hastriggers ||
- tableinfo.relkind == RELKIND_PARTITIONED_TABLE)
+ /* Print foreign-key constraints */
+ if (pset.sversion >= 120000 &&
+ (tableinfo.ispartition || tableinfo.relkind == RELKIND_PARTITIONED_TABLE))
{
- if (pset.sversion >= 120000 &&
- (tableinfo.ispartition || tableinfo.relkind == RELKIND_PARTITIONED_TABLE))
+ /*
+ * Put the constraints defined in this table first, followed
+ * by the constraints defined in ancestor partitioned tables.
+ */
+ printfPQExpBuffer(&buf,
+ "SELECT conrelid = '%s'::pg_catalog.regclass AS sametable,\n"
+ " conname,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef,\n"
+ " conrelid::pg_catalog.regclass AS ontable\n"
+ " FROM pg_catalog.pg_constraint,\n"
+ " pg_catalog.pg_partition_ancestors('%s')\n"
+ " WHERE conrelid = relid AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
+ "ORDER BY sametable DESC, conname;",
+ oid, oid);
+ }
+ else
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT true as sametable, conname,\n"
+ " pg_catalog.pg_get_constraintdef(r.oid, true) as condef,\n"
+ " conrelid::pg_catalog.regclass AS ontable\n"
+ "FROM pg_catalog.pg_constraint r\n"
+ "WHERE r.conrelid = '%s' AND r.contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n",
+ oid);
+
+ if (pset.sversion >= 120000)
+ appendPQExpBufferStr(&buf, " AND conparentid = 0\n");
+ appendPQExpBufferStr(&buf, "ORDER BY conname");
+ }
+
+ result = PSQLexec(buf.data);
+ if (!result)
+ goto error_return;
+ else
+ tuples = PQntuples(result);
+
+ if (tuples > 0)
+ {
+ int i_sametable = PQfnumber(result, "sametable"),
+ i_conname = PQfnumber(result, "conname"),
+ i_condef = PQfnumber(result, "condef"),
+ i_ontable = PQfnumber(result, "ontable");
+
+ printTableAddFooter(&cont, _("Foreign-key constraints:"));
+ for (i = 0; i < tuples; i++)
{
/*
- * Put the constraints defined in this table first, followed
- * by the constraints defined in ancestor partitioned tables.
+ * Print untranslated constraint name and definition. Use
+ * a "TABLE tab" prefix when the constraint is defined in
+ * a parent partitioned table.
*/
- printfPQExpBuffer(&buf,
- "SELECT conrelid = '%s'::pg_catalog.regclass AS sametable,\n"
- " conname,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef,\n"
- " conrelid::pg_catalog.regclass AS ontable\n"
- " FROM pg_catalog.pg_constraint,\n"
- " pg_catalog.pg_partition_ancestors('%s')\n"
- " WHERE conrelid = relid AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
- "ORDER BY sametable DESC, conname;",
- oid, oid);
- }
- else
- {
- printfPQExpBuffer(&buf,
- "SELECT true as sametable, conname,\n"
- " pg_catalog.pg_get_constraintdef(r.oid, true) as condef,\n"
- " conrelid::pg_catalog.regclass AS ontable\n"
- "FROM pg_catalog.pg_constraint r\n"
- "WHERE r.conrelid = '%s' AND r.contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n",
- oid);
-
- if (pset.sversion >= 120000)
- appendPQExpBufferStr(&buf, " AND conparentid = 0\n");
- appendPQExpBufferStr(&buf, "ORDER BY conname");
- }
-
- result = PSQLexec(buf.data);
- if (!result)
- goto error_return;
- else
- tuples = PQntuples(result);
-
- if (tuples > 0)
- {
- int i_sametable = PQfnumber(result, "sametable"),
- i_conname = PQfnumber(result, "conname"),
- i_condef = PQfnumber(result, "condef"),
- i_ontable = PQfnumber(result, "ontable");
-
- printTableAddFooter(&cont, _("Foreign-key constraints:"));
- for (i = 0; i < tuples; i++)
- {
- /*
- * Print untranslated constraint name and definition. Use
- * a "TABLE tab" prefix when the constraint is defined in
- * a parent partitioned table.
- */
- if (strcmp(PQgetvalue(result, i, i_sametable), "f") == 0)
- printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
- PQgetvalue(result, i, i_ontable),
- PQgetvalue(result, i, i_conname),
- PQgetvalue(result, i, i_condef));
- else
- printfPQExpBuffer(&buf, " \"%s\" %s",
- PQgetvalue(result, i, i_conname),
- PQgetvalue(result, i, i_condef));
-
- printTableAddFooter(&cont, buf.data);
- }
- }
- PQclear(result);
- }
-
- /* print incoming foreign-key references */
- if (tableinfo.hastriggers ||
- tableinfo.relkind == RELKIND_PARTITIONED_TABLE)
- {
- if (pset.sversion >= 120000)
- {
- printfPQExpBuffer(&buf,
- "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
- " FROM pg_catalog.pg_constraint c\n"
- " WHERE confrelid IN (SELECT pg_catalog.pg_partition_ancestors('%s')\n"
- " UNION ALL VALUES ('%s'::pg_catalog.regclass))\n"
- " AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
- "ORDER BY conname;",
- oid, oid);
- }
- else
- {
- printfPQExpBuffer(&buf,
- "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
- " FROM pg_catalog.pg_constraint\n"
- " WHERE confrelid = %s AND contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n"
- "ORDER BY conname;",
- oid);
- }
-
- result = PSQLexec(buf.data);
- if (!result)
- goto error_return;
- else
- tuples = PQntuples(result);
-
- if (tuples > 0)
- {
- int i_conname = PQfnumber(result, "conname"),
- i_ontable = PQfnumber(result, "ontable"),
- i_condef = PQfnumber(result, "condef");
-
- printTableAddFooter(&cont, _("Referenced by:"));
- for (i = 0; i < tuples; i++)
- {
+ if (strcmp(PQgetvalue(result, i, i_sametable), "f") == 0)
printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
PQgetvalue(result, i, i_ontable),
PQgetvalue(result, i, i_conname),
PQgetvalue(result, i, i_condef));
+ else
+ printfPQExpBuffer(&buf, " \"%s\" %s",
+ PQgetvalue(result, i, i_conname),
+ PQgetvalue(result, i, i_condef));
- printTableAddFooter(&cont, buf.data);
- }
+ printTableAddFooter(&cont, buf.data);
}
- PQclear(result);
}
+ PQclear(result);
+
+ /* print incoming foreign-key references */
+ if (pset.sversion >= 120000)
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
+ " FROM pg_catalog.pg_constraint c\n"
+ " WHERE confrelid IN (SELECT pg_catalog.pg_partition_ancestors('%s')\n"
+ " UNION ALL VALUES ('%s'::pg_catalog.regclass))\n"
+ " AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
+ "ORDER BY conname;",
+ oid, oid);
+ }
+ else
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
+ " FROM pg_catalog.pg_constraint\n"
+ " WHERE confrelid = %s AND contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n"
+ "ORDER BY conname;",
+ oid);
+ }
+
+ result = PSQLexec(buf.data);
+ if (!result)
+ goto error_return;
+ else
+ tuples = PQntuples(result);
+
+ if (tuples > 0)
+ {
+ int i_conname = PQfnumber(result, "conname"),
+ i_ontable = PQfnumber(result, "ontable"),
+ i_condef = PQfnumber(result, "condef");
+
+ printTableAddFooter(&cont, _("Referenced by:"));
+ for (i = 0; i < tuples; i++)
+ {
+ printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
+ PQgetvalue(result, i, i_ontable),
+ PQgetvalue(result, i, i_conname),
+ PQgetvalue(result, i, i_condef));
+
+ printTableAddFooter(&cont, buf.data);
+ }
+ }
+ PQclear(result);
/* print any row-level policies */
if (pset.sversion >= 90500)
--
2.43.5
v12-0006-Add-support-for-NOT-ENFORCED-in-foreign-key-cons.patchapplication/octet-stream; name=v12-0006-Add-support-for-NOT-ENFORCED-in-foreign-key-cons.patchDownload
From ece7cd8433c502c123ebc00e9fb2ef1c37e21213 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 20 Jan 2025 21:45:07 +0530
Subject: [PATCH v12 6/7] Add support for NOT ENFORCED in foreign key
constraints.
Typically, when a foreign key (FK) constraint is created on a table,
action and check triggers are added to maintain data integrity. With
this patch, if a constraint is marked as NOT ENFORCED, integrity
checks are no longer required, making these triggers unnecessary.
Consequently, when creating a NOT ENFORCED FK constraint, triggers
will not be created, and the constraint will be marked as NOT VALID.
Similarly, if an existing FK constraint is changed to NOT ENFORCED,
the associated triggers will be dropped, and the constraint will also
be marked as NOT VALID. Conversely, if a NOT ENFORCED FK constraint is
changed to ENFORCED, the necessary triggers will be created, and the
constraint will remain in the NOT VALID state.
----
NOTE: In this patch, the tryAttachPartitionForeignKey() function will
not merge the constraint if the enforcibility differs. This will be
addressed in the next patch.
----
---
doc/src/sgml/advanced.sgml | 13 +
doc/src/sgml/catalogs.sgml | 2 +-
doc/src/sgml/ddl.sgml | 12 +-
doc/src/sgml/ref/alter_table.sgml | 12 +-
doc/src/sgml/ref/create_table.sgml | 2 +-
src/backend/catalog/pg_constraint.c | 5 +-
src/backend/catalog/sql_features.txt | 2 +-
src/backend/commands/tablecmds.c | 429 +++++++++++++++++-----
src/backend/parser/gram.y | 12 +-
src/backend/parser/parse_utilcmd.c | 14 +-
src/backend/utils/cache/relcache.c | 1 +
src/include/nodes/parsenodes.h | 12 +-
src/include/utils/rel.h | 3 +
src/test/regress/expected/constraints.out | 11 +-
src/test/regress/expected/foreign_key.out | 162 +++++++-
src/test/regress/sql/constraints.sql | 1 -
src/test/regress/sql/foreign_key.sql | 109 +++++-
17 files changed, 664 insertions(+), 138 deletions(-)
diff --git a/doc/src/sgml/advanced.sgml b/doc/src/sgml/advanced.sgml
index e15a3323dfb..14ed11e1f4d 100644
--- a/doc/src/sgml/advanced.sgml
+++ b/doc/src/sgml/advanced.sgml
@@ -125,6 +125,19 @@ ERROR: insert or update on table "weather" violates foreign key constraint "wea
DETAIL: Key (city)=(Berkeley) is not present in table "cities".
</screen>
</para>
+ <para>
+ If, for some reason, you do not want to enforce this constraint and wish to
+ avoid the error, you can modify the foreign key constraint to <literal>NOT ENFORCED</literal>,
+ or specify it in the <link linkend="sql-createtable"><command>CREATE TABLE</command></link>.
+ </para>
+ <para>
+ Let's change the constraint to <literal>NOT ENFORCED</literal>, and when you
+ attempt to reinsert the same record, it will not result in any error.
+<programlisting>
+ALTER TABLE weather ALTER CONSTRAINT weather_city_fkey NOT ENFORCED;
+INSERT INTO weather VALUES ('Berkeley', 45, 53, 0.0, '1994-11-28');
+</programlisting>
+ </para>
<para>
The behavior of foreign keys can be finely tuned to your
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 088fb175cce..d8aa52993a8 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2597,7 +2597,7 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l
</para>
<para>
Is the constraint enforced?
- Currently, can be false only for CHECK constraints
+ Currently, can be false only for foreign keys and CHECK constraints
</para></entry>
</row>
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index 7ff39ae8c67..4779cc4f839 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -1055,11 +1055,13 @@ CREATE TABLE example (
</indexterm>
<para>
- A foreign key constraint specifies that the values in a column (or
- a group of columns) must match the values appearing in some row
- of another table.
- We say this maintains the <firstterm>referential
- integrity</firstterm> between two related tables.
+ A foreign key constraint with <literal>ENFORCED</literal>, the default
+ setting, specifies that the values in a column (or a group of columns)
+ must match the values appearing in some row of another table. We say this
+ maintains the <firstterm>referential integrity</firstterm> between two
+ related tables. If a foreign key constraint is created with <literal>NOT ENFORCED</literal>,
+ the check for aforementioned <firstterm>referential integrity</firstterm>
+ will not be performed.
</para>
<para>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index f9576da435e..8d4d0dec9d2 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -58,7 +58,7 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable>
ALTER [ COLUMN ] <replaceable class="parameter">column_name</replaceable> SET COMPRESSION <replaceable class="parameter">compression_method</replaceable>
ADD <replaceable class="parameter">table_constraint</replaceable> [ NOT VALID ]
ADD <replaceable class="parameter">table_constraint_using_index</replaceable>
- ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+ ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]
VALIDATE CONSTRAINT <replaceable class="parameter">constraint_name</replaceable>
DROP CONSTRAINT [ IF EXISTS ] <replaceable class="parameter">constraint_name</replaceable> [ RESTRICT | CASCADE ]
DISABLE TRIGGER [ <replaceable class="parameter">trigger_name</replaceable> | ALL | USER ]
@@ -553,6 +553,13 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
This form alters the attributes of a constraint that was previously
created. Currently only foreign key constraints may be altered.
</para>
+ <para>
+ Note that when a constraint is changed to <literal>NOT ENFORCED</literal>,
+ it is also marked as <literal>NOT VALID</literal>. While this might seem
+ unnecessary, it is done for consistency and should be set accordingly.
+ This ensures that if the constraint is later changed to <literal>ENFORCED</literal>,
+ it will automatically be in the correct <literal>NOT VALID</literal> state.
+ </para>
</listitem>
</varlistentry>
@@ -563,7 +570,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
This form validates a foreign key or check constraint that was
previously created as <literal>NOT VALID</literal>, by scanning the
table to ensure there are no rows for which the constraint is not
- satisfied. Nothing happens if the constraint is already marked valid.
+ satisfied. If the constraint is not enforced, an error is thrown.
+ Nothing happens if the constraint is already marked valid.
(See <xref linkend="sql-altertable-notes"/> below for an explanation
of the usefulness of this command.)
</para>
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index 2237321cb4f..5071dfeabf9 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -1401,7 +1401,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</para>
<para>
- This is currently only supported for <literal>CHECK</literal>
+ This is currently only supported for foreign key and <literal>CHECK</literal>
constraints.
</para>
</listitem>
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index ac80652baf2..0467e7442ff 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -100,8 +100,9 @@ CreateConstraintEntry(const char *constraintName,
ObjectAddresses *addrs_auto;
ObjectAddresses *addrs_normal;
- /* Only CHECK constraint can be not enforced */
- Assert(isEnforced || constraintType == CONSTRAINT_CHECK);
+ /* Only CHECK or FOREIGN KEY constraint can be not enforced */
+ Assert(isEnforced || constraintType == CONSTRAINT_CHECK ||
+ constraintType == CONSTRAINT_FOREIGN);
/* NOT ENFORCED constraint must be NOT VALID */
Assert(isEnforced || !isValidated);
diff --git a/src/backend/catalog/sql_features.txt b/src/backend/catalog/sql_features.txt
index 2f250d2c57b..d49471bbdf2 100644
--- a/src/backend/catalog/sql_features.txt
+++ b/src/backend/catalog/sql_features.txt
@@ -281,7 +281,7 @@ F461 Named character sets NO
F471 Scalar subquery values YES
F481 Expanded NULL predicate YES
F491 Constraint management YES
-F492 Optional table constraint enforcement NO check constraints only
+F492 Optional table constraint enforcement NO foreign key and check constraints only
F501 Features and conformance views YES
F501 Features and conformance views 01 SQL_FEATURES view YES
F501 Features and conformance views 02 SQL_SIZING view YES
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c5447837fa9..d3930f8e4f3 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -394,8 +394,19 @@ static ObjectAddress ATExecAlterConstraint(Relation rel, AlterConstraintStmt *cm
static bool ATExecAlterConstrRecurse(AlterConstraintStmt *cmdcon, Relation conrel,
Relation tgrel, const Oid fkrelid, const Oid pkrelid,
HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode);
-static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
+ LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger);
+static void ATExecAlterConstrEnforceability(AlterConstraintStmt *cmdcon, Relation conrel,
+ Relation tgrel, const Oid fkrelid,
+ const Oid pkrelid, HeapTuple contuple,
+ List **otherrelids, LOCKMODE lockmode,
+ Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger);
+static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Oid fkrel,
bool deferrable, bool initdeferred,
List **otherrelids);
static void ATExecAlterChildConstr(AlterConstraintStmt *cmdcon, Relation conrel,
@@ -10444,7 +10455,7 @@ addFkConstraint(addFkConstraintSides fkside,
CONSTRAINT_FOREIGN,
fkconstraint->deferrable,
fkconstraint->initdeferred,
- true, /* Is Enforced */
+ fkconstraint->is_enforced,
fkconstraint->initially_valid,
parentConstr,
RelationGetRelid(rel),
@@ -10562,21 +10573,23 @@ addFkRecurseReferenced(Constraint *fkconstraint, Relation rel,
Oid parentDelTrigger, Oid parentUpdTrigger,
bool with_period)
{
- Oid deleteTriggerOid,
- updateTriggerOid;
+ Oid deleteTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
Assert(CheckRelationLockedByMe(pkrel, ShareRowExclusiveLock, true));
Assert(CheckRelationLockedByMe(rel, ShareRowExclusiveLock, true));
/*
- * Create the action triggers that enforce the constraint.
+ * Create action triggers to enforce the constraint, or skip them if the
+ * constraint is not enforced.
*/
- createForeignKeyActionTriggers(RelationGetRelid(rel),
- RelationGetRelid(pkrel),
- fkconstraint,
- parentConstr, indexOid,
- parentDelTrigger, parentUpdTrigger,
- &deleteTriggerOid, &updateTriggerOid);
+ if (fkconstraint->is_enforced)
+ createForeignKeyActionTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
+ fkconstraint,
+ parentConstr, indexOid,
+ parentDelTrigger, parentUpdTrigger,
+ &deleteTriggerOid, &updateTriggerOid);
/*
* If the referenced table is partitioned, recurse on ourselves to handle
@@ -10697,8 +10710,8 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
Oid parentInsTrigger, Oid parentUpdTrigger,
bool with_period)
{
- Oid insertTriggerOid,
- updateTriggerOid;
+ Oid insertTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
Assert(OidIsValid(parentConstr));
Assert(CheckRelationLockedByMe(rel, ShareRowExclusiveLock, true));
@@ -10710,29 +10723,32 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
errmsg("foreign key constraints are not supported on foreign tables")));
/*
- * Add the check triggers to it and, if necessary, schedule it to be
- * checked in Phase 3.
+ * Add check triggers if the constraint is enforced, and if needed,
+ * schedule them to be checked in Phase 3.
*
* If the relation is partitioned, drill down to do it to its partitions.
*/
- createForeignKeyCheckTriggers(RelationGetRelid(rel),
- RelationGetRelid(pkrel),
- fkconstraint,
- parentConstr,
- indexOid,
- parentInsTrigger, parentUpdTrigger,
- &insertTriggerOid, &updateTriggerOid);
+ if (fkconstraint->is_enforced)
+ createForeignKeyCheckTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
+ fkconstraint,
+ parentConstr,
+ indexOid,
+ parentInsTrigger, parentUpdTrigger,
+ &insertTriggerOid, &updateTriggerOid);
if (rel->rd_rel->relkind == RELKIND_RELATION)
{
/*
* Tell Phase 3 to check that the constraint is satisfied by existing
- * rows. We can skip this during table creation, when requested
- * explicitly by specifying NOT VALID in an ADD FOREIGN KEY command,
- * and when we're recreating a constraint following a SET DATA TYPE
- * operation that did not impugn its validity.
+ * rows. We can skip this during table creation, when constraint is
+ * specified as NOT ENFORCED, when requested explicitly by specifying
+ * NOT VALID in an ADD FOREIGN KEY command, and when we're recreating
+ * a constraint following a SET DATA TYPE operation that did not
+ * impugn its validity.
*/
- if (wqueue && !old_check_ok && !fkconstraint->skip_validation)
+ if (wqueue && !old_check_ok && !fkconstraint->skip_validation &&
+ fkconstraint->is_enforced)
{
NewConstraint *newcon;
AlteredTableInfo *tab;
@@ -10963,8 +10979,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
AttrNumber confdelsetcols[INDEX_MAX_KEYS];
Constraint *fkconstraint;
ObjectAddress address;
- Oid deleteTriggerOid,
- updateTriggerOid;
+ Oid deleteTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(constrOid));
if (!HeapTupleIsValid(tuple))
@@ -11024,8 +11040,9 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
fkconstraint->fk_del_set_cols = NIL;
fkconstraint->old_conpfeqop = NIL;
fkconstraint->old_pktable_oid = InvalidOid;
+ fkconstraint->is_enforced = constrForm->conenforced;
fkconstraint->skip_validation = false;
- fkconstraint->initially_valid = true;
+ fkconstraint->initially_valid = constrForm->convalidated;
/* set up colnames that are used to generate the constraint name */
for (int i = 0; i < numfks; i++)
@@ -11053,9 +11070,10 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
* parent OIDs for similar triggers that will be created on the
* partition in addFkRecurseReferenced().
*/
- GetForeignKeyActionTriggers(trigrel, constrOid,
- constrForm->confrelid, constrForm->conrelid,
- &deleteTriggerOid, &updateTriggerOid);
+ if (constrForm->conenforced)
+ GetForeignKeyActionTriggers(trigrel, constrOid,
+ constrForm->confrelid, constrForm->conrelid,
+ &deleteTriggerOid, &updateTriggerOid);
/* Add this constraint ... */
address = addFkConstraint(addFkReferencedSide,
@@ -11221,17 +11239,18 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
mapped_conkey[i] = attmap->attnums[conkey[i] - 1];
/*
- * Get the "check" triggers belonging to the constraint to pass as
- * parent OIDs for similar triggers that will be created on the
- * partition in addFkRecurseReferencing(). They are also passed to
- * tryAttachPartitionForeignKey() below to simply assign as parents to
- * the partition's existing "check" triggers, that is, if the
- * corresponding constraints is deemed attachable to the parent
- * constraint.
+ * Get the "check" triggers belonging to the constraint, if it is
+ * enforced, to pass as parent OIDs for similar triggers that will be
+ * created on the partition in addFkRecurseReferencing(). They are
+ * also passed to tryAttachPartitionForeignKey() below to simply
+ * assign as parents to the partition's existing "check" triggers,
+ * that is, if the corresponding constraints is deemed attachable to
+ * the parent constraint.
*/
- GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
- constrForm->confrelid, constrForm->conrelid,
- &insertTriggerOid, &updateTriggerOid);
+ if (constrForm->conenforced)
+ GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
+ constrForm->confrelid, constrForm->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
/*
* Before creating a new constraint, see whether any existing FKs are
@@ -11284,6 +11303,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
fkconstraint->fk_del_set_cols = NIL;
fkconstraint->old_conpfeqop = NIL;
fkconstraint->old_pktable_oid = InvalidOid;
+ fkconstraint->is_enforced = constrForm->conenforced;
fkconstraint->skip_validation = false;
fkconstraint->initially_valid = constrForm->convalidated;
for (int i = 0; i < numfks; i++)
@@ -11401,6 +11421,7 @@ tryAttachPartitionForeignKey(List **wqueue,
if (OidIsValid(partConstr->conparentid) ||
partConstr->condeferrable != parentConstr->condeferrable ||
partConstr->condeferred != parentConstr->condeferred ||
+ partConstr->conenforced != parentConstr->conenforced ||
partConstr->confupdtype != parentConstr->confupdtype ||
partConstr->confdeltype != parentConstr->confdeltype ||
partConstr->confmatchtype != parentConstr->confmatchtype)
@@ -11444,8 +11465,9 @@ AttachPartitionForeignKey(List **wqueue,
bool queueValidation;
Oid partConstrFrelid;
Oid partConstrRelid;
- Oid insertTriggerOid,
- updateTriggerOid;
+ bool parentConstrIsEnforced;
+ bool partConstrIsEnforced;
+ bool partConstrParentIsSet;
/* Fetch the parent constraint tuple */
parentConstrTup = SearchSysCache1(CONSTROID,
@@ -11453,6 +11475,7 @@ AttachPartitionForeignKey(List **wqueue,
if (!HeapTupleIsValid(parentConstrTup))
elog(ERROR, "cache lookup failed for constraint %u", parentConstrOid);
parentConstr = (Form_pg_constraint) GETSTRUCT(parentConstrTup);
+ parentConstrIsEnforced = parentConstr->conenforced;
/* Fetch the child constraint tuple */
partcontup = SearchSysCache1(CONSTROID,
@@ -11487,17 +11510,24 @@ AttachPartitionForeignKey(List **wqueue,
/*
* Like the constraint, attach partition's "check" triggers to the
- * corresponding parent triggers.
+ * corresponding parent triggers if the constraint is enforced.
+ * Non-enforced constraints do not have these triggers.
*/
- GetForeignKeyCheckTriggers(trigrel,
- partConstrOid, partConstrFrelid, partConstrRelid,
- &insertTriggerOid, &updateTriggerOid);
- Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
- TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
- RelationGetRelid(partition));
- Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
- TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
- RelationGetRelid(partition));
+ if (parentConstrIsEnforced)
+ {
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
+ GetForeignKeyCheckTriggers(trigrel,
+ partConstrOid, partConstrFrelid, partConstrRelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
+ RelationGetRelid(partition));
+ Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
+ RelationGetRelid(partition));
+ }
/*
* If the referenced table is partitioned, then the partition we're
@@ -11626,6 +11656,10 @@ RemoveInheritedConstraint(Relation conrel, Relation trigrel, Oid conoid,
*
* The subroutine for tryAttachPartitionForeignKey handles the deletion of
* action triggers for the foreign key constraint.
+ *
+ * If valid confrelid and conrelid values are not provided, the respective
+ * trigger check will be skipped, and the trigger will be considered for
+ * removal.
*/
static void
DropForeignKeyConstraintTriggers(Relation trigrel, Oid conoid, Oid confrelid,
@@ -11646,10 +11680,27 @@ DropForeignKeyConstraintTriggers(Relation trigrel, Oid conoid, Oid confrelid,
Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
ObjectAddress trigger;
- if (trgform->tgconstrrelid != conrelid)
+ /* Invalid if trigger is not for a referential integrity constraint */
+ if (!OidIsValid(trgform->tgconstrrelid))
continue;
- if (trgform->tgrelid != confrelid)
+ if (OidIsValid(conrelid) && trgform->tgconstrrelid != conrelid)
continue;
+ if (OidIsValid(confrelid) && trgform->tgrelid != confrelid)
+ continue;
+
+ /* We should be droping trigger related to foreign ke constraint */
+ Assert(trgform->tgfoid == F_RI_FKEY_CHECK_INS ||
+ trgform->tgfoid == F_RI_FKEY_CHECK_UPD ||
+ trgform->tgfoid == F_RI_FKEY_CASCADE_DEL ||
+ trgform->tgfoid == F_RI_FKEY_CASCADE_UPD ||
+ trgform->tgfoid == F_RI_FKEY_RESTRICT_DEL ||
+ trgform->tgfoid == F_RI_FKEY_RESTRICT_UPD ||
+ trgform->tgfoid == F_RI_FKEY_SETNULL_DEL ||
+ trgform->tgfoid == F_RI_FKEY_SETNULL_UPD ||
+ trgform->tgfoid == F_RI_FKEY_SETDEFAULT_DEL ||
+ trgform->tgfoid == F_RI_FKEY_SETDEFAULT_UPD ||
+ trgform->tgfoid == F_RI_FKEY_NOACTION_DEL ||
+ trgform->tgfoid == F_RI_FKEY_NOACTION_UPD);
/*
* The constraint is originally set up to contain this trigger as an
@@ -11849,10 +11900,19 @@ ATExecAlterConstraint(Relation rel, AlterConstraintStmt *cmdcon, bool recurse,
currcon = (Form_pg_constraint) GETSTRUCT(contuple);
if (currcon->contype != CONSTRAINT_FOREIGN)
- ereport(ERROR,
- (errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("constraint \"%s\" of relation \"%s\" is not a foreign key constraint",
- cmdcon->conname, RelationGetRelationName(rel))));
+ {
+ if (cmdcon->alterEnforceability)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot alter enforceability of constraint \"%s\" of relation \"%s\"",
+ cmdcon->conname, RelationGetRelationName(rel)),
+ errdetail("Enforceability can only be altered for foreign key constraints.")));
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("constraint \"%s\" of relation \"%s\" is not a foreign key constraint",
+ cmdcon->conname, RelationGetRelationName(rel))));
+ }
/*
* If it's not the topmost constraint, raise an error.
@@ -11904,13 +11964,13 @@ ATExecAlterConstraint(Relation rel, AlterConstraintStmt *cmdcon, bool recurse,
* processed regardless, in case they had the constraint locally changed.
*/
address = InvalidObjectAddress;
- if (currcon->condeferrable != cmdcon->deferrable ||
- currcon->condeferred != cmdcon->initdeferred ||
+ if (cmdcon->alterDeferrability || cmdcon->alterEnforceability ||
rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
if (ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, currcon->conrelid,
currcon->confrelid, contuple, &otherrelids,
- lockmode))
+ lockmode, InvalidOid, InvalidOid, InvalidOid,
+ InvalidOid))
ObjectAddressSet(address, ConstraintRelationId, currcon->oid);
}
@@ -11945,7 +12005,10 @@ static bool
ATExecAlterConstrRecurse(AlterConstraintStmt *cmdcon, Relation conrel,
Relation tgrel, const Oid fkrelid, const Oid pkrelid,
HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode)
+ LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger)
{
Form_pg_constraint currcon;
Oid conoid;
@@ -11969,15 +12032,36 @@ ATExecAlterConstrRecurse(AlterConstraintStmt *cmdcon, Relation conrel,
* silently do nothing.
*/
if (currcon->condeferrable != cmdcon->deferrable ||
- currcon->condeferred != cmdcon->initdeferred)
+ currcon->condeferred != cmdcon->initdeferred ||
+ currcon->conenforced != cmdcon->is_enforced)
{
HeapTuple copyTuple;
Form_pg_constraint copy_con;
copyTuple = heap_copytuple(contuple);
copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
- copy_con->condeferrable = cmdcon->deferrable;
- copy_con->condeferred = cmdcon->initdeferred;
+
+ if (cmdcon->alterDeferrability)
+ {
+ copy_con->condeferrable = cmdcon->deferrable;
+ copy_con->condeferred = cmdcon->initdeferred;
+ }
+
+ if (cmdcon->alterEnforceability)
+ {
+ copy_con->conenforced = cmdcon->is_enforced;
+
+ /*
+ * NB: The convalidated status is irrelevant when the constraint
+ * is set to NOT ENFORCED, but for consistency, it should be set
+ * accordingly. This ensures that if the constraint is later
+ * changed to ENFORCED, it will automatically be in the correct
+ * NOT VALID state.
+ */
+ if (!cmdcon->is_enforced)
+ copy_con->convalidated = false;
+ }
+
CatalogTupleUpdate(conrel, ©Tuple->t_self, copyTuple);
InvokeObjectPostAlterHook(ConstraintRelationId,
@@ -11988,32 +12072,171 @@ ATExecAlterConstrRecurse(AlterConstraintStmt *cmdcon, Relation conrel,
/* Make new constraint flags visible to others */
CacheInvalidateRelcache(rel);
+ }
+ /*
+ * Note that even if deferrability is requested to be altered along with
+ * enforceability, we don't need to call AlterConstrTriggerDeferrability()
+ * explicitly. Changing enforceability involves either creating or
+ * dropping the trigger, during which the deferrability will be set
+ * accordingly.
+ */
+ if (cmdcon->alterEnforceability &&
+ currcon->conenforced != cmdcon->is_enforced)
+ {
+ ATExecAlterConstrEnforceability(cmdcon, conrel, tgrel, fkrelid, pkrelid,
+ contuple, otherrelids, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
+ }
+ else
+ {
/*
* Now we need to update the multiple entries in pg_trigger that
* implement the constraint.
*/
- AlterConstrTriggerDeferrability(conoid, tgrel, rel, cmdcon->deferrable,
- cmdcon->initdeferred, otherrelids);
+ if (cmdcon->alterDeferrability &&
+ (currcon->condeferrable != cmdcon->deferrable ||
+ currcon->condeferred != cmdcon->initdeferred))
+ AlterConstrTriggerDeferrability(conoid, tgrel,
+ RelationGetRelid(rel),
+ cmdcon->deferrable,
+ cmdcon->initdeferred,
+ otherrelids);
+
+ /*
+ * If the table at either end of the constraint is partitioned, we
+ * need to recurse and handle every constraint that is a child of this
+ * one.
+ *
+ * (This assumes that the recurse flag is forcibly set for partitioned
+ * tables, and not set for legacy inheritance, though we don't check
+ * for that here.)
+ */
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)
+ ATExecAlterChildConstr(cmdcon, conrel, tgrel, fkrelid, pkrelid,
+ contuple, otherrelids, lockmode);
}
- /*
- * If the table at either end of the constraint is partitioned, we need to
- * recurse and handle every constraint that is a child of this one.
- *
- * (This assumes that the recurse flag is forcibly set for partitioned
- * tables, and not set for legacy inheritance, though we don't check for
- * that here.)
- */
- if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
- get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)
- ATExecAlterChildConstr(cmdcon, conrel, tgrel, fkrelid, pkrelid,
- contuple, otherrelids, lockmode);
table_close(rel, NoLock);
return changed;
}
+/*
+ * A subroutine of ATExecAlterConstrRecurse that updates the enforceability of
+ * a foreign key constraint. Depending on whether the constraint is being set
+ * to enforced or not enforced, it creates or drops the trigger accordingly.
+ *
+ * The arguments to this function have the same meaning as the arguments to
+ * ATExecAlterConstrRecurse.
+ */
+static void
+ATExecAlterConstrEnforceability(AlterConstraintStmt *cmdcon, Relation conrel,
+ Relation tgrel, const Oid fkrelid,
+ const Oid pkrelid, HeapTuple contuple,
+ List **otherrelids, LOCKMODE lockmode,
+ Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger)
+{
+ Form_pg_constraint currcon;
+ Oid conoid;
+
+ currcon = (Form_pg_constraint) GETSTRUCT(contuple);
+ conoid = currcon->oid;
+
+ /* Drop triggers */
+ if (!cmdcon->is_enforced)
+ {
+ /*
+ * When setting a constraint to NOT ENFORCED, the constraint triggers
+ * need to be dropped. Therefore, we must process the child relations
+ * first, followed by the parent, to account for dependencies.
+ */
+ if (get_rel_relkind(currcon->conrelid) == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE)
+ ATExecAlterChildConstr(cmdcon, conrel, tgrel, fkrelid, pkrelid,
+ contuple, otherrelids, lockmode);
+
+ /* Drop all the triggers */
+ DropForeignKeyConstraintTriggers(tgrel, conoid, InvalidOid, InvalidOid);
+ }
+ else /* Create triggers */
+ {
+ Oid ReferencedDelTriggerOid = InvalidOid,
+ ReferencedUpdTriggerOid = InvalidOid,
+ ReferencingInsTriggerOid = InvalidOid,
+ ReferencingUpdTriggerOid = InvalidOid;
+
+ /* Prepare the minimal information required for trigger creation. */
+ Constraint *fkconstraint = makeNode(Constraint);
+
+ fkconstraint->fk_matchtype = currcon->confmatchtype;
+ fkconstraint->fk_upd_action = currcon->confupdtype;
+ fkconstraint->fk_del_action = currcon->confdeltype;
+
+ /* Create referenced triggers */
+ if (currcon->conrelid == fkrelid)
+ createForeignKeyActionTriggers(currcon->conrelid,
+ currcon->confrelid,
+ fkconstraint,
+ conoid,
+ currcon->conindid,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ &ReferencedDelTriggerOid,
+ &ReferencedUpdTriggerOid);
+
+ /* Create referencing triggers */
+ if (currcon->confrelid == pkrelid)
+ createForeignKeyCheckTriggers(currcon->conrelid,
+ pkrelid,
+ fkconstraint,
+ conoid,
+ currcon->conindid,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger,
+ &ReferencingInsTriggerOid,
+ &ReferencingUpdTriggerOid);
+
+ /*
+ * If the table at either end of the constraint is partitioned, we
+ * need to recurse and create triggers for each constraint that is a
+ * child of this one.
+ */
+ if (get_rel_relkind(currcon->conrelid) == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE)
+ {
+ ScanKeyData pkey;
+ SysScanDesc pscan;
+ HeapTuple childtup;
+
+ ScanKeyInit(&pkey,
+ Anum_pg_constraint_conparentid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+
+ pscan = systable_beginscan(conrel, ConstraintParentIndexId,
+ true, NULL, 1, &pkey);
+
+ while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
+ ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, fkrelid, pkrelid,
+ childtup, otherrelids, lockmode,
+ ReferencedDelTriggerOid,
+ ReferencedUpdTriggerOid,
+ ReferencingInsTriggerOid,
+ ReferencingUpdTriggerOid);
+
+ systable_endscan(pscan);
+ }
+ }
+}
+
/*
* A subroutine of ATExecAlterConstrRecurse that updated constraint trigger's
* deferrability.
@@ -12022,7 +12245,7 @@ ATExecAlterConstrRecurse(AlterConstraintStmt *cmdcon, Relation conrel,
* ATExecAlterConstrRecurse.
*/
static void
-AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
+AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Oid fkrel,
bool deferrable, bool initdeferred,
List **otherrelids)
{
@@ -12048,7 +12271,7 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
* other rels that don't have a trigger whose properties change, but
* let's be conservative.)
*/
- if (tgform->tgrelid != RelationGetRelid(rel))
+ if (tgform->tgrelid != fkrel)
*otherrelids = list_append_unique_oid(*otherrelids,
tgform->tgrelid);
@@ -12111,7 +12334,8 @@ ATExecAlterChildConstr(AlterConstraintStmt *cmdcon, Relation conrel,
while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, fkrelid, pkrelid,
- childtup, otherrelids, lockmode);
+ childtup, otherrelids, lockmode, InvalidOid,
+ InvalidOid, InvalidOid, InvalidOid);
systable_endscan(pscan);
}
@@ -20169,8 +20393,6 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
Form_pg_constraint conform;
- Oid insertTriggerOid,
- updateTriggerOid;
contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
if (!HeapTupleIsValid(contup))
@@ -20197,17 +20419,25 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
/*
* Also, look up the partition's "check" triggers corresponding to the
- * constraint being detached and detach them from the parent triggers.
+ * enforced constraint being detached and detach them from the parent
+ * triggers. Non-enforced constraints do not have these triggers;
+ * therefore, this step is not needed.
*/
- GetForeignKeyCheckTriggers(trigrel,
- fk->conoid, fk->confrelid, fk->conrelid,
- &insertTriggerOid, &updateTriggerOid);
- Assert(OidIsValid(insertTriggerOid));
- TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
- RelationGetRelid(partRel));
- Assert(OidIsValid(updateTriggerOid));
- TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
- RelationGetRelid(partRel));
+ if (fk->conenforced)
+ {
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ Assert(OidIsValid(updateTriggerOid));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ }
/*
* Lastly, create the action triggers on the referenced table, using
@@ -20247,8 +20477,9 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
fkconstraint->conname = pstrdup(NameStr(conform->conname));
fkconstraint->deferrable = conform->condeferrable;
fkconstraint->initdeferred = conform->condeferred;
+ fkconstraint->is_enforced = conform->conenforced;
fkconstraint->skip_validation = true;
- fkconstraint->initially_valid = true;
+ fkconstraint->initially_valid = conform->convalidated;
/* a few irrelevant fields omitted here */
fkconstraint->pktable = NULL;
fkconstraint->fk_attrs = NIL;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index e845e888b06..4b465884e94 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -2665,7 +2665,12 @@ alter_table_cmd:
processCASbits($4, @4, "ALTER CONSTRAINT statement",
&c->deferrable,
&c->initdeferred,
- NULL, NULL, NULL, yyscanner);
+ &c->is_enforced, NULL, NULL, yyscanner);
+ c->alterDeferrability =
+ ($4 & (CAS_NOT_DEFERRABLE | CAS_DEFERRABLE |
+ CAS_INITIALLY_IMMEDIATE | CAS_INITIALLY_IMMEDIATE)) != 0;
+ c->alterEnforceability =
+ ($4 & (CAS_NOT_ENFORCED | CAS_ENFORCED)) != 0;
$$ = (Node *) n;
}
/* ALTER TABLE <name> VALIDATE CONSTRAINT ... */
@@ -4034,6 +4039,7 @@ ColConstraintElem:
n->is_enforced = true;
n->skip_validation = false;
n->initially_valid = true;
+ n->is_enforced = true;
$$ = (Node *) n;
}
;
@@ -4308,8 +4314,8 @@ ConstraintElem:
n->fk_del_set_cols = ($11)->deleteAction->cols;
processCASbits($12, @12, "FOREIGN KEY",
&n->deferrable, &n->initdeferred,
- NULL, &n->skip_validation, NULL,
- yyscanner);
+ &n->is_enforced, &n->skip_validation,
+ NULL, yyscanner);
n->initially_valid = !n->skip_validation;
$$ = (Node *) n;
}
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index ca028d2a66d..170f8032d59 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -2961,8 +2961,10 @@ transformFKConstraints(CreateStmtContext *cxt,
/*
* If CREATE TABLE or adding a column with NULL default, we can safely
- * skip validation of FK constraints, and nonetheless mark them valid.
- * (This will override any user-supplied NOT VALID flag.)
+ * skip validation of FK constraints, and mark them as valid based on the
+ * constraint enforcement flag, since NOT ENFORCED constraints must always
+ * be marked as NOT VALID. (This will override any user-supplied NOT VALID
+ * flag.)
*/
if (skipValidation)
{
@@ -2971,7 +2973,7 @@ transformFKConstraints(CreateStmtContext *cxt,
Constraint *constraint = (Constraint *) lfirst(fkclist);
constraint->skip_validation = true;
- constraint->initially_valid = true;
+ constraint->initially_valid = constraint->is_enforced;
}
}
@@ -3966,7 +3968,8 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
case CONSTR_ATTR_ENFORCED:
if (lastprimarycon == NULL ||
- lastprimarycon->contype != CONSTR_CHECK)
+ (lastprimarycon->contype != CONSTR_CHECK &&
+ lastprimarycon->contype != CONSTR_FOREIGN))
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("misplaced ENFORCED clause"),
@@ -3982,7 +3985,8 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
case CONSTR_ATTR_NOT_ENFORCED:
if (lastprimarycon == NULL ||
- lastprimarycon->contype != CONSTR_CHECK)
+ (lastprimarycon->contype != CONSTR_CHECK &&
+ lastprimarycon->contype != CONSTR_FOREIGN))
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("misplaced NOT ENFORCED clause"),
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 485cd2178bc..cb3c19c7448 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4689,6 +4689,7 @@ RelationGetFKeyList(Relation relation)
info->conoid = constraint->oid;
info->conrelid = constraint->conrelid;
info->confrelid = constraint->confrelid;
+ info->conenforced = constraint->conenforced;
DeconstructFkConstraintRow(htup, &info->nkeys,
info->conkey,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 4ae9906a674..e73ed1b6789 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2511,10 +2511,16 @@ typedef struct AlterConstraintStmt
{
NodeTag type;
- char *conname; /* Constraint name */
+ char *conname; /* Constraint name */
- bool deferrable; /* DEFERRABLE? */
- bool initdeferred; /* INITIALLY DEFERRED? */
+ bool alterDeferrability; /* True when any of the following flags
+ * are specifically set for modification */
+ bool deferrable; /* DEFERRABLE? */
+ bool initdeferred; /* INITIALLY DEFERRED? */
+
+ bool alterEnforceability; /* True when following flag is
+ * specifically set for modification */
+ bool is_enforced; /* enforced constraint? */
} AlterConstraintStmt;
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 33d1e4a4e2e..c707ebec7b6 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -284,6 +284,9 @@ typedef struct ForeignKeyCacheInfo
/* number of columns in the foreign key */
int nkeys;
+ /* Is enforced ? */
+ bool conenforced;
+
/*
* these arrays each have nkeys valid entries:
*/
diff --git a/src/test/regress/expected/constraints.out b/src/test/regress/expected/constraints.out
index 692a69fe457..75d40d06b6e 100644
--- a/src/test/regress/expected/constraints.out
+++ b/src/test/regress/expected/constraints.out
@@ -744,15 +744,12 @@ CREATE TABLE UNIQUE_NOTEN_TBL(i int UNIQUE NOT ENFORCED);
ERROR: misplaced NOT ENFORCED clause
LINE 1: CREATE TABLE UNIQUE_NOTEN_TBL(i int UNIQUE NOT ENFORCED);
^
--- XXX: error message is misleading here
ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
-ERROR: ALTER CONSTRAINT statement constraints cannot be marked ENFORCED
-LINE 1: ...TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
- ^
+ERROR: cannot alter enforceability of constraint "unique_tbl_i_key" of relation "unique_tbl"
+DETAIL: Enforceability can only be altered for foreign key constraints.
ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORCED;
-ERROR: ALTER CONSTRAINT statement constraints cannot be marked NOT ENFORCED
-LINE 1: ...ABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORC...
- ^
+ERROR: cannot alter enforceability of constraint "unique_tbl_i_key" of relation "unique_tbl"
+DETAIL: Enforceability can only be altered for foreign key constraints.
DROP TABLE unique_tbl;
--
-- EXCLUDE constraints
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 374dcb266e7..cd8c334bd38 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -1,12 +1,44 @@
--
-- FOREIGN KEY
--
--- MATCH FULL
+-- NOT ENFORCED
--
-- First test, check and cascade
--
CREATE TABLE PKTABLE ( ptest1 int PRIMARY KEY, ptest2 text );
-CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE, ftest2 int );
+CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE NOT ENFORCED,
+ ftest2 int );
+-- Inserting into the foreign key table will not result in an error, even if
+-- there is no matching key in the referenced table.
+INSERT INTO FKTABLE VALUES (1, 2);
+INSERT INTO FKTABLE VALUES (2, 3);
+-- Check FKTABLE
+SELECT * FROM FKTABLE;
+ ftest1 | ftest2
+--------+--------
+ 1 | 2
+ 2 | 3
+(2 rows)
+
+-- Changing it back to ENFORCED will leave the constraint in the NOT VALID state
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+-- Which needs to be explicitly validated.
+ALTER TABLE FKTABLE VALIDATE CONSTRAINT fktable_ftest1_fkey;
+ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_fkey"
+DETAIL: Key (ftest1)=(1) is not present in table "pktable".
+-- Remove any existing rows that violate the constraint, then attempt to change
+-- it.
+TRUNCATE FKTABLE;
+ALTER TABLE FKTABLE VALIDATE CONSTRAINT fktable_ftest1_fkey;
+-- Any further inserts will fail due to the enforcement.
+INSERT INTO FKTABLE VALUES (3, 4);
+ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_fkey"
+DETAIL: Key (ftest1)=(3) is not present in table "pktable".
+--
+-- MATCH FULL
+--
+-- First test, check and cascade
+--
-- Insert test data into PKTABLE
INSERT INTO PKTABLE VALUES (1, 'Test1');
INSERT INTO PKTABLE VALUES (2, 'Test2');
@@ -351,6 +383,42 @@ INSERT INTO FKTABLE VALUES (1, NULL);
ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL;
ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_ftest2_fkey"
DETAIL: MATCH FULL does not allow mixing of null and nonnull key values.
+-- NOT ENFORCED will bypass the initial check
+ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL NOT ENFORCED;
+-- Modifying other attributes of a constraint should not affect its enforceability, and vice versa
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_ftest2_fkey DEFERRABLE INITIALLY DEFERRED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fktable_ftest1_ftest2_fkey';
+ condeferrable | condeferred | conenforced | convalidated
+---------------+-------------+-------------+--------------
+ t | t | f | f
+(1 row)
+
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_ftest2_fkey NOT ENFORCED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fktable_ftest1_ftest2_fkey';
+ condeferrable | condeferred | conenforced | convalidated
+---------------+-------------+-------------+--------------
+ t | t | f | f
+(1 row)
+
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_ftest2_fkey ENFORCED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fktable_ftest1_ftest2_fkey';
+ condeferrable | condeferred | conenforced | convalidated
+---------------+-------------+-------------+--------------
+ t | t | t | f
+(1 row)
+
+-- Can change enforceability and deferrability together
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_ftest2_fkey ENFORCED NOT DEFERRABLE;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fktable_ftest1_ftest2_fkey';
+ condeferrable | condeferred | conenforced | convalidated
+---------------+-------------+-------------+--------------
+ f | f | t | f
+(1 row)
+
DROP TABLE FKTABLE;
DROP TABLE PKTABLE;
-- MATCH SIMPLE
@@ -1276,6 +1344,13 @@ INSERT INTO fktable VALUES (0, 20);
ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_fk_fkey"
DETAIL: Key (fk)=(20) is not present in table "pktable".
COMMIT;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT ENFORCED;
+BEGIN;
+-- doesn't match FK, no error.
+UPDATE pktable SET id = 10 WHERE id = 5;
+-- doesn't match PK, no error.
+INSERT INTO fktable VALUES (0, 20);
+ROLLBACK;
-- try additional syntax
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE;
-- illegal option
@@ -1283,6 +1358,14 @@ ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE INITIALLY DE
ERROR: constraint declared INITIALLY DEFERRED must be DEFERRABLE
LINE 1: ...e ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE INITIALLY ...
^
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORCED;
+ERROR: conflicting constraint properties
+LINE 1: ...fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORC...
+ ^
+CREATE TEMP TABLE fktable2 (fk int references pktable ENFORCED NOT ENFORCED);
+ERROR: multiple ENFORCED/NOT ENFORCED clauses not allowed
+LINE 1: ...ABLE fktable2 (fk int references pktable ENFORCED NOT ENFORC...
+ ^
-- test order of firing of FK triggers when several RI-induced changes need to
-- be made to the same row. This was broken by subtransaction-related
-- changes in 8.0.
@@ -1580,10 +1663,14 @@ ALTER TABLE fk_partitioned_fk DROP COLUMN fdrop1;
CREATE TABLE fk_partitioned_fk_1 (fdrop1 int, fdrop2 int, a int, fdrop3 int, b int);
ALTER TABLE fk_partitioned_fk_1 DROP COLUMN fdrop1, DROP COLUMN fdrop2, DROP COLUMN fdrop3;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FROM (0,0) TO (1000,1000);
-ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk;
+ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
+ALTER TABLE fk_partitioned_fk_2 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
+-- Changing to ENFORCED does not alter the validation status; it must be explicitly validated.
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+ALTER TABLE fk_partitioned_fk VALIDATE CONSTRAINT fk_partitioned_fk_a_b_fkey ;
CREATE TABLE fk_partitioned_fk_3 (fdrop1 int, fdrop2 int, fdrop3 int, fdrop4 int, b int, a int)
PARTITION BY HASH (a);
ALTER TABLE fk_partitioned_fk_3 DROP COLUMN fdrop1, DROP COLUMN fdrop2,
@@ -1606,10 +1693,10 @@ INSERT INTO fk_partitioned_fk_1 (a,b) VALUES (500, 501);
ERROR: insert or update on table "fk_partitioned_fk_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
DETAIL: Key (a, b)=(500, 501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (1500, 1501);
-ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_2_a_b_fkey"
DETAIL: Key (a, b)=(1500, 1501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk_2 (a,b) VALUES (1500, 1501);
-ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_2_a_b_fkey"
DETAIL: Key (a, b)=(1500, 1501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (2500, 2502);
ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
@@ -1659,6 +1746,37 @@ Indexes:
Referenced by:
TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b)
+-- Check the exsting FK trigger
+CREATE TEMP TABLE tmp_trg_info AS SELECT tgtype, tgrelid::regclass, tgconstraint
+FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+SELECT count(1) FROM tmp_trg_info;
+ count
+-------
+ 14
+(1 row)
+
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+-- No triggers
+SELECT count(1) FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+ count
+-------
+ 0
+(1 row)
+
+-- Changing it back to ENFORCED will recreate the necessary triggers.
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+-- Should be exactly the same number of triggers found as before
+SELECT COUNT(1) FROM pg_trigger pgt JOIN tmp_trg_info tt
+ON (pgt.tgtype = tt.tgtype AND pgt.tgrelid = tt.tgrelid AND pgt.tgconstraint = tt.tgconstraint);
+ count
+-------
+ 14
+(1 row)
+
ALTER TABLE fk_partitioned_fk DROP CONSTRAINT fk_partitioned_fk_a_b_fkey;
-- done.
DROP TABLE fk_notpartitioned_pk, fk_partitioned_fk;
@@ -1956,6 +2074,40 @@ Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
Foreign-key constraints:
TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
+DROP TABLE fk_partitioned_fk_2;
+CREATE TABLE fk_partitioned_fk_2 (b int, a int,
+ FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
+BEGIN;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should be having two constraints
+\d fk_partitioned_fk_2
+ Table "public.fk_partitioned_fk_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ b | integer | | |
+ a | integer | | |
+Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
+Foreign-key constraints:
+ "fk_partitioned_fk_2_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED
+ TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
+
+DROP TABLE fk_partitioned_fk_2;
+ROLLBACK;
+BEGIN;
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have only one constraint
+\d fk_partitioned_fk_2
+ Table "public.fk_partitioned_fk_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ b | integer | | |
+ a | integer | | |
+Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
+Foreign-key constraints:
+ TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED
+
+ROLLBACK;
DROP TABLE fk_partitioned_fk_2;
CREATE TABLE fk_partitioned_fk_4 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE) PARTITION BY RANGE (b, a);
CREATE TABLE fk_partitioned_fk_4_1 PARTITION OF fk_partitioned_fk_4 FOR VALUES FROM (1,1) TO (100,100);
diff --git a/src/test/regress/sql/constraints.sql b/src/test/regress/sql/constraints.sql
index d6742f83fb9..e61e7324768 100644
--- a/src/test/regress/sql/constraints.sql
+++ b/src/test/regress/sql/constraints.sql
@@ -534,7 +534,6 @@ SELECT * FROM unique_tbl;
-- enforcibility cannot be specified or set for unique constrain
CREATE TABLE UNIQUE_EN_TBL(i int UNIQUE ENFORCED);
CREATE TABLE UNIQUE_NOTEN_TBL(i int UNIQUE NOT ENFORCED);
--- XXX: error message is misleading here
ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORCED;
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index bc0adb8cfe9..e59fa8aa8c7 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -2,13 +2,40 @@
-- FOREIGN KEY
--
--- MATCH FULL
+-- NOT ENFORCED
--
-- First test, check and cascade
--
CREATE TABLE PKTABLE ( ptest1 int PRIMARY KEY, ptest2 text );
-CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE, ftest2 int );
+CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE NOT ENFORCED,
+ ftest2 int );
+-- Inserting into the foreign key table will not result in an error, even if
+-- there is no matching key in the referenced table.
+INSERT INTO FKTABLE VALUES (1, 2);
+INSERT INTO FKTABLE VALUES (2, 3);
+
+-- Check FKTABLE
+SELECT * FROM FKTABLE;
+
+-- Changing it back to ENFORCED will leave the constraint in the NOT VALID state
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+-- Which needs to be explicitly validated.
+ALTER TABLE FKTABLE VALIDATE CONSTRAINT fktable_ftest1_fkey;
+
+-- Remove any existing rows that violate the constraint, then attempt to change
+-- it.
+TRUNCATE FKTABLE;
+ALTER TABLE FKTABLE VALIDATE CONSTRAINT fktable_ftest1_fkey;
+
+-- Any further inserts will fail due to the enforcement.
+INSERT INTO FKTABLE VALUES (3, 4);
+
+--
+-- MATCH FULL
+--
+-- First test, check and cascade
+--
-- Insert test data into PKTABLE
INSERT INTO PKTABLE VALUES (1, 'Test1');
INSERT INTO PKTABLE VALUES (2, 'Test2');
@@ -230,6 +257,27 @@ INSERT INTO FKTABLE VALUES (1, NULL);
ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL;
+-- NOT ENFORCED will bypass the initial check
+ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL NOT ENFORCED;
+
+-- Modifying other attributes of a constraint should not affect its enforceability, and vice versa
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_ftest2_fkey DEFERRABLE INITIALLY DEFERRED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fktable_ftest1_ftest2_fkey';
+
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_ftest2_fkey NOT ENFORCED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fktable_ftest1_ftest2_fkey';
+
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_ftest2_fkey ENFORCED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fktable_ftest1_ftest2_fkey';
+
+-- Can change enforceability and deferrability together
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_ftest2_fkey ENFORCED NOT DEFERRABLE;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fktable_ftest1_ftest2_fkey';
+
DROP TABLE FKTABLE;
DROP TABLE PKTABLE;
@@ -968,10 +1016,23 @@ INSERT INTO fktable VALUES (0, 20);
COMMIT;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT ENFORCED;
+
+BEGIN;
+
+-- doesn't match FK, no error.
+UPDATE pktable SET id = 10 WHERE id = 5;
+-- doesn't match PK, no error.
+INSERT INTO fktable VALUES (0, 20);
+
+ROLLBACK;
+
-- try additional syntax
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE;
-- illegal option
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORCED;
+CREATE TEMP TABLE fktable2 (fk int references pktable ENFORCED NOT ENFORCED);
-- test order of firing of FK triggers when several RI-induced changes need to
-- be made to the same row. This was broken by subtransaction-related
@@ -1182,11 +1243,16 @@ ALTER TABLE fk_partitioned_fk DROP COLUMN fdrop1;
CREATE TABLE fk_partitioned_fk_1 (fdrop1 int, fdrop2 int, a int, fdrop3 int, b int);
ALTER TABLE fk_partitioned_fk_1 DROP COLUMN fdrop1, DROP COLUMN fdrop2, DROP COLUMN fdrop3;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FROM (0,0) TO (1000,1000);
-ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk;
+ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
+ALTER TABLE fk_partitioned_fk_2 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
+-- Changing to ENFORCED does not alter the validation status; it must be explicitly validated.
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+ALTER TABLE fk_partitioned_fk VALIDATE CONSTRAINT fk_partitioned_fk_a_b_fkey ;
+
CREATE TABLE fk_partitioned_fk_3 (fdrop1 int, fdrop2 int, fdrop3 int, fdrop4 int, b int, a int)
PARTITION BY HASH (a);
ALTER TABLE fk_partitioned_fk_3 DROP COLUMN fdrop1, DROP COLUMN fdrop2,
@@ -1232,6 +1298,27 @@ UPDATE fk_notpartitioned_pk SET b = 1502 WHERE a = 1500;
UPDATE fk_notpartitioned_pk SET b = 2504 WHERE a = 2500;
-- check psql behavior
\d fk_notpartitioned_pk
+
+-- Check the exsting FK trigger
+CREATE TEMP TABLE tmp_trg_info AS SELECT tgtype, tgrelid::regclass, tgconstraint
+FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+SELECT count(1) FROM tmp_trg_info;
+
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+-- No triggers
+SELECT count(1) FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+
+-- Changing it back to ENFORCED will recreate the necessary triggers.
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+
+-- Should be exactly the same number of triggers found as before
+SELECT COUNT(1) FROM pg_trigger pgt JOIN tmp_trg_info tt
+ON (pgt.tgtype = tt.tgtype AND pgt.tgrelid = tt.tgrelid AND pgt.tgconstraint = tt.tgconstraint);
+
ALTER TABLE fk_partitioned_fk DROP CONSTRAINT fk_partitioned_fk_a_b_fkey;
-- done.
DROP TABLE fk_notpartitioned_pk, fk_partitioned_fk;
@@ -1439,6 +1526,22 @@ ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN
\d fk_partitioned_fk_2
DROP TABLE fk_partitioned_fk_2;
+CREATE TABLE fk_partitioned_fk_2 (b int, a int,
+ FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
+BEGIN;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should be having two constraints
+\d fk_partitioned_fk_2
+DROP TABLE fk_partitioned_fk_2;
+ROLLBACK;
+BEGIN;
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have only one constraint
+\d fk_partitioned_fk_2
+ROLLBACK;
+DROP TABLE fk_partitioned_fk_2;
+
CREATE TABLE fk_partitioned_fk_4 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE) PARTITION BY RANGE (b, a);
CREATE TABLE fk_partitioned_fk_4_1 PARTITION OF fk_partitioned_fk_4 FOR VALUES FROM (1,1) TO (100,100);
CREATE TABLE fk_partitioned_fk_4_2 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE SET NULL);
--
2.43.5
v12-0007-Merge-the-parent-and-child-constraints-with-diff.patchapplication/octet-stream; name=v12-0007-Merge-the-parent-and-child-constraints-with-diff.patchDownload
From 92a20b4dd0fedb40fd337c4ae3af6aa86fecc52f Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Tue, 4 Feb 2025 13:49:18 +0530
Subject: [PATCH v12 7/7] Merge the parent and child constraints with differing
enforcibility.
If an enforced parent constraint is attached to a non-enforced child
constraint, the child constraint will be made enforced, with
validation applied if the parent constraint is validated as well.
Otherwise, a new enforced constraint (with validation, if the parent
constraint is validated) would need to be created on the child table,
which would be unnecessary if a similar constraint already exists and
can be attached.
On the other hand, having a not-enforced parent constraint with an
enforced child constraint does not cause any issues, and no changes
are required.
----
NOTE: This patch is intended to reduce the diff noise from the main
patch and is not meant to be committed separately. It should be
squashed with the main patch that adds ENFORCED/NOT ENFORCED.
----
---
src/backend/commands/tablecmds.c | 158 ++++++++++++++++++++--
src/test/regress/expected/foreign_key.out | 38 +++++-
src/test/regress/sql/foreign_key.sql | 16 ++-
3 files changed, 192 insertions(+), 20 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d3930f8e4f3..73cefbb66ea 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -11206,8 +11206,8 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
Oid indexOid;
ObjectAddress address;
ListCell *lc;
- Oid insertTriggerOid,
- updateTriggerOid;
+ Oid insertTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
bool with_period;
tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(parentConstrOid));
@@ -11421,7 +11421,6 @@ tryAttachPartitionForeignKey(List **wqueue,
if (OidIsValid(partConstr->conparentid) ||
partConstr->condeferrable != parentConstr->condeferrable ||
partConstr->condeferred != parentConstr->condeferred ||
- partConstr->conenforced != parentConstr->conenforced ||
partConstr->confupdtype != parentConstr->confupdtype ||
partConstr->confdeltype != parentConstr->confdeltype ||
partConstr->confmatchtype != parentConstr->confmatchtype)
@@ -11483,9 +11482,79 @@ AttachPartitionForeignKey(List **wqueue,
if (!HeapTupleIsValid(partcontup))
elog(ERROR, "cache lookup failed for constraint %u", partConstrOid);
partConstr = (Form_pg_constraint) GETSTRUCT(partcontup);
+ partConstrIsEnforced = partConstr->conenforced;
partConstrFrelid = partConstr->confrelid;
partConstrRelid = partConstr->conrelid;
+ /*
+ * When the enforcibility of both constraints is the same, no special
+ * action is taken, and the flow remains unchanged. However, there are two
+ * scenarios to consider when it differs:
+ *
+ * 1. Parent constraint is not-enforced and child constraint is enforced:
+ *
+ * This is allowed because the not-enforced parent constraint does not
+ * have triggers, so no redundancy issues arise with the child constraint,
+ * even if it is enforced. In this case, we allow the child constraint to
+ * remain enforced and keep its trigger intact. This ensures that
+ * referential integrity checks for the child continue as before, even
+ * with the parent constraint not enforced. The relationship between the
+ * two constraints is maintained by setting the parent constraint, which
+ * enables us to locate the child constraint. This is important if the
+ * parent constraint is later changed to enforced, at which point the
+ * necessary trigger will be created for the parent. Any potential
+ * redundancy from these triggers will be addressed accordingly.
+ *
+ * 2. Parent constraint is enforced and child constraint is not-enforced:
+ *
+ * This is not allowed, as it would violate referential integrity. In such
+ * cases, the child constraint will first be made enforced before merging
+ * it with the enforced parent constraint. Then, action triggers and
+ * constraint redundancy will be handled in the usual manner, similar to
+ * how two enforced constraints are merged.
+ */
+ if (!parentConstrIsEnforced && partConstrIsEnforced)
+ {
+ ReleaseSysCache(partcontup);
+ ReleaseSysCache(parentConstrTup);
+
+ ConstraintSetParentConstraint(partConstrOid, parentConstrOid,
+ RelationGetRelid(partition));
+ CommandCounterIncrement();
+
+ return;
+ }
+ else if (parentConstrIsEnforced && !partConstrIsEnforced)
+ {
+ AlterConstraintStmt *cmdcon = makeNode(AlterConstraintStmt);
+ Relation conrel;
+ List *otherrelids = NIL;
+
+ cmdcon->conname = NameStr(partConstr->conname);
+ cmdcon->deferrable = partConstr->condeferrable;
+ cmdcon->initdeferred = partConstr->condeferred;
+ cmdcon->alterEnforceability = true;
+ cmdcon->is_enforced = true;
+
+ conrel = table_open(ConstraintRelationId, RowExclusiveLock);
+
+ ATExecAlterConstrRecurse(cmdcon, conrel, trigrel, partConstr->conrelid,
+ partConstr->confrelid, partcontup, &otherrelids,
+ AccessExclusiveLock, InvalidOid, InvalidOid,
+ InvalidOid, InvalidOid);
+
+ table_close(conrel, RowExclusiveLock);
+
+ CommandCounterIncrement();
+ }
+
+ /*
+ * The constraint parent shouldn't be set beforehand, or if it's already
+ * set, it should be the specified parent.
+ */
+ partConstrParentIsSet = OidIsValid(partConstr->conparentid);
+ Assert(!partConstrParentIsSet || partConstr->conparentid == parentConstrOid);
+
/*
* Will we need to validate this constraint? A valid parent constraint
* implies that all child constraints have been validated, so if this one
@@ -11505,8 +11574,10 @@ AttachPartitionForeignKey(List **wqueue,
DropForeignKeyConstraintTriggers(trigrel, partConstrOid, partConstrFrelid,
partConstrRelid);
- ConstraintSetParentConstraint(partConstrOid, parentConstrOid,
- RelationGetRelid(partition));
+ /* Skip if the parent is already set */
+ if (!partConstrParentIsSet)
+ ConstraintSetParentConstraint(partConstrOid, parentConstrOid,
+ RelationGetRelid(partition));
/*
* Like the constraint, attach partition's "check" triggers to the
@@ -11555,7 +11626,7 @@ AttachPartitionForeignKey(List **wqueue,
CommandCounterIncrement();
/* If validation is needed, put it in the queue now. */
- if (queueValidation)
+ if (wqueue && queueValidation)
{
Relation conrel;
@@ -12165,6 +12236,17 @@ ATExecAlterConstrEnforceability(AlterConstraintStmt *cmdcon, Relation conrel,
/* Drop all the triggers */
DropForeignKeyConstraintTriggers(tgrel, conoid, InvalidOid, InvalidOid);
+
+ /*
+ * If the referenced table is partitioned, the child constraint we're
+ * changing to not-enforced may have additional pg_constraint rows and
+ * action triggers that remain untouched while this child constraint
+ * is attached to the not-enforced parent. These must now be removed.
+ * For more details, see AttachPartitionForeignKey().
+ */
+ if (OidIsValid(currcon->conparentid) &&
+ get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE)
+ RemoveInheritedConstraint(conrel, tgrel, currcon->conrelid, conoid);
}
else /* Create triggers */
{
@@ -12225,13 +12307,41 @@ ATExecAlterConstrEnforceability(AlterConstraintStmt *cmdcon, Relation conrel,
true, NULL, 1, &pkey);
while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
- ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, fkrelid, pkrelid,
- childtup, otherrelids, lockmode,
- ReferencedDelTriggerOid,
- ReferencedUpdTriggerOid,
- ReferencingInsTriggerOid,
- ReferencingUpdTriggerOid);
+ {
+ Form_pg_constraint childcon;
+ childcon = (Form_pg_constraint) GETSTRUCT(childtup);
+
+ if (childcon->conenforced)
+ {
+ /*
+ * The child constraint is attached to the parent
+ * constraint, which is already enforced. Now, as the
+ * parent constraint is being modified to be enforced,
+ * some constraints and action triggers on the child table
+ * may become redundant and need to be removed.
+ */
+ if (currcon->confrelid == pkrelid)
+ {
+ Relation rel = table_open(childcon->conrelid, lockmode);
+
+ AttachPartitionForeignKey(NULL, rel, childcon->oid,
+ conoid,
+ ReferencingInsTriggerOid,
+ ReferencingUpdTriggerOid,
+ tgrel);
+
+ table_close(rel, NoLock);
+ }
+ }
+ else
+ ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, childtup, otherrelids,
+ lockmode, ReferencedDelTriggerOid,
+ ReferencedUpdTriggerOid,
+ ReferencingInsTriggerOid,
+ ReferencingUpdTriggerOid);
+ }
systable_endscan(pscan);
}
}
@@ -20392,7 +20502,9 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
{
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
+ HeapTuple parentContup;
Form_pg_constraint conform;
+ Oid parentConstrIsEnforced;
contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
if (!HeapTupleIsValid(contup))
@@ -20411,12 +20523,34 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
continue;
}
+ /* Get the enforcibility of the parent constraint */
+ parentContup = SearchSysCache1(CONSTROID,
+ ObjectIdGetDatum(conform->conparentid));
+ if (!HeapTupleIsValid(parentContup))
+ elog(ERROR, "cache lookup failed for constraint %u",
+ conform->conparentid);
+ parentConstrIsEnforced =
+ ((Form_pg_constraint) GETSTRUCT(parentContup))->conenforced;
+ ReleaseSysCache(parentContup);
+
/*
* The constraint on this table must be marked no longer a child of
* the parent's constraint, as do its check triggers.
*/
ConstraintSetParentConstraint(fk->conoid, InvalidOid, InvalidOid);
+ /*
+ * Unsetting the parent is sufficient when the parent constraint is
+ * not enforced and the child constraint is enforced, as we link them
+ * by setting the constraint parent, while leaving the rest unchanged.
+ * For more details, see AttachPartitionForeignKey().
+ */
+ if (!parentConstrIsEnforced && fk->conenforced)
+ {
+ ReleaseSysCache(contup);
+ continue;
+ }
+
/*
* Also, look up the partition's "check" triggers corresponding to the
* enforced constraint being detached and detach them from the parent
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index cd8c334bd38..08544f3faa7 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -1677,8 +1677,33 @@ ALTER TABLE fk_partitioned_fk_3 DROP COLUMN fdrop1, DROP COLUMN fdrop2,
DROP COLUMN fdrop3, DROP COLUMN fdrop4;
CREATE TABLE fk_partitioned_fk_3_0 PARTITION OF fk_partitioned_fk_3 FOR VALUES WITH (MODULUS 5, REMAINDER 0);
CREATE TABLE fk_partitioned_fk_3_1 PARTITION OF fk_partitioned_fk_3 FOR VALUES WITH (MODULUS 5, REMAINDER 1);
+-- Merge the not-enforced parent constraint with the enforced and not-enforced child constraints.
+ALTER TABLE fk_partitioned_fk_3_0 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ENFORCED;
+ALTER TABLE fk_partitioned_fk_3_1 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk_3 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+SELECT conname, conenforced, convalidated, conrelid::regclass FROM pg_constraint
+WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+ conname | conenforced | convalidated | conrelid
+--------------------------------+-------------+--------------+-----------------------
+ fk_partitioned_fk_3_0_a_b_fkey | t | t | fk_partitioned_fk_3_0
+ fk_partitioned_fk_3_1_a_b_fkey | f | f | fk_partitioned_fk_3_1
+ fk_partitioned_fk_3_a_b_fkey | f | f | fk_partitioned_fk_3
+(3 rows)
+
+-- Merging an enforced parent constraint (validated) with a not-enforced child
+-- constraint will implicitly change the child constraint to enforced and apply
+-- the validation as well.
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_3
FOR VALUES FROM (2000,2000) TO (3000,3000);
+SELECT conname, conenforced, convalidated, conrelid::regclass FROM pg_constraint
+WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+ conname | conenforced | convalidated | conrelid
+--------------------------------+-------------+--------------+-----------------------
+ fk_partitioned_fk_3_0_a_b_fkey | t | t | fk_partitioned_fk_3_0
+ fk_partitioned_fk_3_1_a_b_fkey | t | t | fk_partitioned_fk_3_1
+ fk_partitioned_fk_3_a_b_fkey | t | t | fk_partitioned_fk_3
+(3 rows)
+
-- Creating a foreign key with ONLY on a partitioned table referencing
-- a non-partitioned table fails.
ALTER TABLE ONLY fk_partitioned_fk ADD FOREIGN KEY (a, b)
@@ -1699,16 +1724,16 @@ INSERT INTO fk_partitioned_fk_2 (a,b) VALUES (1500, 1501);
ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_2_a_b_fkey"
DETAIL: Key (a, b)=(1500, 1501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (2500, 2502);
-ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_3_1_a_b_fkey"
DETAIL: Key (a, b)=(2500, 2502) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk_3 (a,b) VALUES (2500, 2502);
-ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_3_1_a_b_fkey"
DETAIL: Key (a, b)=(2500, 2502) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (2501, 2503);
-ERROR: insert or update on table "fk_partitioned_fk_3_0" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_0" violates foreign key constraint "fk_partitioned_fk_3_0_a_b_fkey"
DETAIL: Key (a, b)=(2501, 2503) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk_3 (a,b) VALUES (2501, 2503);
-ERROR: insert or update on table "fk_partitioned_fk_3_0" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_0" violates foreign key constraint "fk_partitioned_fk_3_0_a_b_fkey"
DETAIL: Key (a, b)=(2501, 2503) is not present in table "fk_notpartitioned_pk".
-- but if we insert the values that make them valid, then they work
INSERT INTO fk_notpartitioned_pk VALUES (500, 501), (1500, 1501),
@@ -1719,7 +1744,7 @@ INSERT INTO fk_partitioned_fk (a,b) VALUES (2500, 2502);
INSERT INTO fk_partitioned_fk (a,b) VALUES (2501, 2503);
-- this update fails because there is no referenced row
UPDATE fk_partitioned_fk SET a = a + 1 WHERE a = 2501;
-ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_3_1_a_b_fkey"
DETAIL: Key (a, b)=(2502, 2503) is not present in table "fk_notpartitioned_pk".
-- but we can fix it thusly:
INSERT INTO fk_notpartitioned_pk (a,b) VALUES (2502, 2503);
@@ -2079,7 +2104,7 @@ CREATE TABLE fk_partitioned_fk_2 (b int, a int,
FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
BEGIN;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
--- should be having two constraints
+-- should only have one constraint
\d fk_partitioned_fk_2
Table "public.fk_partitioned_fk_2"
Column | Type | Collation | Nullable | Default
@@ -2088,7 +2113,6 @@ ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN
a | integer | | |
Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
Foreign-key constraints:
- "fk_partitioned_fk_2_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED
TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
DROP TABLE fk_partitioned_fk_2;
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index e59fa8aa8c7..e9401ec8cdd 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -1259,9 +1259,23 @@ ALTER TABLE fk_partitioned_fk_3 DROP COLUMN fdrop1, DROP COLUMN fdrop2,
DROP COLUMN fdrop3, DROP COLUMN fdrop4;
CREATE TABLE fk_partitioned_fk_3_0 PARTITION OF fk_partitioned_fk_3 FOR VALUES WITH (MODULUS 5, REMAINDER 0);
CREATE TABLE fk_partitioned_fk_3_1 PARTITION OF fk_partitioned_fk_3 FOR VALUES WITH (MODULUS 5, REMAINDER 1);
+-- Merge the not-enforced parent constraint with the enforced and not-enforced child constraints.
+ALTER TABLE fk_partitioned_fk_3_0 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ENFORCED;
+ALTER TABLE fk_partitioned_fk_3_1 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk_3 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+
+SELECT conname, conenforced, convalidated, conrelid::regclass FROM pg_constraint
+WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+
+-- Merging an enforced parent constraint (validated) with a not-enforced child
+-- constraint will implicitly change the child constraint to enforced and apply
+-- the validation as well.
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_3
FOR VALUES FROM (2000,2000) TO (3000,3000);
+SELECT conname, conenforced, convalidated, conrelid::regclass FROM pg_constraint
+WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+
-- Creating a foreign key with ONLY on a partitioned table referencing
-- a non-partitioned table fails.
ALTER TABLE ONLY fk_partitioned_fk ADD FOREIGN KEY (a, b)
@@ -1530,7 +1544,7 @@ CREATE TABLE fk_partitioned_fk_2 (b int, a int,
FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
BEGIN;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
--- should be having two constraints
+-- should only have one constraint
\d fk_partitioned_fk_2
DROP TABLE fk_partitioned_fk_2;
ROLLBACK;
--
2.43.5
On 03.02.25 06:19, Ashutosh Bapat wrote:
Here's how I see the state conversions happening.
NOT VALID, NOT ENFORCED changed to NOT_VALID, ENFORCED - no data
validation required, constraint is enforced on the new tuples/changes
NOT VALID, ENFORCED changed to NOT VALID, NOT ENFORCED - no data
validation, constraint isn't enforced anymore
VALID, NOT ENFORCED changed to VALID, ENFORCED - data validation
required, constraint is enforced
VALID, ENFORCED changed to VALID, NOT ENFORCED - no data validation
required, constrain isn't enforced anymore, we rely on user to enforce
the constraint on their side
This looks sensible to me.
On 03.02.25 08:50, Alvaro Herrera wrote:
On 2025-Feb-03, Ashutosh Bapat wrote:
VALID, NOT ENFORCED changed to VALID, ENFORCED - data validation
required, constraint is enforcedThere's no such thing as a VALID NOT ENFORCED constraint. It just
cannot exist.
The way I interpret this is that the VALID flag is just recording what
would happen if the constraint was enforced. So you you take a [NOT]
VALID ENFORCED constraint and switch it to NOT ENFORCED and back and you
get back to where you started.
On 03.02.25 13:19, Alvaro Herrera wrote:
On 2025-Feb-03, Ashutosh Bapat wrote:
```
If the
constraint is <literal>NOT ENFORCED</literal>, the database system will
not check the constraint. It is then up to the application code to
ensure that the constraints are satisfied. The database system might
still assume that the data actually satisfies the constraint for
optimization decisions where this does not affect the correctness of the
result.
```IMO the third sentence should be removed because it is bogus. There's
no situation in which a not-enforced constraint can be used for any
query optimizations -- you cannot know if a constraint remains valid
after it's been turned NOT ENFORCED, because anyone could insert data
that violates it milliseconds after it stops being enforced. I think
the expectation that the application is going to correctly enforce the
constraint after it's told the database server not to enforce it, is
going to be problematic. As I recall, we already do this in FDWs for
instance and it's already a problem.
The database system could use the presence of a not enforced constraint
for selectivity estimation, for example.
On 2025-Feb-04, Peter Eisentraut wrote:
On 03.02.25 08:50, Alvaro Herrera wrote:
On 2025-Feb-03, Ashutosh Bapat wrote:
VALID, NOT ENFORCED changed to VALID, ENFORCED - data validation
required, constraint is enforcedThere's no such thing as a VALID NOT ENFORCED constraint. It just
cannot exist.The way I interpret this is that the VALID flag is just recording what would
happen if the constraint was enforced. So you you take a [NOT] VALID
ENFORCED constraint and switch it to NOT ENFORCED and back and you get back
to where you started.
I think it is dangerous. You can easily end up with undetected
violating rows in the table, and then you won't be able to dump/restore
it anymore.
--
Álvaro Herrera Breisgau, Deutschland — https://www.EnterpriseDB.com/
"After a quick R of TFM, all I can say is HOLY CR** THAT IS COOL! PostgreSQL was
amazing when I first started using it at 7.2, and I'm continually astounded by
learning new features and techniques made available by the continuing work of
the development team."
Berend Tober, http://archives.postgresql.org/pgsql-hackers/2007-08/msg01009.php
On Tue, Feb 4, 2025 at 7:22 PM Peter Eisentraut <peter@eisentraut.org> wrote:
On 03.02.25 06:19, Ashutosh Bapat wrote:
Here's how I see the state conversions happening.
NOT VALID, NOT ENFORCED changed to NOT_VALID, ENFORCED - no data
validation required, constraint is enforced on the new tuples/changes
NOT VALID, ENFORCED changed to NOT VALID, NOT ENFORCED - no data
validation, constraint isn't enforced anymore
VALID, NOT ENFORCED changed to VALID, ENFORCED - data validation
required, constraint is enforced
VALID, ENFORCED changed to VALID, NOT ENFORCED - no data validation
required, constrain isn't enforced anymore, we rely on user to enforce
the constraint on their sideThis looks sensible to me.
Attached patch implemented this behaviour. To achieve this, we have to
revert (see 0007) some committed code and relax the restriction that
the NOT ENFORCED constraint must also be NOT VALID. Now, NOT ENFORCED
and NOT VALID are independent statuses, and the psql-meta meta command
will display NOT VALID alongside the NOT ENFORCED constraint.
Previously, we hid NOT VALID for NOT ENFORCED constraints, assuming it
would be implied, but that is no longer the case.
Apart from this, I have added a few more tests in 0009. Additionally,
I made some minor code rearrangements in the
AttachPartitionForeignKey() function -- was introduced in the 0002
refactoring patch. I kept these rearrangement changes separate in 0003
to highlight them, allowing the reviewer to identify any potential
issues, though I don't believe there are any. Of course, I could be
wrong.
Also, I need more time for additional testing of the 0008 and 0009
patches, which I plan to complete by the end of this week. Thanks.
Regards,
Amul
Attachments:
v13-0001-Add-AlterConstraintStmt-struct-for-ALTER-.-CONST.patchapplication/x-patch; name=v13-0001-Add-AlterConstraintStmt-struct-for-ALTER-.-CONST.patchDownload
From 69a5d1d8553c9990241b05167476a303435d7fa8 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Tue, 4 Feb 2025 09:55:18 +0530
Subject: [PATCH v13 1/9] Add AlterConstraintStmt struct for ALTER ..
CONSTRAINT.
Replace the use of Constraint with the new AlterConstraintStmt struct,
which allows us to pass additional information, such as whether the
constraint attribute is set. This is necessary for the feature patches
that involve altering the enforceability of foreign key and check
constraints as part of the implementation.
---
src/backend/commands/tablecmds.c | 36 ++++++++++++++++----------------
src/backend/parser/gram.y | 3 +--
src/include/nodes/parsenodes.h | 14 +++++++++++++
src/tools/pgindent/typedefs.list | 1 +
4 files changed, 34 insertions(+), 20 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 5823fce9340..5c8484ba256 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -389,17 +389,17 @@ static void AlterIndexNamespaces(Relation classRel, Relation rel,
static void AlterSeqNamespaces(Relation classRel, Relation rel,
Oid oldNspOid, Oid newNspOid, ObjectAddresses *objsMoved,
LOCKMODE lockmode);
-static ObjectAddress ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd,
+static ObjectAddress ATExecAlterConstraint(Relation rel, AlterConstraintStmt *cmdcon,
bool recurse, bool recursing, LOCKMODE lockmode);
-static bool ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode);
+static bool ATExecAlterConstrRecurse(AlterConstraintStmt *cmdcon, Relation conrel,
+ Relation tgrel, Relation rel, HeapTuple contuple,
+ List **otherrelids, LOCKMODE lockmode);
static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
bool deferrable, bool initdeferred,
List **otherrelids);
-static void ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode);
+static void ATExecAlterChildConstr(AlterConstraintStmt *cmdcon, Relation conrel,
+ Relation tgrel, Relation rel, HeapTuple contuple,
+ List **otherrelids, LOCKMODE lockmode);
static ObjectAddress ATExecValidateConstraint(List **wqueue,
Relation rel, char *constrName,
bool recurse, bool recursing, LOCKMODE lockmode);
@@ -5450,7 +5450,10 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
lockmode);
break;
case AT_AlterConstraint: /* ALTER CONSTRAINT */
- address = ATExecAlterConstraint(rel, cmd, false, false, lockmode);
+ address =
+ ATExecAlterConstraint(rel,
+ castNode(AlterConstraintStmt, cmd->def),
+ false, false, lockmode);
break;
case AT_ValidateConstraint: /* VALIDATE CONSTRAINT */
address = ATExecValidateConstraint(wqueue, rel, cmd->name, cmd->recurse,
@@ -11801,10 +11804,9 @@ GetForeignKeyCheckTriggers(Relation trigrel,
* InvalidObjectAddress.
*/
static ObjectAddress
-ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
+ATExecAlterConstraint(Relation rel, AlterConstraintStmt *cmdcon, bool recurse,
bool recursing, LOCKMODE lockmode)
{
- Constraint *cmdcon;
Relation conrel;
Relation tgrel;
SysScanDesc scan;
@@ -11815,8 +11817,6 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
List *otherrelids = NIL;
ListCell *lc;
- cmdcon = castNode(Constraint, cmd->def);
-
conrel = table_open(ConstraintRelationId, RowExclusiveLock);
tgrel = table_open(TriggerRelationId, RowExclusiveLock);
@@ -11939,9 +11939,9 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
* but existing releases don't do that.)
*/
static bool
-ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode)
+ATExecAlterConstrRecurse(AlterConstraintStmt *cmdcon, Relation conrel,
+ Relation tgrel, Relation rel, HeapTuple contuple,
+ List **otherrelids, LOCKMODE lockmode)
{
Form_pg_constraint currcon;
Oid conoid;
@@ -12079,9 +12079,9 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
* ATExecAlterConstrRecurse.
*/
static void
-ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode)
+ATExecAlterChildConstr(AlterConstraintStmt *cmdcon, Relation conrel,
+ Relation tgrel, Relation rel, HeapTuple contuple,
+ List **otherrelids, LOCKMODE lockmode)
{
Form_pg_constraint currcon;
Oid conoid;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index d3887628d46..4835c8dd307 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -2657,11 +2657,10 @@ alter_table_cmd:
| ALTER CONSTRAINT name ConstraintAttributeSpec
{
AlterTableCmd *n = makeNode(AlterTableCmd);
- Constraint *c = makeNode(Constraint);
+ AlterConstraintStmt *c = makeNode(AlterConstraintStmt);
n->subtype = AT_AlterConstraint;
n->def = (Node *) c;
- c->contype = CONSTR_FOREIGN; /* others not supported, yet */
c->conname = $3;
processCASbits($4, @4, "ALTER CONSTRAINT statement",
&c->deferrable,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 8dd421fa0ef..909bf71e601 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2503,6 +2503,20 @@ typedef struct AlterCollationStmt
List *collname;
} AlterCollationStmt;
+/* ----------------------
+ * Alter Constraint
+ * ----------------------
+ */
+typedef struct AlterConstraintStmt
+{
+ NodeTag type;
+
+ char *conname; /* Constraint name */
+
+ bool deferrable; /* DEFERRABLE? */
+ bool initdeferred; /* INITIALLY DEFERRED? */
+} AlterConstraintStmt;
+
/* ----------------------
* Alter Domain
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9a3bee93dec..b7780ad57a3 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -66,6 +66,7 @@ AllocSetFreeList
AllocateDesc
AllocateDescKind
AlterCollationStmt
+AlterConstraintStmt
AlterDatabaseRefreshCollStmt
AlterDatabaseSetStmt
AlterDatabaseStmt
--
2.43.5
v13-0002-refactor-Split-tryAttachPartitionForeignKey.patchapplication/x-patch; name=v13-0002-refactor-Split-tryAttachPartitionForeignKey.patchDownload
From dffb57dee483cce84a3ba90b352d55775826129d Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Wed, 29 Jan 2025 11:35:27 +0530
Subject: [PATCH v13 2/9] refactor: Split tryAttachPartitionForeignKey()
Split tryAttachPartitionForeignKey() into three functions:
AttachPartitionForeignKey(), RemoveInheritedConstraint() and
DropForeignKeyConstraintTriggers(), so they can be reused in the next
patch.
---
src/backend/commands/tablecmds.c | 319 ++++++++++++++++++++-----------
1 file changed, 204 insertions(+), 115 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 5c8484ba256..d4915efa590 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -583,6 +583,14 @@ static bool tryAttachPartitionForeignKey(List **wqueue,
Oid parentInsTrigger,
Oid parentUpdTrigger,
Relation trigrel);
+static void AttachPartitionForeignKey(List **wqueue, Relation partition,
+ Oid partConstrOid, Oid parentConstrOid,
+ Oid parentInsTrigger, Oid parentUpdTrigger,
+ Relation trigrel);
+static void RemoveInheritedConstraint(Relation conrel, Relation trigrel,
+ Oid conoid, Oid conrelid);
+static void DropForeignKeyConstraintTriggers(Relation trigrel, Oid conoid,
+ Oid confrelid, Oid conrelid);
static void GetForeignKeyActionTriggers(Relation trigrel,
Oid conoid, Oid confrelid, Oid conrelid,
Oid *deleteTriggerOid,
@@ -11445,12 +11453,6 @@ tryAttachPartitionForeignKey(List **wqueue,
Form_pg_constraint parentConstr;
HeapTuple partcontup;
Form_pg_constraint partConstr;
- bool queueValidation;
- ScanKeyData key;
- SysScanDesc scan;
- HeapTuple trigtup;
- Oid insertTriggerOid,
- updateTriggerOid;
parentConstrTup = SearchSysCache1(CONSTROID,
ObjectIdGetDatum(parentConstrOid));
@@ -11495,6 +11497,59 @@ tryAttachPartitionForeignKey(List **wqueue,
return false;
}
+ ReleaseSysCache(parentConstrTup);
+ ReleaseSysCache(partcontup);
+
+ /* Looks good! Attach this constraint. */
+ AttachPartitionForeignKey(wqueue, partition, fk->conoid,
+ parentConstrOid, parentInsTrigger,
+ parentUpdTrigger, trigrel);
+
+ return true;
+}
+
+/*
+ * AttachPartitionForeignKey
+ *
+ * The subroutine for tryAttachPartitionForeignKey performs the final tasks of
+ * attaching the constraint, removing redundant triggers and entries from
+ * pg_constraint, and setting the constraint's parent.
+ */
+static void
+AttachPartitionForeignKey(List **wqueue,
+ Relation partition,
+ Oid partConstrOid,
+ Oid parentConstrOid,
+ Oid parentInsTrigger,
+ Oid parentUpdTrigger,
+ Relation trigrel)
+{
+ HeapTuple parentConstrTup;
+ Form_pg_constraint parentConstr;
+ HeapTuple partcontup;
+ Form_pg_constraint partConstr;
+ bool queueValidation;
+ Oid partConstrFrelid;
+ Oid partConstrRelid;
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
+ /* Fetch the parent constraint tuple */
+ parentConstrTup = SearchSysCache1(CONSTROID,
+ ObjectIdGetDatum(parentConstrOid));
+ if (!HeapTupleIsValid(parentConstrTup))
+ elog(ERROR, "cache lookup failed for constraint %u", parentConstrOid);
+ parentConstr = (Form_pg_constraint) GETSTRUCT(parentConstrTup);
+
+ /* Fetch the child constraint tuple */
+ partcontup = SearchSysCache1(CONSTROID,
+ ObjectIdGetDatum(partConstrOid));
+ if (!HeapTupleIsValid(partcontup))
+ elog(ERROR, "cache lookup failed for constraint %u", partConstrOid);
+ partConstr = (Form_pg_constraint) GETSTRUCT(partcontup);
+ partConstrFrelid = partConstr->confrelid;
+ partConstrRelid = partConstr->conrelid;
+
/*
* Will we need to validate this constraint? A valid parent constraint
* implies that all child constraints have been validated, so if this one
@@ -11506,50 +11561,15 @@ tryAttachPartitionForeignKey(List **wqueue,
ReleaseSysCache(parentConstrTup);
/*
- * Looks good! Attach this constraint. The action triggers in the new
- * partition become redundant -- the parent table already has equivalent
- * ones, and those will be able to reach the partition. Remove the ones
- * in the partition. We identify them because they have our constraint
- * OID, as well as being on the referenced rel.
+ * The action triggers in the new partition become redundant -- the parent
+ * table already has equivalent ones, and those will be able to reach the
+ * partition. Remove the ones in the partition. We identify them because
+ * they have our constraint OID, as well as being on the referenced rel.
*/
- ScanKeyInit(&key,
- Anum_pg_trigger_tgconstraint,
- BTEqualStrategyNumber, F_OIDEQ,
- ObjectIdGetDatum(fk->conoid));
- scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
- NULL, 1, &key);
- while ((trigtup = systable_getnext(scan)) != NULL)
- {
- Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
- ObjectAddress trigger;
+ DropForeignKeyConstraintTriggers(trigrel, partConstrOid, partConstrFrelid,
+ partConstrRelid);
- if (trgform->tgconstrrelid != fk->conrelid)
- continue;
- if (trgform->tgrelid != fk->confrelid)
- continue;
-
- /*
- * The constraint is originally set up to contain this trigger as an
- * implementation object, so there's a dependency record that links
- * the two; however, since the trigger is no longer needed, we remove
- * the dependency link in order to be able to drop the trigger while
- * keeping the constraint intact.
- */
- deleteDependencyRecordsFor(TriggerRelationId,
- trgform->oid,
- false);
- /* make dependency deletion visible to performDeletion */
- CommandCounterIncrement();
- ObjectAddressSet(trigger, TriggerRelationId,
- trgform->oid);
- performDeletion(&trigger, DROP_RESTRICT, 0);
- /* make trigger drop visible, in case the loop iterates */
- CommandCounterIncrement();
- }
-
- systable_endscan(scan);
-
- ConstraintSetParentConstraint(fk->conoid, parentConstrOid,
+ ConstraintSetParentConstraint(partConstrOid, parentConstrOid,
RelationGetRelid(partition));
/*
@@ -11557,7 +11577,7 @@ tryAttachPartitionForeignKey(List **wqueue,
* corresponding parent triggers.
*/
GetForeignKeyCheckTriggers(trigrel,
- fk->conoid, fk->confrelid, fk->conrelid,
+ partConstrOid, partConstrFrelid, partConstrRelid,
&insertTriggerOid, &updateTriggerOid);
Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
@@ -11571,72 +11591,12 @@ tryAttachPartitionForeignKey(List **wqueue,
* attaching now has extra pg_constraint rows and action triggers that are
* no longer needed. Remove those.
*/
- if (get_rel_relkind(fk->confrelid) == RELKIND_PARTITIONED_TABLE)
+ if (get_rel_relkind(partConstrFrelid) == RELKIND_PARTITIONED_TABLE)
{
Relation pg_constraint = table_open(ConstraintRelationId, RowShareLock);
- ObjectAddresses *objs;
- HeapTuple consttup;
- ScanKeyInit(&key,
- Anum_pg_constraint_conrelid,
- BTEqualStrategyNumber, F_OIDEQ,
- ObjectIdGetDatum(fk->conrelid));
-
- scan = systable_beginscan(pg_constraint,
- ConstraintRelidTypidNameIndexId,
- true, NULL, 1, &key);
- objs = new_object_addresses();
- while ((consttup = systable_getnext(scan)) != NULL)
- {
- Form_pg_constraint conform = (Form_pg_constraint) GETSTRUCT(consttup);
-
- if (conform->conparentid != fk->conoid)
- continue;
- else
- {
- ObjectAddress addr;
- SysScanDesc scan2;
- ScanKeyData key2;
- int n PG_USED_FOR_ASSERTS_ONLY;
-
- ObjectAddressSet(addr, ConstraintRelationId, conform->oid);
- add_exact_object_address(&addr, objs);
-
- /*
- * First we must delete the dependency record that binds the
- * constraint records together.
- */
- n = deleteDependencyRecordsForSpecific(ConstraintRelationId,
- conform->oid,
- DEPENDENCY_INTERNAL,
- ConstraintRelationId,
- fk->conoid);
- Assert(n == 1); /* actually only one is expected */
-
- /*
- * Now search for the triggers for this constraint and set
- * them up for deletion too
- */
- ScanKeyInit(&key2,
- Anum_pg_trigger_tgconstraint,
- BTEqualStrategyNumber, F_OIDEQ,
- ObjectIdGetDatum(conform->oid));
- scan2 = systable_beginscan(trigrel, TriggerConstraintIndexId,
- true, NULL, 1, &key2);
- while ((trigtup = systable_getnext(scan2)) != NULL)
- {
- ObjectAddressSet(addr, TriggerRelationId,
- ((Form_pg_trigger) GETSTRUCT(trigtup))->oid);
- add_exact_object_address(&addr, objs);
- }
- systable_endscan(scan2);
- }
- }
- /* make the dependency deletions visible */
- CommandCounterIncrement();
- performMultipleDeletions(objs, DROP_RESTRICT,
- PERFORM_DELETION_INTERNAL);
- systable_endscan(scan);
+ RemoveInheritedConstraint(pg_constraint, trigrel, partConstrOid,
+ partConstrRelid);
table_close(pg_constraint, RowShareLock);
}
@@ -11657,9 +11617,10 @@ tryAttachPartitionForeignKey(List **wqueue,
Relation conrel;
conrel = table_open(ConstraintRelationId, RowExclusiveLock);
- partcontup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
+
+ partcontup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(partConstrOid));
if (!HeapTupleIsValid(partcontup))
- elog(ERROR, "cache lookup failed for constraint %u", fk->conoid);
+ elog(ERROR, "cache lookup failed for constraint %u", partConstrOid);
/* Use the same lock as for AT_ValidateConstraint */
QueueFKConstraintValidation(wqueue, conrel, partition, partcontup,
@@ -11667,8 +11628,136 @@ tryAttachPartitionForeignKey(List **wqueue,
ReleaseSysCache(partcontup);
table_close(conrel, RowExclusiveLock);
}
+}
- return true;
+/*
+ * RemoveInheritedConstraint
+ *
+ * Removes the constraint and its associated trigger from the specified
+ * relation, which inherited the given constraint.
+ */
+static void
+RemoveInheritedConstraint(Relation conrel, Relation trigrel, Oid conoid,
+ Oid conrelid)
+{
+ ObjectAddresses *objs;
+ HeapTuple consttup;
+ ScanKeyData key;
+ SysScanDesc scan;
+ HeapTuple trigtup;
+
+ ScanKeyInit(&key,
+ Anum_pg_constraint_conrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conrelid));
+
+ scan = systable_beginscan(conrel,
+ ConstraintRelidTypidNameIndexId,
+ true, NULL, 1, &key);
+ objs = new_object_addresses();
+ while ((consttup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_constraint conform = (Form_pg_constraint) GETSTRUCT(consttup);
+
+ if (conform->conparentid != conoid)
+ continue;
+ else
+ {
+ ObjectAddress addr;
+ SysScanDesc scan2;
+ ScanKeyData key2;
+ int n PG_USED_FOR_ASSERTS_ONLY;
+
+ ObjectAddressSet(addr, ConstraintRelationId, conform->oid);
+ add_exact_object_address(&addr, objs);
+
+ /*
+ * First we must delete the dependency record that binds the
+ * constraint records together.
+ */
+ n = deleteDependencyRecordsForSpecific(ConstraintRelationId,
+ conform->oid,
+ DEPENDENCY_INTERNAL,
+ ConstraintRelationId,
+ conoid);
+ Assert(n == 1); /* actually only one is expected */
+
+ /*
+ * Now search for the triggers for this constraint and set them up
+ * for deletion too
+ */
+ ScanKeyInit(&key2,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conform->oid));
+ scan2 = systable_beginscan(trigrel, TriggerConstraintIndexId,
+ true, NULL, 1, &key2);
+ while ((trigtup = systable_getnext(scan2)) != NULL)
+ {
+ ObjectAddressSet(addr, TriggerRelationId,
+ ((Form_pg_trigger) GETSTRUCT(trigtup))->oid);
+ add_exact_object_address(&addr, objs);
+ }
+ systable_endscan(scan2);
+ }
+ }
+ /* make the dependency deletions visible */
+ CommandCounterIncrement();
+ performMultipleDeletions(objs, DROP_RESTRICT,
+ PERFORM_DELETION_INTERNAL);
+ systable_endscan(scan);
+}
+
+/*
+ * DropForeignKeyConstraintTriggers
+ *
+ * The subroutine for tryAttachPartitionForeignKey handles the deletion of
+ * action triggers for the foreign key constraint.
+ */
+static void
+DropForeignKeyConstraintTriggers(Relation trigrel, Oid conoid, Oid confrelid,
+ Oid conrelid)
+{
+ ScanKeyData key;
+ SysScanDesc scan;
+ HeapTuple trigtup;
+
+ ScanKeyInit(&key,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+ scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
+ NULL, 1, &key);
+ while ((trigtup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
+ ObjectAddress trigger;
+
+ if (trgform->tgconstrrelid != conrelid)
+ continue;
+ if (trgform->tgrelid != confrelid)
+ continue;
+
+ /*
+ * The constraint is originally set up to contain this trigger as an
+ * implementation object, so there's a dependency record that links
+ * the two; however, since the trigger is no longer needed, we remove
+ * the dependency link in order to be able to drop the trigger while
+ * keeping the constraint intact.
+ */
+ deleteDependencyRecordsFor(TriggerRelationId,
+ trgform->oid,
+ false);
+ /* make dependency deletion visible to performDeletion */
+ CommandCounterIncrement();
+ ObjectAddressSet(trigger, TriggerRelationId,
+ trgform->oid);
+ performDeletion(&trigger, DROP_RESTRICT, 0);
+ /* make trigger drop visible, in case the loop iterates */
+ CommandCounterIncrement();
+ }
+
+ systable_endscan(scan);
}
/*
--
2.43.5
v13-0003-Move-the-RemoveInheritedConstraint-function-call.patchapplication/x-patch; name=v13-0003-Move-the-RemoveInheritedConstraint-function-call.patchDownload
From bf68c5e5d233ebc0a55de2c28f5db28e0e790f6d Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 10 Feb 2025 09:12:06 +0530
Subject: [PATCH v13 3/9] Move the RemoveInheritedConstraint() function call
slightly earlier.
This change is harmless and does not affect the existing intended
operation. It is necessary for the feature patch operation, where we
may need to change the child constraint to enforced. In this case, we
would create the necessary triggers and queue the constraint for
validation, so it is important to remove any unnecessary constraints
before proceeding.
-- NOTE --
This is a small change that could have been included in the previous
"split tryAttachPartitionForeignKey" refactoring patch, but was kept
separate to highlight the changes.
---------
---
src/backend/commands/tablecmds.c | 30 +++++++++++++++---------------
1 file changed, 15 insertions(+), 15 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d4915efa590..dc0bf8c629f 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -11550,6 +11550,21 @@ AttachPartitionForeignKey(List **wqueue,
partConstrFrelid = partConstr->confrelid;
partConstrRelid = partConstr->conrelid;
+ /*
+ * If the referenced table is partitioned, then the partition we're
+ * attaching now has extra pg_constraint rows and action triggers that are
+ * no longer needed. Remove those.
+ */
+ if (get_rel_relkind(partConstrFrelid) == RELKIND_PARTITIONED_TABLE)
+ {
+ Relation pg_constraint = table_open(ConstraintRelationId, RowShareLock);
+
+ RemoveInheritedConstraint(pg_constraint, trigrel, partConstrOid,
+ partConstrRelid);
+
+ table_close(pg_constraint, RowShareLock);
+ }
+
/*
* Will we need to validate this constraint? A valid parent constraint
* implies that all child constraints have been validated, so if this one
@@ -11586,21 +11601,6 @@ AttachPartitionForeignKey(List **wqueue,
TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
RelationGetRelid(partition));
- /*
- * If the referenced table is partitioned, then the partition we're
- * attaching now has extra pg_constraint rows and action triggers that are
- * no longer needed. Remove those.
- */
- if (get_rel_relkind(partConstrFrelid) == RELKIND_PARTITIONED_TABLE)
- {
- Relation pg_constraint = table_open(ConstraintRelationId, RowShareLock);
-
- RemoveInheritedConstraint(pg_constraint, trigrel, partConstrOid,
- partConstrRelid);
-
- table_close(pg_constraint, RowShareLock);
- }
-
/*
* We updated this pg_constraint row above to set its parent; validating
* it will cause its convalidated flag to change, so we need CCI here. In
--
2.43.5
v13-0004-refactor-Pass-Relid-instead-of-Relation-to-creat.patchapplication/x-patch; name=v13-0004-refactor-Pass-Relid-instead-of-Relation-to-creat.patchDownload
From 2a0ddd8a34b96df8dabe18a9fa2481768bbbcbf8 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 20 Jan 2025 18:49:41 +0530
Subject: [PATCH v13 4/9] refactor: Pass Relid instead of Relation to
createForeignKeyCheckTriggers().
Currently, createForeignKeyCheckTriggers() takes a Relation type as
its first argument, but it doesn't use that argument directly.
Instead, it fetches the Relid by calling RelationGetRelid().
Therefore, it would be more consistent with other functions (e.g.,
createForeignKeyCheckTriggers()) to pass the Relid directly instead of
the whole Relation.
---
src/backend/commands/tablecmds.c | 20 ++++++++++----------
1 file changed, 10 insertions(+), 10 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index dc0bf8c629f..5a7f4808821 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -569,7 +569,7 @@ static void createForeignKeyCheckTriggers(Oid myRelOid, Oid refRelOid,
Oid indexOid,
Oid parentInsTrigger, Oid parentUpdTrigger,
Oid *insertTrigOid, Oid *updateTrigOid);
-static void createForeignKeyActionTriggers(Relation rel, Oid refRelOid,
+static void createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid,
Constraint *fkconstraint, Oid constraintOid,
Oid indexOid,
Oid parentDelTrigger, Oid parentUpdTrigger,
@@ -10659,7 +10659,8 @@ addFkRecurseReferenced(Constraint *fkconstraint, Relation rel,
/*
* Create the action triggers that enforce the constraint.
*/
- createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
+ createForeignKeyActionTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
fkconstraint,
parentConstr, indexOid,
parentDelTrigger, parentUpdTrigger,
@@ -13034,10 +13035,11 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
* *updateTrigOid.
*/
static void
-createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstraint,
- Oid constraintOid, Oid indexOid,
- Oid parentDelTrigger, Oid parentUpdTrigger,
- Oid *deleteTrigOid, Oid *updateTrigOid)
+createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid,
+ Constraint *fkconstraint, Oid constraintOid,
+ Oid indexOid, Oid parentDelTrigger,
+ Oid parentUpdTrigger, Oid *deleteTrigOid,
+ Oid *updateTrigOid)
{
CreateTrigStmt *fk_trigger;
ObjectAddress trigAddress;
@@ -13093,8 +13095,7 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
break;
}
- trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid,
- RelationGetRelid(rel),
+ trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid, myRelOid,
constraintOid, indexOid, InvalidOid,
parentDelTrigger, NULL, true, false);
if (deleteTrigOid)
@@ -13154,8 +13155,7 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
break;
}
- trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid,
- RelationGetRelid(rel),
+ trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid, myRelOid,
constraintOid, indexOid, InvalidOid,
parentUpdTrigger, NULL, true, false);
if (updateTrigOid)
--
2.43.5
v13-0005-refactor-Change-ATExecAlterConstrRecurse-argumen.patchapplication/x-patch; name=v13-0005-refactor-Change-ATExecAlterConstrRecurse-argumen.patchDownload
From 6f97a87e234e198b14fa8ab574a50ea4ef9f2000 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Tue, 14 Jan 2025 22:35:32 +0530
Subject: [PATCH v13 5/9] refactor: Change ATExecAlterConstrRecurse() argument.
Instead of passing the Relation of the referencing relation, we will
pass a relid. Opening the relation using the relid again should not
cause significant issues. However, this approach makes recursion more
efficient, especially in the later patch where we will be recreating
the referencing and referred triggers. In addition to that passing
relid of the referred relation too.
----
NOTE: This patch is not meant to be committed separately. It should
be squashed with the main patch that adds ENFORCED/NOT ENFORCED.
----
---
src/backend/commands/tablecmds.c | 44 +++++++++++++++++---------------
1 file changed, 23 insertions(+), 21 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 5a7f4808821..9f26088a0bb 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -392,14 +392,16 @@ static void AlterSeqNamespaces(Relation classRel, Relation rel,
static ObjectAddress ATExecAlterConstraint(Relation rel, AlterConstraintStmt *cmdcon,
bool recurse, bool recursing, LOCKMODE lockmode);
static bool ATExecAlterConstrRecurse(AlterConstraintStmt *cmdcon, Relation conrel,
- Relation tgrel, Relation rel, HeapTuple contuple,
- List **otherrelids, LOCKMODE lockmode);
+ Relation tgrel, const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode);
static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
bool deferrable, bool initdeferred,
List **otherrelids);
static void ATExecAlterChildConstr(AlterConstraintStmt *cmdcon, Relation conrel,
- Relation tgrel, Relation rel, HeapTuple contuple,
- List **otherrelids, LOCKMODE lockmode);
+ Relation tgrel, const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode);
static ObjectAddress ATExecValidateConstraint(List **wqueue,
Relation rel, char *constrName,
bool recurse, bool recursing, LOCKMODE lockmode);
@@ -11996,8 +11998,9 @@ ATExecAlterConstraint(Relation rel, AlterConstraintStmt *cmdcon, bool recurse,
currcon->condeferred != cmdcon->initdeferred ||
rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
- if (ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, rel, contuple,
- &otherrelids, lockmode))
+ if (ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, currcon->conrelid,
+ currcon->confrelid, contuple, &otherrelids,
+ lockmode))
ObjectAddressSet(address, ConstraintRelationId, currcon->oid);
}
@@ -12030,13 +12033,15 @@ ATExecAlterConstraint(Relation rel, AlterConstraintStmt *cmdcon, bool recurse,
*/
static bool
ATExecAlterConstrRecurse(AlterConstraintStmt *cmdcon, Relation conrel,
- Relation tgrel, Relation rel, HeapTuple contuple,
- List **otherrelids, LOCKMODE lockmode)
+ Relation tgrel, const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode)
{
Form_pg_constraint currcon;
Oid conoid;
Oid refrelid;
bool changed = false;
+ Relation rel;
/* since this function recurses, it could be driven to stack overflow */
check_stack_depth();
@@ -12045,6 +12050,8 @@ ATExecAlterConstrRecurse(AlterConstraintStmt *cmdcon, Relation conrel,
conoid = currcon->oid;
refrelid = currcon->confrelid;
+ rel = table_open(currcon->conrelid, lockmode);
+
/*
* Update pg_constraint with the flags from cmdcon.
*
@@ -12090,8 +12097,9 @@ ATExecAlterConstrRecurse(AlterConstraintStmt *cmdcon, Relation conrel,
*/
if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)
- ATExecAlterChildConstr(cmdcon, conrel, tgrel, rel, contuple,
- otherrelids, lockmode);
+ ATExecAlterChildConstr(cmdcon, conrel, tgrel, fkrelid, pkrelid,
+ contuple, otherrelids, lockmode);
+ table_close(rel, NoLock);
return changed;
}
@@ -12170,8 +12178,9 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
*/
static void
ATExecAlterChildConstr(AlterConstraintStmt *cmdcon, Relation conrel,
- Relation tgrel, Relation rel, HeapTuple contuple,
- List **otherrelids, LOCKMODE lockmode)
+ Relation tgrel, const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode)
{
Form_pg_constraint currcon;
Oid conoid;
@@ -12191,15 +12200,8 @@ ATExecAlterChildConstr(AlterConstraintStmt *cmdcon, Relation conrel,
true, NULL, 1, &pkey);
while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
- {
- Form_pg_constraint childcon = (Form_pg_constraint) GETSTRUCT(childtup);
- Relation childrel;
-
- childrel = table_open(childcon->conrelid, lockmode);
- ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, childrel, childtup,
- otherrelids, lockmode);
- table_close(childrel, NoLock);
- }
+ ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, fkrelid, pkrelid,
+ childtup, otherrelids, lockmode);
systable_endscan(pscan);
}
--
2.43.5
v13-0006-Remove-hastriggers-flag-check-before-fetching-FK.patchapplication/x-patch; name=v13-0006-Remove-hastriggers-flag-check-before-fetching-FK.patchDownload
From 65f911fe4c088b05ef02ef9b54dbb1bc4619e18f Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 7 Oct 2024 12:40:07 +0530
Subject: [PATCH v13 6/9] Remove hastriggers flag check before fetching FK
constraints.
With NOT ENFORCED, foreign key constraints will be created without
triggers. Therefore, the criteria for fetching foreign keys based on
the presence of triggers no longer apply.
----
NOTE: This patch is intended to reduce the diff noise from the main
patch and is not meant to be committed separately. It should be
squashed with the main patch that adds ENFORCED/NOT ENFORCED.
----
---
src/backend/utils/cache/relcache.c | 5 -
src/bin/pg_dump/pg_dump.c | 8 +-
src/bin/psql/describe.c | 226 ++++++++++++++---------------
3 files changed, 108 insertions(+), 131 deletions(-)
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 398114373e9..c010de4aeca 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4661,11 +4661,6 @@ RelationGetFKeyList(Relation relation)
if (relation->rd_fkeyvalid)
return relation->rd_fkeylist;
- /* Fast path: non-partitioned tables without triggers can't have FKs */
- if (!relation->rd_rel->relhastriggers &&
- relation->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
- return NIL;
-
/*
* We build the list we intend to return (in the caller's context) while
* doing the scan. After successfully completing the scan, we copy that
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 520e1338c28..8f685995bf1 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -7842,13 +7842,7 @@ getConstraints(Archive *fout, TableInfo tblinfo[], int numTables)
{
TableInfo *tinfo = &tblinfo[i];
- /*
- * For partitioned tables, foreign keys have no triggers so they must
- * be included anyway in case some foreign keys are defined.
- */
- if ((!tinfo->hastriggers &&
- tinfo->relkind != RELKIND_PARTITIONED_TABLE) ||
- !(tinfo->dobj.dump & DUMP_COMPONENT_DEFINITION))
+ if (!(tinfo->dobj.dump & DUMP_COMPONENT_DEFINITION))
continue;
/* OK, we need info for this table */
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 3b7ba66fad0..3f214e9805b 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2550,136 +2550,124 @@ describeOneTableDetails(const char *schemaname,
PQclear(result);
}
- /*
- * Print foreign-key constraints (there are none if no triggers,
- * except if the table is partitioned, in which case the triggers
- * appear in the partitions)
- */
- if (tableinfo.hastriggers ||
- tableinfo.relkind == RELKIND_PARTITIONED_TABLE)
+ /* Print foreign-key constraints */
+ if (pset.sversion >= 120000 &&
+ (tableinfo.ispartition || tableinfo.relkind == RELKIND_PARTITIONED_TABLE))
{
- if (pset.sversion >= 120000 &&
- (tableinfo.ispartition || tableinfo.relkind == RELKIND_PARTITIONED_TABLE))
+ /*
+ * Put the constraints defined in this table first, followed
+ * by the constraints defined in ancestor partitioned tables.
+ */
+ printfPQExpBuffer(&buf,
+ "SELECT conrelid = '%s'::pg_catalog.regclass AS sametable,\n"
+ " conname,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef,\n"
+ " conrelid::pg_catalog.regclass AS ontable\n"
+ " FROM pg_catalog.pg_constraint,\n"
+ " pg_catalog.pg_partition_ancestors('%s')\n"
+ " WHERE conrelid = relid AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
+ "ORDER BY sametable DESC, conname;",
+ oid, oid);
+ }
+ else
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT true as sametable, conname,\n"
+ " pg_catalog.pg_get_constraintdef(r.oid, true) as condef,\n"
+ " conrelid::pg_catalog.regclass AS ontable\n"
+ "FROM pg_catalog.pg_constraint r\n"
+ "WHERE r.conrelid = '%s' AND r.contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n",
+ oid);
+
+ if (pset.sversion >= 120000)
+ appendPQExpBufferStr(&buf, " AND conparentid = 0\n");
+ appendPQExpBufferStr(&buf, "ORDER BY conname");
+ }
+
+ result = PSQLexec(buf.data);
+ if (!result)
+ goto error_return;
+ else
+ tuples = PQntuples(result);
+
+ if (tuples > 0)
+ {
+ int i_sametable = PQfnumber(result, "sametable"),
+ i_conname = PQfnumber(result, "conname"),
+ i_condef = PQfnumber(result, "condef"),
+ i_ontable = PQfnumber(result, "ontable");
+
+ printTableAddFooter(&cont, _("Foreign-key constraints:"));
+ for (i = 0; i < tuples; i++)
{
/*
- * Put the constraints defined in this table first, followed
- * by the constraints defined in ancestor partitioned tables.
+ * Print untranslated constraint name and definition. Use
+ * a "TABLE tab" prefix when the constraint is defined in
+ * a parent partitioned table.
*/
- printfPQExpBuffer(&buf,
- "SELECT conrelid = '%s'::pg_catalog.regclass AS sametable,\n"
- " conname,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef,\n"
- " conrelid::pg_catalog.regclass AS ontable\n"
- " FROM pg_catalog.pg_constraint,\n"
- " pg_catalog.pg_partition_ancestors('%s')\n"
- " WHERE conrelid = relid AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
- "ORDER BY sametable DESC, conname;",
- oid, oid);
- }
- else
- {
- printfPQExpBuffer(&buf,
- "SELECT true as sametable, conname,\n"
- " pg_catalog.pg_get_constraintdef(r.oid, true) as condef,\n"
- " conrelid::pg_catalog.regclass AS ontable\n"
- "FROM pg_catalog.pg_constraint r\n"
- "WHERE r.conrelid = '%s' AND r.contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n",
- oid);
-
- if (pset.sversion >= 120000)
- appendPQExpBufferStr(&buf, " AND conparentid = 0\n");
- appendPQExpBufferStr(&buf, "ORDER BY conname");
- }
-
- result = PSQLexec(buf.data);
- if (!result)
- goto error_return;
- else
- tuples = PQntuples(result);
-
- if (tuples > 0)
- {
- int i_sametable = PQfnumber(result, "sametable"),
- i_conname = PQfnumber(result, "conname"),
- i_condef = PQfnumber(result, "condef"),
- i_ontable = PQfnumber(result, "ontable");
-
- printTableAddFooter(&cont, _("Foreign-key constraints:"));
- for (i = 0; i < tuples; i++)
- {
- /*
- * Print untranslated constraint name and definition. Use
- * a "TABLE tab" prefix when the constraint is defined in
- * a parent partitioned table.
- */
- if (strcmp(PQgetvalue(result, i, i_sametable), "f") == 0)
- printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
- PQgetvalue(result, i, i_ontable),
- PQgetvalue(result, i, i_conname),
- PQgetvalue(result, i, i_condef));
- else
- printfPQExpBuffer(&buf, " \"%s\" %s",
- PQgetvalue(result, i, i_conname),
- PQgetvalue(result, i, i_condef));
-
- printTableAddFooter(&cont, buf.data);
- }
- }
- PQclear(result);
- }
-
- /* print incoming foreign-key references */
- if (tableinfo.hastriggers ||
- tableinfo.relkind == RELKIND_PARTITIONED_TABLE)
- {
- if (pset.sversion >= 120000)
- {
- printfPQExpBuffer(&buf,
- "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
- " FROM pg_catalog.pg_constraint c\n"
- " WHERE confrelid IN (SELECT pg_catalog.pg_partition_ancestors('%s')\n"
- " UNION ALL VALUES ('%s'::pg_catalog.regclass))\n"
- " AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
- "ORDER BY conname;",
- oid, oid);
- }
- else
- {
- printfPQExpBuffer(&buf,
- "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
- " FROM pg_catalog.pg_constraint\n"
- " WHERE confrelid = %s AND contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n"
- "ORDER BY conname;",
- oid);
- }
-
- result = PSQLexec(buf.data);
- if (!result)
- goto error_return;
- else
- tuples = PQntuples(result);
-
- if (tuples > 0)
- {
- int i_conname = PQfnumber(result, "conname"),
- i_ontable = PQfnumber(result, "ontable"),
- i_condef = PQfnumber(result, "condef");
-
- printTableAddFooter(&cont, _("Referenced by:"));
- for (i = 0; i < tuples; i++)
- {
+ if (strcmp(PQgetvalue(result, i, i_sametable), "f") == 0)
printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
PQgetvalue(result, i, i_ontable),
PQgetvalue(result, i, i_conname),
PQgetvalue(result, i, i_condef));
+ else
+ printfPQExpBuffer(&buf, " \"%s\" %s",
+ PQgetvalue(result, i, i_conname),
+ PQgetvalue(result, i, i_condef));
- printTableAddFooter(&cont, buf.data);
- }
+ printTableAddFooter(&cont, buf.data);
}
- PQclear(result);
}
+ PQclear(result);
+
+ /* print incoming foreign-key references */
+ if (pset.sversion >= 120000)
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
+ " FROM pg_catalog.pg_constraint c\n"
+ " WHERE confrelid IN (SELECT pg_catalog.pg_partition_ancestors('%s')\n"
+ " UNION ALL VALUES ('%s'::pg_catalog.regclass))\n"
+ " AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
+ "ORDER BY conname;",
+ oid, oid);
+ }
+ else
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
+ " FROM pg_catalog.pg_constraint\n"
+ " WHERE confrelid = %s AND contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n"
+ "ORDER BY conname;",
+ oid);
+ }
+
+ result = PSQLexec(buf.data);
+ if (!result)
+ goto error_return;
+ else
+ tuples = PQntuples(result);
+
+ if (tuples > 0)
+ {
+ int i_conname = PQfnumber(result, "conname"),
+ i_ontable = PQfnumber(result, "ontable"),
+ i_condef = PQfnumber(result, "condef");
+
+ printTableAddFooter(&cont, _("Referenced by:"));
+ for (i = 0; i < tuples; i++)
+ {
+ printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
+ PQgetvalue(result, i, i_ontable),
+ PQgetvalue(result, i, i_conname),
+ PQgetvalue(result, i, i_condef));
+
+ printTableAddFooter(&cont, buf.data);
+ }
+ }
+ PQclear(result);
/* print any row-level policies */
if (pset.sversion >= 90500)
--
2.43.5
v13-0007-Ease-the-restriction-that-a-NOT-ENFORCED-constra.patchapplication/x-patch; name=v13-0007-Ease-the-restriction-that-a-NOT-ENFORCED-constra.patchDownload
From 3dd65d92b3472b2cd7bbec1841285fc8712534dc Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Fri, 7 Feb 2025 09:41:20 +0530
Subject: [PATCH v13 7/9] Ease the restriction that a NOT ENFORCED constraint
must be INVALID.
In the initial support for NOT ENFORCED check constraints in commit
ca87c415e2fccf81cec6fd45698dde9fae0ab570, we introduced a restriction
that a NOT ENFORCED constraint must be NOT VALID. However, we still
mark a NOT ENFORCED constraint as NOT VALID when adding it to an
already existing table, as validation cannot be performed on a NOT
ENFORCED constraint. Conversely, when the constraint is created along
with the table, it is acceptable to mark the constraint as valid.
---
src/backend/catalog/heap.c | 3 +-
src/backend/catalog/pg_constraint.c | 2 -
src/backend/commands/tablecmds.c | 19 ++---
src/backend/optimizer/util/plancat.c | 9 +--
src/backend/parser/gram.y | 9 ---
src/backend/parser/parse_utilcmd.c | 13 ++--
src/backend/utils/adt/ruleutils.c | 6 +-
src/test/regress/expected/alter_table.out | 4 +-
src/test/regress/expected/inherit.out | 88 ++++++++++++++---------
src/test/regress/sql/alter_table.sql | 3 +-
src/test/regress/sql/inherit.sql | 13 +++-
11 files changed, 84 insertions(+), 85 deletions(-)
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 956f196fc95..913e63fe93f 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -2740,7 +2740,7 @@ MergeWithExistingConstraint(Relation rel, const char *ccname, Node *expr,
* If the child constraint is "not valid" then cannot merge with a
* valid parent constraint.
*/
- if (is_initially_valid && con->conenforced && !con->convalidated)
+ if (is_initially_valid && !con->convalidated)
ereport(ERROR,
(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
errmsg("constraint \"%s\" conflicts with NOT VALID constraint on relation \"%s\"",
@@ -2803,7 +2803,6 @@ MergeWithExistingConstraint(Relation rel, const char *ccname, Node *expr,
{
Assert(is_local);
con->conenforced = true;
- con->convalidated = true;
}
CatalogTupleUpdate(conDesc, &tup->t_self, tup);
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index ac80652baf2..60bc54a96b6 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -102,8 +102,6 @@ CreateConstraintEntry(const char *constraintName,
/* Only CHECK constraint can be not enforced */
Assert(isEnforced || constraintType == CONSTRAINT_CHECK);
- /* NOT ENFORCED constraint must be NOT VALID */
- Assert(isEnforced || !isValidated);
conDesc = table_open(ConstraintRelationId, RowExclusiveLock);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 9f26088a0bb..820a837e18b 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -3166,10 +3166,7 @@ MergeCheckConstraint(List *constraints, const char *name, Node *expr, bool is_en
* marked as ENFORCED because one of the parents is ENFORCED.
*/
if (!ccon->is_enforced && is_enforced)
- {
ccon->is_enforced = true;
- ccon->skip_validation = false;
- }
return constraints;
}
@@ -3190,7 +3187,6 @@ MergeCheckConstraint(List *constraints, const char *name, Node *expr, bool is_en
newcon->expr = expr;
newcon->inhcount = 1;
newcon->is_enforced = is_enforced;
- newcon->skip_validation = !is_enforced;
return lappend(constraints, newcon);
}
@@ -16897,8 +16893,7 @@ MergeConstraintsIntoExisting(Relation child_rel, Relation parent_rel)
* If the child constraint is "not valid" then cannot merge with a
* valid parent constraint
*/
- if (parent_con->convalidated && child_con->conenforced &&
- !child_con->convalidated)
+ if (parent_con->convalidated && !child_con->convalidated)
ereport(ERROR,
(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
errmsg("constraint \"%s\" conflicts with NOT VALID constraint on child table \"%s\"",
@@ -19266,18 +19261,12 @@ ConstraintImpliedByRelConstraint(Relation scanrel, List *testConstraint, List *p
Node *cexpr;
/*
- * If this constraint hasn't been fully validated yet, we must ignore
- * it here.
+ * If this constraint hasn't been fully validated yet or is not
+ * enforced, we must ignore it here.
*/
- if (!constr->check[i].ccvalid)
+ if (!constr->check[i].ccvalid || !constr->check[i].ccenforced)
continue;
- /*
- * NOT ENFORCED constraints are always marked as invalid, which should
- * have been ignored.
- */
- Assert(constr->check[i].ccenforced);
-
cexpr = stringToNode(constr->check[i].ccbin);
/*
diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c
index 71abb01f655..233a5d8da98 100644
--- a/src/backend/optimizer/util/plancat.c
+++ b/src/backend/optimizer/util/plancat.c
@@ -1302,22 +1302,15 @@ get_relation_constraints(PlannerInfo *root,
* ignore it here. Also ignore if NO INHERIT and we weren't told
* that that's safe.
*/
- if (!constr->check[i].ccvalid)
+ if (!constr->check[i].ccvalid || !constr->check[i].ccenforced)
continue;
- /*
- * NOT ENFORCED constraints are always marked as invalid, which
- * should have been ignored.
- */
- Assert(constr->check[i].ccenforced);
-
/*
* Also ignore if NO INHERIT and we weren't told that that's safe.
*/
if (constr->check[i].ccnoinherit && !include_noinherit)
continue;
-
cexpr = stringToNode(constr->check[i].ccbin);
/*
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 4835c8dd307..a36e322c78e 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -19558,15 +19558,6 @@ processCASbits(int cas_bits, int location, const char *constrType,
errmsg("%s constraints cannot be marked NOT ENFORCED",
constrType),
parser_errposition(location)));
-
- /*
- * NB: The validated status is irrelevant when the constraint is set to
- * NOT ENFORCED, but for consistency, it should be set accordingly.
- * This ensures that if the constraint is later changed to ENFORCED, it
- * will automatically be in the correct NOT VALIDATED state.
- */
- if (not_valid)
- *not_valid = true;
}
if (cas_bits & CAS_ENFORCED)
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index eb7716cd84c..9053c898e06 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -1452,7 +1452,6 @@ expandTableLikeClause(RangeVar *heapRel, TableLikeClause *table_like_clause)
char *ccname = constr->check[ccnum].ccname;
char *ccbin = constr->check[ccnum].ccbin;
bool ccenforced = constr->check[ccnum].ccenforced;
- bool ccvalid = constr->check[ccnum].ccvalid;
bool ccnoinherit = constr->check[ccnum].ccnoinherit;
Node *ccbin_node;
bool found_whole_row;
@@ -1483,13 +1482,13 @@ expandTableLikeClause(RangeVar *heapRel, TableLikeClause *table_like_clause)
n->conname = pstrdup(ccname);
n->location = -1;
n->is_enforced = ccenforced;
- n->initially_valid = ccvalid;
n->is_no_inherit = ccnoinherit;
n->raw_expr = NULL;
n->cooked_expr = nodeToString(ccbin_node);
/* We can skip validation, since the new table should be empty. */
n->skip_validation = true;
+ n->initially_valid = true;
atsubcmd = makeNode(AlterTableCmd);
atsubcmd->subtype = AT_AddConstraint;
@@ -2942,11 +2941,9 @@ transformCheckConstraints(CreateStmtContext *cxt, bool skipValidation)
return;
/*
- * When creating a new table (but not a foreign table), we can safely skip
- * the validation of check constraints and mark them as valid based on the
- * constraint enforcement flag, since NOT ENFORCED constraints must always
- * be marked as NOT VALID. (This will override any user-supplied NOT VALID
- * flag.)
+ * If creating a new table (but not a foreign table), we can safely skip
+ * validation of check constraints, and nonetheless mark them valid. (This
+ * will override any user-supplied NOT VALID flag.)
*/
if (skipValidation)
{
@@ -2955,7 +2952,7 @@ transformCheckConstraints(CreateStmtContext *cxt, bool skipValidation)
Constraint *constraint = (Constraint *) lfirst(ckclist);
constraint->skip_validation = true;
- constraint->initially_valid = constraint->is_enforced;
+ constraint->initially_valid = true;
}
}
}
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 54dad975553..41751181f61 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -2594,12 +2594,10 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
appendStringInfoString(&buf, " DEFERRABLE");
if (conForm->condeferred)
appendStringInfoString(&buf, " INITIALLY DEFERRED");
-
- /* Validated status is irrelevant when the constraint is NOT ENFORCED. */
+ if (!conForm->convalidated)
+ appendStringInfoString(&buf, " NOT VALID");
if (!conForm->conenforced)
appendStringInfoString(&buf, " NOT ENFORCED");
- else if (!conForm->convalidated)
- appendStringInfoString(&buf, " NOT VALID");
/* Cleanup */
systable_endscan(scandesc);
diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out
index 362f38856d2..54bdb3f2250 100644
--- a/src/test/regress/expected/alter_table.out
+++ b/src/test/regress/expected/alter_table.out
@@ -507,7 +507,9 @@ ALTER TABLE attmp3 validate constraint attmpconstr;
ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten CHECK (b > 10); -- fail
ERROR: check constraint "b_greater_than_ten" of relation "attmp3" is violated by some row
ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten CHECK (b > 10) NOT VALID; -- succeeds
-ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten_not_enforced CHECK (b > 10) NOT ENFORCED; -- succeeds
+ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten_not_enforced CHECK (b > 10) NOT ENFORCED; -- fail
+ERROR: check constraint "b_greater_than_ten_not_enforced" of relation "attmp3" is violated by some row
+ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten_not_enforced CHECK (b > 10) NOT VALID NOT ENFORCED; -- succeeds
ALTER TABLE attmp3 VALIDATE CONSTRAINT b_greater_than_ten; -- fails
ERROR: check constraint "b_greater_than_ten" of relation "attmp3" is violated by some row
DELETE FROM attmp3 WHERE NOT b > 10;
diff --git a/src/test/regress/expected/inherit.out b/src/test/regress/expected/inherit.out
index dbf3835cb14..d29f870feea 100644
--- a/src/test/regress/expected/inherit.out
+++ b/src/test/regress/expected/inherit.out
@@ -1332,6 +1332,18 @@ NOTICE: merging constraint "inh_check_constraint5" with inherited definition
alter table p1 add constraint inh_check_constraint6 check (f1 < 10) not enforced;
alter table p1_c1 add constraint inh_check_constraint6 check (f1 < 10) enforced;
NOTICE: merging constraint "inh_check_constraint6" with inherited definition
+-- however, cannot merge a not valid enforced child constraint with a valid
+-- not-enforced parent constraint.
+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;
+ERROR: constraint "inh_check_constraint9" conflicts with NOT VALID constraint on relation "p1_c1"
+-- but, allowed if the parent constraint is also invalid.
+alter table p1 add constraint inh_check_constraint9 check (f1 < 10) not valid not enforced;
+NOTICE: merging constraint "inh_check_constraint9" with inherited definition
+-- the invalid state of the child constraint will be ignored here.
+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
create table p1_c2(f1 int constraint inh_check_constraint4 check (f1 < 10)) inherits(p1);
NOTICE: merging column "f1" with inherited definition
NOTICE: merging constraint "inh_check_constraint4" with inherited definition
@@ -1353,42 +1365,50 @@ create table p1_fail(f1 int constraint inh_check_constraint6 check (f1 < 10) not
NOTICE: merging multiple inherited definitions of column "f1"
NOTICE: merging column "f1" with inherited definition
ERROR: constraint "inh_check_constraint6" conflicts with NOT ENFORCED constraint on relation "p1_fail"
-select conrelid::regclass::text as relname, conname, conislocal, coninhcount, conenforced
+select conrelid::regclass::text as relname, conname, conislocal, coninhcount, conenforced, convalidated
from pg_constraint where conname like 'inh\_check\_constraint%'
order by 1, 2;
- relname | conname | conislocal | coninhcount | conenforced
----------+-----------------------+------------+-------------+-------------
- p1 | inh_check_constraint1 | t | 0 | t
- p1 | inh_check_constraint2 | t | 0 | t
- p1 | inh_check_constraint3 | t | 0 | f
- p1 | inh_check_constraint4 | t | 0 | f
- p1 | inh_check_constraint5 | t | 0 | f
- p1 | inh_check_constraint6 | t | 0 | f
- p1 | inh_check_constraint8 | t | 0 | t
- p1_c1 | inh_check_constraint1 | t | 1 | t
- p1_c1 | inh_check_constraint2 | t | 1 | t
- p1_c1 | inh_check_constraint3 | t | 1 | f
- p1_c1 | inh_check_constraint4 | t | 1 | f
- p1_c1 | inh_check_constraint5 | t | 1 | t
- p1_c1 | inh_check_constraint6 | t | 1 | t
- p1_c1 | inh_check_constraint7 | t | 0 | f
- p1_c1 | inh_check_constraint8 | f | 1 | t
- p1_c2 | inh_check_constraint1 | f | 1 | t
- p1_c2 | inh_check_constraint2 | f | 1 | t
- p1_c2 | inh_check_constraint3 | f | 1 | f
- p1_c2 | inh_check_constraint4 | t | 1 | t
- p1_c2 | inh_check_constraint5 | f | 1 | f
- p1_c2 | inh_check_constraint6 | f | 1 | f
- p1_c2 | inh_check_constraint8 | f | 1 | t
- p1_c3 | inh_check_constraint1 | f | 2 | t
- p1_c3 | inh_check_constraint2 | f | 2 | t
- p1_c3 | inh_check_constraint3 | f | 2 | f
- p1_c3 | inh_check_constraint4 | f | 2 | f
- p1_c3 | inh_check_constraint5 | f | 2 | t
- p1_c3 | inh_check_constraint6 | f | 2 | t
- p1_c3 | inh_check_constraint7 | f | 1 | f
- p1_c3 | inh_check_constraint8 | f | 2 | t
-(30 rows)
+ relname | conname | conislocal | coninhcount | conenforced | convalidated
+---------+------------------------+------------+-------------+-------------+--------------
+ p1 | inh_check_constraint1 | t | 0 | t | t
+ p1 | inh_check_constraint10 | t | 0 | f | t
+ p1 | inh_check_constraint2 | t | 0 | t | t
+ p1 | inh_check_constraint3 | t | 0 | f | t
+ p1 | inh_check_constraint4 | t | 0 | f | t
+ p1 | inh_check_constraint5 | t | 0 | f | t
+ p1 | inh_check_constraint6 | t | 0 | f | t
+ 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_constraint2 | t | 1 | t | t
+ p1_c1 | inh_check_constraint3 | t | 1 | f | t
+ p1_c1 | inh_check_constraint4 | t | 1 | f | t
+ p1_c1 | inh_check_constraint5 | t | 1 | t | t
+ p1_c1 | inh_check_constraint6 | t | 1 | t | t
+ p1_c1 | inh_check_constraint7 | t | 0 | f | t
+ p1_c1 | inh_check_constraint8 | f | 1 | t | t
+ p1_c1 | inh_check_constraint9 | t | 1 | t | f
+ p1_c2 | inh_check_constraint1 | f | 1 | t | t
+ p1_c2 | inh_check_constraint10 | f | 1 | f | t
+ p1_c2 | inh_check_constraint2 | f | 1 | t | t
+ p1_c2 | inh_check_constraint3 | f | 1 | f | t
+ p1_c2 | inh_check_constraint4 | t | 1 | t | t
+ p1_c2 | inh_check_constraint5 | f | 1 | f | t
+ p1_c2 | inh_check_constraint6 | f | 1 | f | t
+ p1_c2 | inh_check_constraint8 | f | 1 | t | t
+ p1_c2 | inh_check_constraint9 | f | 1 | f | t
+ p1_c3 | inh_check_constraint1 | f | 2 | t | t
+ p1_c3 | inh_check_constraint10 | f | 2 | t | t
+ p1_c3 | inh_check_constraint2 | f | 2 | t | t
+ p1_c3 | inh_check_constraint3 | f | 2 | f | t
+ p1_c3 | inh_check_constraint4 | f | 2 | f | t
+ p1_c3 | inh_check_constraint5 | f | 2 | t | t
+ p1_c3 | inh_check_constraint6 | f | 2 | t | t
+ p1_c3 | inh_check_constraint7 | f | 1 | f | t
+ p1_c3 | inh_check_constraint8 | f | 2 | t | t
+ p1_c3 | inh_check_constraint9 | f | 2 | t | t
+(38 rows)
drop table p1 cascade;
NOTICE: drop cascades to 3 other objects
diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql
index 84e93ef575e..a9268de14b5 100644
--- a/src/test/regress/sql/alter_table.sql
+++ b/src/test/regress/sql/alter_table.sql
@@ -387,7 +387,8 @@ ALTER TABLE attmp3 validate constraint attmpconstr;
-- Try a non-verified CHECK constraint
ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten CHECK (b > 10); -- fail
ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten CHECK (b > 10) NOT VALID; -- succeeds
-ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten_not_enforced CHECK (b > 10) NOT ENFORCED; -- succeeds
+ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten_not_enforced CHECK (b > 10) NOT ENFORCED; -- fail
+ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten_not_enforced CHECK (b > 10) NOT VALID NOT ENFORCED; -- succeeds
ALTER TABLE attmp3 VALIDATE CONSTRAINT b_greater_than_ten; -- fails
DELETE FROM attmp3 WHERE NOT b > 10;
ALTER TABLE attmp3 VALIDATE CONSTRAINT b_greater_than_ten; -- succeeds
diff --git a/src/test/regress/sql/inherit.sql b/src/test/regress/sql/inherit.sql
index 49aae426f3c..35a85203286 100644
--- a/src/test/regress/sql/inherit.sql
+++ b/src/test/regress/sql/inherit.sql
@@ -481,6 +481,17 @@ alter table p1 add constraint inh_check_constraint5 check (f1 < 10) not enforced
alter table p1 add constraint inh_check_constraint6 check (f1 < 10) not enforced;
alter table p1_c1 add constraint inh_check_constraint6 check (f1 < 10) enforced;
+-- however, cannot merge a not valid enforced child constraint with a valid
+-- not-enforced parent constraint.
+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;
+-- but, allowed if the parent constraint is also invalid.
+alter table p1 add constraint inh_check_constraint9 check (f1 < 10) not valid not enforced;
+
+-- the invalid state of the child constraint will be ignored here.
+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;
+
create table p1_c2(f1 int constraint inh_check_constraint4 check (f1 < 10)) inherits(p1);
-- but reverse is not allowed
@@ -498,7 +509,7 @@ create table p1_c3() inherits(p1, p1_c1);
-- but not allowed if the child constraint is explicitly asked to be NOT ENFORCED
create table p1_fail(f1 int constraint inh_check_constraint6 check (f1 < 10) not enforced) inherits(p1, p1_c1);
-select conrelid::regclass::text as relname, conname, conislocal, coninhcount, conenforced
+select conrelid::regclass::text as relname, conname, conislocal, coninhcount, conenforced, convalidated
from pg_constraint where conname like 'inh\_check\_constraint%'
order by 1, 2;
--
2.43.5
v13-0008-Add-support-for-NOT-ENFORCED-in-foreign-key-cons.patchapplication/x-patch; name=v13-0008-Add-support-for-NOT-ENFORCED-in-foreign-key-cons.patchDownload
From b3adc8c243ab524830e91965d90a963df85cf65d Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 20 Jan 2025 21:45:07 +0530
Subject: [PATCH v13 8/9] Add support for NOT ENFORCED in foreign key
constraints.
Typically, when a foreign key (FK) constraint is created on a table,
action and check triggers are added to maintain data integrity. With
this patch, if a constraint is marked as NOT ENFORCED, integrity
checks are no longer required, making these triggers unnecessary.
Consequently, when creating a NOT ENFORCED FK constraint, triggers
will not be created, and the constraint will be marked as NOT VALID.
Similarly, if an existing FK constraint is changed to NOT ENFORCED,
the associated triggers will be dropped, and the constraint will also
be marked as NOT VALID. Conversely, if a NOT ENFORCED FK constraint is
changed to ENFORCED, the necessary triggers will be created, and the
constraint will remain in the NOT VALID state.
----
NOTE: In this patch, the tryAttachPartitionForeignKey() function will
not merge the constraint if the enforcibility differs. This will be
addressed in the next patch.
----
---
doc/src/sgml/advanced.sgml | 13 +
doc/src/sgml/catalogs.sgml | 2 +-
doc/src/sgml/ddl.sgml | 12 +-
doc/src/sgml/ref/alter_table.sgml | 5 +-
doc/src/sgml/ref/create_table.sgml | 2 +-
src/backend/catalog/pg_constraint.c | 5 +-
src/backend/catalog/sql_features.txt | 2 +-
src/backend/commands/tablecmds.c | 489 ++++++++++++++++------
src/backend/parser/gram.y | 11 +-
src/backend/parser/parse_utilcmd.c | 6 +-
src/backend/utils/cache/relcache.c | 1 +
src/include/nodes/parsenodes.h | 6 +
src/include/utils/rel.h | 3 +
src/test/regress/expected/constraints.out | 11 +-
src/test/regress/expected/foreign_key.out | 158 ++++++-
src/test/regress/sql/constraints.sql | 1 -
src/test/regress/sql/foreign_key.sql | 104 ++++-
17 files changed, 679 insertions(+), 152 deletions(-)
diff --git a/doc/src/sgml/advanced.sgml b/doc/src/sgml/advanced.sgml
index e15a3323dfb..14ed11e1f4d 100644
--- a/doc/src/sgml/advanced.sgml
+++ b/doc/src/sgml/advanced.sgml
@@ -125,6 +125,19 @@ ERROR: insert or update on table "weather" violates foreign key constraint "wea
DETAIL: Key (city)=(Berkeley) is not present in table "cities".
</screen>
</para>
+ <para>
+ If, for some reason, you do not want to enforce this constraint and wish to
+ avoid the error, you can modify the foreign key constraint to <literal>NOT ENFORCED</literal>,
+ or specify it in the <link linkend="sql-createtable"><command>CREATE TABLE</command></link>.
+ </para>
+ <para>
+ Let's change the constraint to <literal>NOT ENFORCED</literal>, and when you
+ attempt to reinsert the same record, it will not result in any error.
+<programlisting>
+ALTER TABLE weather ALTER CONSTRAINT weather_city_fkey NOT ENFORCED;
+INSERT INTO weather VALUES ('Berkeley', 45, 53, 0.0, '1994-11-28');
+</programlisting>
+ </para>
<para>
The behavior of foreign keys can be finely tuned to your
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index ee59a7e15d0..3831c68eeb1 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2599,7 +2599,7 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l
</para>
<para>
Is the constraint enforced?
- Currently, can be false only for CHECK constraints
+ Currently, can be false only for foreign keys and CHECK constraints
</para></entry>
</row>
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index ae156b6b1cd..90f4264f6d7 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -1081,11 +1081,13 @@ CREATE TABLE example (
</indexterm>
<para>
- A foreign key constraint specifies that the values in a column (or
- a group of columns) must match the values appearing in some row
- of another table.
- We say this maintains the <firstterm>referential
- integrity</firstterm> between two related tables.
+ A foreign key constraint with <literal>ENFORCED</literal>, the default
+ setting, specifies that the values in a column (or a group of columns)
+ must match the values appearing in some row of another table. We say this
+ maintains the <firstterm>referential integrity</firstterm> between two
+ related tables. If a foreign key constraint is created with <literal>NOT ENFORCED</literal>,
+ the check for aforementioned <firstterm>referential integrity</firstterm>
+ will not be performed.
</para>
<para>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 8e56b8e59b0..3d7cd0842de 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -58,7 +58,7 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable>
ALTER [ COLUMN ] <replaceable class="parameter">column_name</replaceable> SET COMPRESSION <replaceable class="parameter">compression_method</replaceable>
ADD <replaceable class="parameter">table_constraint</replaceable> [ NOT VALID ]
ADD <replaceable class="parameter">table_constraint_using_index</replaceable>
- ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+ ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]
VALIDATE CONSTRAINT <replaceable class="parameter">constraint_name</replaceable>
DROP CONSTRAINT [ IF EXISTS ] <replaceable class="parameter">constraint_name</replaceable> [ RESTRICT | CASCADE ]
DISABLE TRIGGER [ <replaceable class="parameter">trigger_name</replaceable> | ALL | USER ]
@@ -568,7 +568,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
This form validates a foreign key or check constraint that was
previously created as <literal>NOT VALID</literal>, by scanning the
table to ensure there are no rows for which the constraint is not
- satisfied. Nothing happens if the constraint is already marked valid.
+ satisfied. If the constraint is not enforced, an error is thrown.
+ Nothing happens if the constraint is already marked valid.
(See <xref linkend="sql-altertable-notes"/> below for an explanation
of the usefulness of this command.)
</para>
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index 9acbc4dd34d..3ec63ff4344 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -1405,7 +1405,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</para>
<para>
- This is currently only supported for <literal>CHECK</literal>
+ This is currently only supported for foreign key and <literal>CHECK</literal>
constraints.
</para>
</listitem>
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index 60bc54a96b6..aed9c0c8d9f 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -100,8 +100,9 @@ CreateConstraintEntry(const char *constraintName,
ObjectAddresses *addrs_auto;
ObjectAddresses *addrs_normal;
- /* Only CHECK constraint can be not enforced */
- Assert(isEnforced || constraintType == CONSTRAINT_CHECK);
+ /* Only CHECK or FOREIGN KEY constraint can be not enforced */
+ Assert(isEnforced || constraintType == CONSTRAINT_CHECK ||
+ constraintType == CONSTRAINT_FOREIGN);
conDesc = table_open(ConstraintRelationId, RowExclusiveLock);
diff --git a/src/backend/catalog/sql_features.txt b/src/backend/catalog/sql_features.txt
index 2f250d2c57b..d49471bbdf2 100644
--- a/src/backend/catalog/sql_features.txt
+++ b/src/backend/catalog/sql_features.txt
@@ -281,7 +281,7 @@ F461 Named character sets NO
F471 Scalar subquery values YES
F481 Expanded NULL predicate YES
F491 Constraint management YES
-F492 Optional table constraint enforcement NO check constraints only
+F492 Optional table constraint enforcement NO foreign key and check constraints only
F501 Features and conformance views YES
F501 Features and conformance views 01 SQL_FEATURES view YES
F501 Features and conformance views 02 SQL_SIZING view YES
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 820a837e18b..74b8ce82a18 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -389,17 +389,33 @@ static void AlterIndexNamespaces(Relation classRel, Relation rel,
static void AlterSeqNamespaces(Relation classRel, Relation rel,
Oid oldNspOid, Oid newNspOid, ObjectAddresses *objsMoved,
LOCKMODE lockmode);
-static ObjectAddress ATExecAlterConstraint(Relation rel, AlterConstraintStmt *cmdcon,
- bool recurse, bool recursing, LOCKMODE lockmode);
-static bool ATExecAlterConstrRecurse(AlterConstraintStmt *cmdcon, Relation conrel,
- Relation tgrel, const Oid fkrelid, const Oid pkrelid,
+static ObjectAddress ATExecAlterConstraint(List **wqueue, Relation rel,
+ AlterConstraintStmt *cmdcon,
+ bool recurse, bool recursing,
+ LOCKMODE lockmode);
+static bool ATExecAlterConstrRecurse(List **wqueue, AlterConstraintStmt *cmdcon,
+ Relation conrel, Relation tgrel,
+ const Oid fkrelid, const Oid pkrelid,
HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode);
-static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
+ LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger);
+static void ATExecAlterConstrEnforceability(List **wqueue, AlterConstraintStmt *cmdcon,
+ Relation conrel, Relation tgrel,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode,
+ Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger);
+static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Oid fkrel,
bool deferrable, bool initdeferred,
List **otherrelids);
-static void ATExecAlterChildConstr(AlterConstraintStmt *cmdcon, Relation conrel,
- Relation tgrel, const Oid fkrelid, const Oid pkrelid,
+static void ATExecAlterChildConstr(List **wqueue, AlterConstraintStmt *cmdcon,
+ Relation conrel, Relation tgrel,
+ const Oid fkrelid, const Oid pkrelid,
HeapTuple contuple, List **otherrelids,
LOCKMODE lockmode);
static ObjectAddress ATExecValidateConstraint(List **wqueue,
@@ -5457,7 +5473,7 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
break;
case AT_AlterConstraint: /* ALTER CONSTRAINT */
address =
- ATExecAlterConstraint(rel,
+ ATExecAlterConstraint(wqueue, rel,
castNode(AlterConstraintStmt, cmd->def),
false, false, lockmode);
break;
@@ -10530,7 +10546,7 @@ addFkConstraint(addFkConstraintSides fkside,
CONSTRAINT_FOREIGN,
fkconstraint->deferrable,
fkconstraint->initdeferred,
- true, /* Is Enforced */
+ fkconstraint->is_enforced,
fkconstraint->initially_valid,
parentConstr,
RelationGetRelid(rel),
@@ -10648,21 +10664,23 @@ addFkRecurseReferenced(Constraint *fkconstraint, Relation rel,
Oid parentDelTrigger, Oid parentUpdTrigger,
bool with_period)
{
- Oid deleteTriggerOid,
- updateTriggerOid;
+ Oid deleteTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
Assert(CheckRelationLockedByMe(pkrel, ShareRowExclusiveLock, true));
Assert(CheckRelationLockedByMe(rel, ShareRowExclusiveLock, true));
/*
- * Create the action triggers that enforce the constraint.
+ * Create action triggers to enforce the constraint, or skip them if the
+ * constraint is not enforced.
*/
- createForeignKeyActionTriggers(RelationGetRelid(rel),
- RelationGetRelid(pkrel),
- fkconstraint,
- parentConstr, indexOid,
- parentDelTrigger, parentUpdTrigger,
- &deleteTriggerOid, &updateTriggerOid);
+ if (fkconstraint->is_enforced)
+ createForeignKeyActionTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
+ fkconstraint,
+ parentConstr, indexOid,
+ parentDelTrigger, parentUpdTrigger,
+ &deleteTriggerOid, &updateTriggerOid);
/*
* If the referenced table is partitioned, recurse on ourselves to handle
@@ -10783,8 +10801,8 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
Oid parentInsTrigger, Oid parentUpdTrigger,
bool with_period)
{
- Oid insertTriggerOid,
- updateTriggerOid;
+ Oid insertTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
Assert(OidIsValid(parentConstr));
Assert(CheckRelationLockedByMe(rel, ShareRowExclusiveLock, true));
@@ -10796,29 +10814,32 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
errmsg("foreign key constraints are not supported on foreign tables")));
/*
- * Add the check triggers to it and, if necessary, schedule it to be
- * checked in Phase 3.
+ * Add check triggers if the constraint is enforced, and if needed,
+ * schedule them to be checked in Phase 3.
*
* If the relation is partitioned, drill down to do it to its partitions.
*/
- createForeignKeyCheckTriggers(RelationGetRelid(rel),
- RelationGetRelid(pkrel),
- fkconstraint,
- parentConstr,
- indexOid,
- parentInsTrigger, parentUpdTrigger,
- &insertTriggerOid, &updateTriggerOid);
+ if (fkconstraint->is_enforced)
+ createForeignKeyCheckTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
+ fkconstraint,
+ parentConstr,
+ indexOid,
+ parentInsTrigger, parentUpdTrigger,
+ &insertTriggerOid, &updateTriggerOid);
if (rel->rd_rel->relkind == RELKIND_RELATION)
{
/*
* Tell Phase 3 to check that the constraint is satisfied by existing
- * rows. We can skip this during table creation, when requested
- * explicitly by specifying NOT VALID in an ADD FOREIGN KEY command,
- * and when we're recreating a constraint following a SET DATA TYPE
- * operation that did not impugn its validity.
+ * rows. We can skip this during table creation, when constraint is
+ * specified as NOT ENFORCED, or when requested explicitly by specifying
+ * NOT VALID in an ADD FOREIGN KEY command, and when we're recreating
+ * a constraint following a SET DATA TYPE operation that did not
+ * impugn its validity.
*/
- if (wqueue && !old_check_ok && !fkconstraint->skip_validation)
+ if (wqueue && !old_check_ok && !fkconstraint->skip_validation &&
+ fkconstraint->is_enforced)
{
NewConstraint *newcon;
AlteredTableInfo *tab;
@@ -11049,8 +11070,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
AttrNumber confdelsetcols[INDEX_MAX_KEYS];
Constraint *fkconstraint;
ObjectAddress address;
- Oid deleteTriggerOid,
- updateTriggerOid;
+ Oid deleteTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(constrOid));
if (!HeapTupleIsValid(tuple))
@@ -11110,6 +11131,7 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
fkconstraint->fk_del_set_cols = NIL;
fkconstraint->old_conpfeqop = NIL;
fkconstraint->old_pktable_oid = InvalidOid;
+ fkconstraint->is_enforced = constrForm->conenforced;
fkconstraint->skip_validation = false;
fkconstraint->initially_valid = true;
@@ -11139,9 +11161,10 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
* parent OIDs for similar triggers that will be created on the
* partition in addFkRecurseReferenced().
*/
- GetForeignKeyActionTriggers(trigrel, constrOid,
- constrForm->confrelid, constrForm->conrelid,
- &deleteTriggerOid, &updateTriggerOid);
+ if (constrForm->conenforced)
+ GetForeignKeyActionTriggers(trigrel, constrOid,
+ constrForm->confrelid, constrForm->conrelid,
+ &deleteTriggerOid, &updateTriggerOid);
/* Add this constraint ... */
address = addFkConstraint(addFkReferencedSide,
@@ -11274,8 +11297,8 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
Oid indexOid;
ObjectAddress address;
ListCell *lc;
- Oid insertTriggerOid,
- updateTriggerOid;
+ Oid insertTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
bool with_period;
tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(parentConstrOid));
@@ -11307,17 +11330,18 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
mapped_conkey[i] = attmap->attnums[conkey[i] - 1];
/*
- * Get the "check" triggers belonging to the constraint to pass as
- * parent OIDs for similar triggers that will be created on the
- * partition in addFkRecurseReferencing(). They are also passed to
- * tryAttachPartitionForeignKey() below to simply assign as parents to
- * the partition's existing "check" triggers, that is, if the
- * corresponding constraints is deemed attachable to the parent
- * constraint.
+ * Get the "check" triggers belonging to the constraint, if it is
+ * enforced, to pass as parent OIDs for similar triggers that will be
+ * created on the partition in addFkRecurseReferencing(). They are
+ * also passed to tryAttachPartitionForeignKey() below to simply
+ * assign as parents to the partition's existing "check" triggers,
+ * that is, if the corresponding constraints is deemed attachable to
+ * the parent constraint.
*/
- GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
- constrForm->confrelid, constrForm->conrelid,
- &insertTriggerOid, &updateTriggerOid);
+ if (constrForm->conenforced)
+ GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
+ constrForm->confrelid, constrForm->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
/*
* Before creating a new constraint, see whether any existing FKs are
@@ -11370,6 +11394,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
fkconstraint->fk_del_set_cols = NIL;
fkconstraint->old_conpfeqop = NIL;
fkconstraint->old_pktable_oid = InvalidOid;
+ fkconstraint->is_enforced = constrForm->conenforced;
fkconstraint->skip_validation = false;
fkconstraint->initially_valid = constrForm->convalidated;
for (int i = 0; i < numfks; i++)
@@ -11487,6 +11512,7 @@ tryAttachPartitionForeignKey(List **wqueue,
if (OidIsValid(partConstr->conparentid) ||
partConstr->condeferrable != parentConstr->condeferrable ||
partConstr->condeferred != parentConstr->condeferred ||
+ partConstr->conenforced != parentConstr->conenforced ||
partConstr->confupdtype != parentConstr->confupdtype ||
partConstr->confdeltype != parentConstr->confdeltype ||
partConstr->confmatchtype != parentConstr->confmatchtype)
@@ -11530,8 +11556,7 @@ AttachPartitionForeignKey(List **wqueue,
bool queueValidation;
Oid partConstrFrelid;
Oid partConstrRelid;
- Oid insertTriggerOid,
- updateTriggerOid;
+ bool parentConstrIsEnforced;
/* Fetch the parent constraint tuple */
parentConstrTup = SearchSysCache1(CONSTROID,
@@ -11539,6 +11564,7 @@ AttachPartitionForeignKey(List **wqueue,
if (!HeapTupleIsValid(parentConstrTup))
elog(ERROR, "cache lookup failed for constraint %u", parentConstrOid);
parentConstr = (Form_pg_constraint) GETSTRUCT(parentConstrTup);
+ parentConstrIsEnforced = parentConstr->conenforced;
/* Fetch the child constraint tuple */
partcontup = SearchSysCache1(CONSTROID,
@@ -11588,17 +11614,24 @@ AttachPartitionForeignKey(List **wqueue,
/*
* Like the constraint, attach partition's "check" triggers to the
- * corresponding parent triggers.
+ * corresponding parent triggers if the constraint is enforced.
+ * Non-enforced constraints do not have these triggers.
*/
- GetForeignKeyCheckTriggers(trigrel,
- partConstrOid, partConstrFrelid, partConstrRelid,
- &insertTriggerOid, &updateTriggerOid);
- Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
- TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
- RelationGetRelid(partition));
- Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
- TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
- RelationGetRelid(partition));
+ if (parentConstrIsEnforced)
+ {
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
+ GetForeignKeyCheckTriggers(trigrel,
+ partConstrOid, partConstrFrelid, partConstrRelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
+ RelationGetRelid(partition));
+ Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
+ RelationGetRelid(partition));
+ }
/*
* We updated this pg_constraint row above to set its parent; validating
@@ -11712,6 +11745,10 @@ RemoveInheritedConstraint(Relation conrel, Relation trigrel, Oid conoid,
*
* The subroutine for tryAttachPartitionForeignKey handles the deletion of
* action triggers for the foreign key constraint.
+ *
+ * If valid confrelid and conrelid values are not provided, the respective
+ * trigger check will be skipped, and the trigger will be considered for
+ * removal.
*/
static void
DropForeignKeyConstraintTriggers(Relation trigrel, Oid conoid, Oid confrelid,
@@ -11732,10 +11769,27 @@ DropForeignKeyConstraintTriggers(Relation trigrel, Oid conoid, Oid confrelid,
Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
ObjectAddress trigger;
- if (trgform->tgconstrrelid != conrelid)
+ /* Invalid if trigger is not for a referential integrity constraint */
+ if (!OidIsValid(trgform->tgconstrrelid))
continue;
- if (trgform->tgrelid != confrelid)
+ if (OidIsValid(conrelid) && trgform->tgconstrrelid != conrelid)
continue;
+ if (OidIsValid(confrelid) && trgform->tgrelid != confrelid)
+ continue;
+
+ /* We should be droping trigger related to foreign ke constraint */
+ Assert(trgform->tgfoid == F_RI_FKEY_CHECK_INS ||
+ trgform->tgfoid == F_RI_FKEY_CHECK_UPD ||
+ trgform->tgfoid == F_RI_FKEY_CASCADE_DEL ||
+ trgform->tgfoid == F_RI_FKEY_CASCADE_UPD ||
+ trgform->tgfoid == F_RI_FKEY_RESTRICT_DEL ||
+ trgform->tgfoid == F_RI_FKEY_RESTRICT_UPD ||
+ trgform->tgfoid == F_RI_FKEY_SETNULL_DEL ||
+ trgform->tgfoid == F_RI_FKEY_SETNULL_UPD ||
+ trgform->tgfoid == F_RI_FKEY_SETDEFAULT_DEL ||
+ trgform->tgfoid == F_RI_FKEY_SETDEFAULT_UPD ||
+ trgform->tgfoid == F_RI_FKEY_NOACTION_DEL ||
+ trgform->tgfoid == F_RI_FKEY_NOACTION_UPD);
/*
* The constraint is originally set up to contain this trigger as an
@@ -11892,8 +11946,8 @@ GetForeignKeyCheckTriggers(Relation trigrel,
* InvalidObjectAddress.
*/
static ObjectAddress
-ATExecAlterConstraint(Relation rel, AlterConstraintStmt *cmdcon, bool recurse,
- bool recursing, LOCKMODE lockmode)
+ATExecAlterConstraint(List **wqueue, Relation rel, AlterConstraintStmt *cmdcon,
+ bool recurse, bool recursing, LOCKMODE lockmode)
{
Relation conrel;
Relation tgrel;
@@ -11935,10 +11989,19 @@ ATExecAlterConstraint(Relation rel, AlterConstraintStmt *cmdcon, bool recurse,
currcon = (Form_pg_constraint) GETSTRUCT(contuple);
if (currcon->contype != CONSTRAINT_FOREIGN)
- ereport(ERROR,
- (errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("constraint \"%s\" of relation \"%s\" is not a foreign key constraint",
- cmdcon->conname, RelationGetRelationName(rel))));
+ {
+ if (cmdcon->alterEnforceability)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot alter enforceability of constraint \"%s\" of relation \"%s\"",
+ cmdcon->conname, RelationGetRelationName(rel)),
+ errdetail("Enforceability can only be altered for foreign key constraints.")));
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("constraint \"%s\" of relation \"%s\" is not a foreign key constraint",
+ cmdcon->conname, RelationGetRelationName(rel))));
+ }
/*
* If it's not the topmost constraint, raise an error.
@@ -11990,13 +12053,14 @@ ATExecAlterConstraint(Relation rel, AlterConstraintStmt *cmdcon, bool recurse,
* processed regardless, in case they had the constraint locally changed.
*/
address = InvalidObjectAddress;
- if (currcon->condeferrable != cmdcon->deferrable ||
- currcon->condeferred != cmdcon->initdeferred ||
+ if (cmdcon->alterDeferrability || cmdcon->alterEnforceability ||
rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
- if (ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, currcon->conrelid,
- currcon->confrelid, contuple, &otherrelids,
- lockmode))
+ if (ATExecAlterConstrRecurse(wqueue, cmdcon, conrel, tgrel,
+ currcon->conrelid, currcon->confrelid,
+ contuple, &otherrelids, lockmode,
+ InvalidOid, InvalidOid, InvalidOid,
+ InvalidOid))
ObjectAddressSet(address, ConstraintRelationId, currcon->oid);
}
@@ -12028,10 +12092,14 @@ ATExecAlterConstraint(Relation rel, AlterConstraintStmt *cmdcon, bool recurse,
* but existing releases don't do that.)
*/
static bool
-ATExecAlterConstrRecurse(AlterConstraintStmt *cmdcon, Relation conrel,
- Relation tgrel, const Oid fkrelid, const Oid pkrelid,
- HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode)
+ATExecAlterConstrRecurse(List **wqueue, AlterConstraintStmt *cmdcon,
+ Relation conrel, Relation tgrel, const Oid fkrelid,
+ const Oid pkrelid, HeapTuple contuple,
+ List **otherrelids, LOCKMODE lockmode,
+ Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger)
{
Form_pg_constraint currcon;
Oid conoid;
@@ -12055,15 +12123,24 @@ ATExecAlterConstrRecurse(AlterConstraintStmt *cmdcon, Relation conrel,
* silently do nothing.
*/
if (currcon->condeferrable != cmdcon->deferrable ||
- currcon->condeferred != cmdcon->initdeferred)
+ currcon->condeferred != cmdcon->initdeferred ||
+ currcon->conenforced != cmdcon->is_enforced)
{
HeapTuple copyTuple;
Form_pg_constraint copy_con;
copyTuple = heap_copytuple(contuple);
copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
- copy_con->condeferrable = cmdcon->deferrable;
- copy_con->condeferred = cmdcon->initdeferred;
+
+ if (cmdcon->alterDeferrability)
+ {
+ copy_con->condeferrable = cmdcon->deferrable;
+ copy_con->condeferred = cmdcon->initdeferred;
+ }
+
+ if (cmdcon->alterEnforceability)
+ copy_con->conenforced = cmdcon->is_enforced;
+
CatalogTupleUpdate(conrel, ©Tuple->t_self, copyTuple);
InvokeObjectPostAlterHook(ConstraintRelationId,
@@ -12074,32 +12151,200 @@ ATExecAlterConstrRecurse(AlterConstraintStmt *cmdcon, Relation conrel,
/* Make new constraint flags visible to others */
CacheInvalidateRelcache(rel);
+ }
+ /*
+ * Note that even if deferrability is requested to be altered along with
+ * enforceability, we don't need to call AlterConstrTriggerDeferrability()
+ * explicitly. Changing enforceability involves either creating or
+ * dropping the trigger, during which the deferrability will be set
+ * accordingly.
+ */
+ if (cmdcon->alterEnforceability &&
+ currcon->conenforced != cmdcon->is_enforced)
+ {
+ ATExecAlterConstrEnforceability(wqueue, cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, contuple, otherrelids, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
+ /*
+ * Tell Phase 3 to check that the constraint is satisfied by existing
+ * rows, but skip this step if the constraint is NOT VALID.
+ */
+ if (cmdcon->is_enforced && currcon->convalidated &&
+ rel->rd_rel->relkind == RELKIND_RELATION)
+ {
+ AlteredTableInfo *tab;
+ NewConstraint *newcon;
+ Constraint *fkconstraint;
+
+ /* Queue validation for phase 3 */
+ fkconstraint = makeNode(Constraint);
+ /* for now this is all we need */
+ fkconstraint->conname = pstrdup(NameStr(currcon->conname));
+
+ newcon = (NewConstraint *) palloc0(sizeof(NewConstraint));
+ newcon->name = fkconstraint->conname;
+ newcon->contype = CONSTR_FOREIGN;
+ newcon->refrelid = currcon->confrelid;
+ newcon->refindid = currcon->conindid;
+ newcon->conid = conoid;
+ newcon->qual = (Node *) fkconstraint;
+
+ /* Find or create work queue entry for this table */
+ tab = ATGetQueueEntry(wqueue, rel);
+ tab->constraints = lappend(tab->constraints, newcon);
+ }
+ }
+ else
+ {
/*
* Now we need to update the multiple entries in pg_trigger that
* implement the constraint.
*/
- AlterConstrTriggerDeferrability(conoid, tgrel, rel, cmdcon->deferrable,
- cmdcon->initdeferred, otherrelids);
+ if (cmdcon->alterDeferrability &&
+ (currcon->condeferrable != cmdcon->deferrable ||
+ currcon->condeferred != cmdcon->initdeferred))
+ AlterConstrTriggerDeferrability(conoid, tgrel,
+ RelationGetRelid(rel),
+ cmdcon->deferrable,
+ cmdcon->initdeferred,
+ otherrelids);
+
+ /*
+ * If the table at either end of the constraint is partitioned, we
+ * need to recurse and handle every constraint that is a child of this
+ * one.
+ *
+ * (This assumes that the recurse flag is forcibly set for partitioned
+ * tables, and not set for legacy inheritance, though we don't check
+ * for that here.)
+ */
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)
+ ATExecAlterChildConstr(wqueue, cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, contuple, otherrelids, lockmode);
}
- /*
- * If the table at either end of the constraint is partitioned, we need to
- * recurse and handle every constraint that is a child of this one.
- *
- * (This assumes that the recurse flag is forcibly set for partitioned
- * tables, and not set for legacy inheritance, though we don't check for
- * that here.)
- */
- if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
- get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)
- ATExecAlterChildConstr(cmdcon, conrel, tgrel, fkrelid, pkrelid,
- contuple, otherrelids, lockmode);
table_close(rel, NoLock);
return changed;
}
+/*
+ * A subroutine of ATExecAlterConstrRecurse that updates the enforceability of
+ * a foreign key constraint. Depending on whether the constraint is being set
+ * to enforced or not enforced, it creates or drops the trigger accordingly.
+ *
+ * The arguments to this function have the same meaning as the arguments to
+ * ATExecAlterConstrRecurse.
+ */
+static void
+ATExecAlterConstrEnforceability(List **wqueue, AlterConstraintStmt *cmdcon,
+ Relation conrel, Relation tgrel,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode,
+ Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger)
+{
+ Form_pg_constraint currcon;
+ Oid conoid;
+
+ currcon = (Form_pg_constraint) GETSTRUCT(contuple);
+ conoid = currcon->oid;
+
+ /* Drop triggers */
+ if (!cmdcon->is_enforced)
+ {
+ /*
+ * When setting a constraint to NOT ENFORCED, the constraint triggers
+ * need to be dropped. Therefore, we must process the child relations
+ * first, followed by the parent, to account for dependencies.
+ */
+ if (get_rel_relkind(currcon->conrelid) == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE)
+ ATExecAlterChildConstr(wqueue, cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, contuple, otherrelids, lockmode);
+
+ /* Drop all the triggers */
+ DropForeignKeyConstraintTriggers(tgrel, conoid, InvalidOid, InvalidOid);
+ }
+ else /* Create triggers */
+ {
+ Oid ReferencedDelTriggerOid = InvalidOid,
+ ReferencedUpdTriggerOid = InvalidOid,
+ ReferencingInsTriggerOid = InvalidOid,
+ ReferencingUpdTriggerOid = InvalidOid;
+
+ /* Prepare the minimal information required for trigger creation. */
+ Constraint *fkconstraint = makeNode(Constraint);
+
+ fkconstraint->fk_matchtype = currcon->confmatchtype;
+ fkconstraint->fk_upd_action = currcon->confupdtype;
+ fkconstraint->fk_del_action = currcon->confdeltype;
+
+ /* Create referenced triggers */
+ if (currcon->conrelid == fkrelid)
+ createForeignKeyActionTriggers(currcon->conrelid,
+ currcon->confrelid,
+ fkconstraint,
+ conoid,
+ currcon->conindid,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ &ReferencedDelTriggerOid,
+ &ReferencedUpdTriggerOid);
+
+ /* Create referencing triggers */
+ if (currcon->confrelid == pkrelid)
+ createForeignKeyCheckTriggers(currcon->conrelid,
+ pkrelid,
+ fkconstraint,
+ conoid,
+ currcon->conindid,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger,
+ &ReferencingInsTriggerOid,
+ &ReferencingUpdTriggerOid);
+
+ /*
+ * If the table at either end of the constraint is partitioned, we
+ * need to recurse and create triggers for each constraint that is a
+ * child of this one.
+ */
+ if (get_rel_relkind(currcon->conrelid) == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE)
+ {
+ ScanKeyData pkey;
+ SysScanDesc pscan;
+ HeapTuple childtup;
+
+ ScanKeyInit(&pkey,
+ Anum_pg_constraint_conparentid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+
+ pscan = systable_beginscan(conrel, ConstraintParentIndexId,
+ true, NULL, 1, &pkey);
+
+ while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
+ ATExecAlterConstrRecurse(wqueue, cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, childtup, otherrelids,
+ lockmode, ReferencedDelTriggerOid,
+ ReferencedUpdTriggerOid,
+ ReferencingInsTriggerOid,
+ ReferencingUpdTriggerOid);
+
+ systable_endscan(pscan);
+ }
+ }
+}
+
/*
* A subroutine of ATExecAlterConstrRecurse that updated constraint trigger's
* deferrability.
@@ -12108,7 +12353,7 @@ ATExecAlterConstrRecurse(AlterConstraintStmt *cmdcon, Relation conrel,
* ATExecAlterConstrRecurse.
*/
static void
-AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
+AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Oid fkrel,
bool deferrable, bool initdeferred,
List **otherrelids)
{
@@ -12134,7 +12379,7 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
* other rels that don't have a trigger whose properties change, but
* let's be conservative.)
*/
- if (tgform->tgrelid != RelationGetRelid(rel))
+ if (tgform->tgrelid != fkrel)
*otherrelids = list_append_unique_oid(*otherrelids,
tgform->tgrelid);
@@ -12173,10 +12418,10 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
* ATExecAlterConstrRecurse.
*/
static void
-ATExecAlterChildConstr(AlterConstraintStmt *cmdcon, Relation conrel,
- Relation tgrel, const Oid fkrelid, const Oid pkrelid,
- HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode)
+ATExecAlterChildConstr(List **wqueue, AlterConstraintStmt *cmdcon,
+ Relation conrel, Relation tgrel, const Oid fkrelid,
+ const Oid pkrelid, HeapTuple contuple,
+ List **otherrelids, LOCKMODE lockmode)
{
Form_pg_constraint currcon;
Oid conoid;
@@ -12196,8 +12441,9 @@ ATExecAlterChildConstr(AlterConstraintStmt *cmdcon, Relation conrel,
true, NULL, 1, &pkey);
while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
- ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, fkrelid, pkrelid,
- childtup, otherrelids, lockmode);
+ ATExecAlterConstrRecurse(wqueue, cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, childtup, otherrelids, lockmode,
+ InvalidOid, InvalidOid, InvalidOid, InvalidOid);
systable_endscan(pscan);
}
@@ -20267,8 +20513,6 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
Form_pg_constraint conform;
- Oid insertTriggerOid,
- updateTriggerOid;
contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
if (!HeapTupleIsValid(contup))
@@ -20295,17 +20539,25 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
/*
* Also, look up the partition's "check" triggers corresponding to the
- * constraint being detached and detach them from the parent triggers.
+ * enforced constraint being detached and detach them from the parent
+ * triggers. Non-enforced constraints do not have these triggers;
+ * therefore, this step is not needed.
*/
- GetForeignKeyCheckTriggers(trigrel,
- fk->conoid, fk->confrelid, fk->conrelid,
- &insertTriggerOid, &updateTriggerOid);
- Assert(OidIsValid(insertTriggerOid));
- TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
- RelationGetRelid(partRel));
- Assert(OidIsValid(updateTriggerOid));
- TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
- RelationGetRelid(partRel));
+ if (fk->conenforced)
+ {
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ Assert(OidIsValid(updateTriggerOid));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ }
/*
* Lastly, create the action triggers on the referenced table, using
@@ -20345,6 +20597,7 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
fkconstraint->conname = pstrdup(NameStr(conform->conname));
fkconstraint->deferrable = conform->condeferrable;
fkconstraint->initdeferred = conform->condeferred;
+ fkconstraint->is_enforced = conform->conenforced;
fkconstraint->skip_validation = true;
fkconstraint->initially_valid = true;
/* a few irrelevant fields omitted here */
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index a36e322c78e..4cab07f989a 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -2665,7 +2665,12 @@ alter_table_cmd:
processCASbits($4, @4, "ALTER CONSTRAINT statement",
&c->deferrable,
&c->initdeferred,
- NULL, NULL, NULL, yyscanner);
+ &c->is_enforced, NULL, NULL, yyscanner);
+ c->alterDeferrability =
+ ($4 & (CAS_NOT_DEFERRABLE | CAS_DEFERRABLE |
+ CAS_INITIALLY_IMMEDIATE | CAS_INITIALLY_IMMEDIATE)) != 0;
+ c->alterEnforceability =
+ ($4 & (CAS_NOT_ENFORCED | CAS_ENFORCED)) != 0;
$$ = (Node *) n;
}
/* ALTER TABLE <name> VALIDATE CONSTRAINT ... */
@@ -4315,8 +4320,8 @@ ConstraintElem:
n->fk_del_set_cols = ($11)->deleteAction->cols;
processCASbits($12, @12, "FOREIGN KEY",
&n->deferrable, &n->initdeferred,
- NULL, &n->skip_validation, NULL,
- yyscanner);
+ &n->is_enforced, &n->skip_validation,
+ NULL, yyscanner);
n->initially_valid = !n->skip_validation;
$$ = (Node *) n;
}
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 9053c898e06..d568eba5cac 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -3977,7 +3977,8 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
case CONSTR_ATTR_ENFORCED:
if (lastprimarycon == NULL ||
- lastprimarycon->contype != CONSTR_CHECK)
+ (lastprimarycon->contype != CONSTR_CHECK &&
+ lastprimarycon->contype != CONSTR_FOREIGN))
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("misplaced ENFORCED clause"),
@@ -3993,7 +3994,8 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
case CONSTR_ATTR_NOT_ENFORCED:
if (lastprimarycon == NULL ||
- lastprimarycon->contype != CONSTR_CHECK)
+ (lastprimarycon->contype != CONSTR_CHECK &&
+ lastprimarycon->contype != CONSTR_FOREIGN))
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("misplaced NOT ENFORCED clause"),
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index c010de4aeca..dd956b0bf4a 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4692,6 +4692,7 @@ RelationGetFKeyList(Relation relation)
info->conoid = constraint->oid;
info->conrelid = constraint->conrelid;
info->confrelid = constraint->confrelid;
+ info->conenforced = constraint->conenforced;
DeconstructFkConstraintRow(htup, &info->nkeys,
info->conkey,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 909bf71e601..edab71e68c1 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2513,8 +2513,14 @@ typedef struct AlterConstraintStmt
char *conname; /* Constraint name */
+ bool alterDeferrability; /* True when any of the following flags
+ * are specifically set for modification */
bool deferrable; /* DEFERRABLE? */
bool initdeferred; /* INITIALLY DEFERRED? */
+
+ bool alterEnforceability; /* True when following flag is
+ * specifically set for modification */
+ bool is_enforced; /* enforced constraint? */
} AlterConstraintStmt;
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 48b95f211f3..fcba0efbd7c 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -284,6 +284,9 @@ typedef struct ForeignKeyCacheInfo
/* number of columns in the foreign key */
int nkeys;
+ /* Is enforced ? */
+ bool conenforced;
+
/*
* these arrays each have nkeys valid entries:
*/
diff --git a/src/test/regress/expected/constraints.out b/src/test/regress/expected/constraints.out
index 692a69fe457..75d40d06b6e 100644
--- a/src/test/regress/expected/constraints.out
+++ b/src/test/regress/expected/constraints.out
@@ -744,15 +744,12 @@ CREATE TABLE UNIQUE_NOTEN_TBL(i int UNIQUE NOT ENFORCED);
ERROR: misplaced NOT ENFORCED clause
LINE 1: CREATE TABLE UNIQUE_NOTEN_TBL(i int UNIQUE NOT ENFORCED);
^
--- XXX: error message is misleading here
ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
-ERROR: ALTER CONSTRAINT statement constraints cannot be marked ENFORCED
-LINE 1: ...TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
- ^
+ERROR: cannot alter enforceability of constraint "unique_tbl_i_key" of relation "unique_tbl"
+DETAIL: Enforceability can only be altered for foreign key constraints.
ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORCED;
-ERROR: ALTER CONSTRAINT statement constraints cannot be marked NOT ENFORCED
-LINE 1: ...ABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORC...
- ^
+ERROR: cannot alter enforceability of constraint "unique_tbl_i_key" of relation "unique_tbl"
+DETAIL: Enforceability can only be altered for foreign key constraints.
DROP TABLE unique_tbl;
--
-- EXCLUDE constraints
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 374dcb266e7..3183b1cbe8f 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -1,12 +1,43 @@
--
-- FOREIGN KEY
--
--- MATCH FULL
+-- NOT ENFORCED
--
-- First test, check and cascade
--
CREATE TABLE PKTABLE ( ptest1 int PRIMARY KEY, ptest2 text );
-CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE, ftest2 int );
+CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE NOT ENFORCED,
+ ftest2 int );
+-- Inserting into the foreign key table will not result in an error, even if
+-- there is no matching key in the referenced table.
+INSERT INTO FKTABLE VALUES (1, 2);
+INSERT INTO FKTABLE VALUES (2, 3);
+-- Check FKTABLE
+SELECT * FROM FKTABLE;
+ ftest1 | ftest2
+--------+--------
+ 1 | 2
+ 2 | 3
+(2 rows)
+
+-- Reverting it back to ENFORCED will result in failure because constraint validation will be triggered,
+-- as it was previously in a valid state.
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_fkey"
+DETAIL: Key (ftest1)=(1) is not present in table "pktable".
+-- Remove any existing rows that violate the constraint, then attempt to change
+-- it.
+TRUNCATE FKTABLE;
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+-- Any further inserts will fail due to the enforcement.
+INSERT INTO FKTABLE VALUES (3, 4);
+ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_fkey"
+DETAIL: Key (ftest1)=(3) is not present in table "pktable".
+--
+-- MATCH FULL
+--
+-- First test, check and cascade
+--
-- Insert test data into PKTABLE
INSERT INTO PKTABLE VALUES (1, 'Test1');
INSERT INTO PKTABLE VALUES (2, 'Test2');
@@ -351,6 +382,41 @@ INSERT INTO FKTABLE VALUES (1, NULL);
ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL;
ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_ftest2_fkey"
DETAIL: MATCH FULL does not allow mixing of null and nonnull key values.
+-- Modifying other attributes of a constraint should not affect its enforceability, and vice versa
+ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE NOT VALID NOT ENFORCED;
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_ftest2_fkey DEFERRABLE INITIALLY DEFERRED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fktable_ftest1_ftest2_fkey';
+ condeferrable | condeferred | conenforced | convalidated
+---------------+-------------+-------------+--------------
+ t | t | f | f
+(1 row)
+
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_ftest2_fkey NOT ENFORCED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fktable_ftest1_ftest2_fkey';
+ condeferrable | condeferred | conenforced | convalidated
+---------------+-------------+-------------+--------------
+ t | t | f | f
+(1 row)
+
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_ftest2_fkey ENFORCED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fktable_ftest1_ftest2_fkey';
+ condeferrable | condeferred | conenforced | convalidated
+---------------+-------------+-------------+--------------
+ t | t | t | f
+(1 row)
+
+-- Can change enforceability and deferrability together
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_ftest2_fkey NOT ENFORCED NOT DEFERRABLE;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fktable_ftest1_ftest2_fkey';
+ condeferrable | condeferred | conenforced | convalidated
+---------------+-------------+-------------+--------------
+ f | f | f | f
+(1 row)
+
DROP TABLE FKTABLE;
DROP TABLE PKTABLE;
-- MATCH SIMPLE
@@ -1276,6 +1342,13 @@ INSERT INTO fktable VALUES (0, 20);
ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_fk_fkey"
DETAIL: Key (fk)=(20) is not present in table "pktable".
COMMIT;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT ENFORCED;
+BEGIN;
+-- doesn't match FK, no error.
+UPDATE pktable SET id = 10 WHERE id = 5;
+-- doesn't match PK, no error.
+INSERT INTO fktable VALUES (0, 20);
+ROLLBACK;
-- try additional syntax
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE;
-- illegal option
@@ -1283,6 +1356,14 @@ ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE INITIALLY DE
ERROR: constraint declared INITIALLY DEFERRED must be DEFERRABLE
LINE 1: ...e ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE INITIALLY ...
^
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORCED;
+ERROR: conflicting constraint properties
+LINE 1: ...fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORC...
+ ^
+CREATE TEMP TABLE fktable2 (fk int references pktable ENFORCED NOT ENFORCED);
+ERROR: multiple ENFORCED/NOT ENFORCED clauses not allowed
+LINE 1: ...ABLE fktable2 (fk int references pktable ENFORCED NOT ENFORC...
+ ^
-- test order of firing of FK triggers when several RI-induced changes need to
-- be made to the same row. This was broken by subtransaction-related
-- changes in 8.0.
@@ -1580,10 +1661,12 @@ ALTER TABLE fk_partitioned_fk DROP COLUMN fdrop1;
CREATE TABLE fk_partitioned_fk_1 (fdrop1 int, fdrop2 int, a int, fdrop3 int, b int);
ALTER TABLE fk_partitioned_fk_1 DROP COLUMN fdrop1, DROP COLUMN fdrop2, DROP COLUMN fdrop3;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FROM (0,0) TO (1000,1000);
-ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk;
+ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
+ALTER TABLE fk_partitioned_fk_2 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
CREATE TABLE fk_partitioned_fk_3 (fdrop1 int, fdrop2 int, fdrop3 int, fdrop4 int, b int, a int)
PARTITION BY HASH (a);
ALTER TABLE fk_partitioned_fk_3 DROP COLUMN fdrop1, DROP COLUMN fdrop2,
@@ -1606,10 +1689,10 @@ INSERT INTO fk_partitioned_fk_1 (a,b) VALUES (500, 501);
ERROR: insert or update on table "fk_partitioned_fk_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
DETAIL: Key (a, b)=(500, 501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (1500, 1501);
-ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_2_a_b_fkey"
DETAIL: Key (a, b)=(1500, 1501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk_2 (a,b) VALUES (1500, 1501);
-ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_2_a_b_fkey"
DETAIL: Key (a, b)=(1500, 1501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (2500, 2502);
ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
@@ -1659,6 +1742,37 @@ Indexes:
Referenced by:
TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b)
+-- Check the exsting FK trigger
+CREATE TEMP TABLE tmp_trg_info AS SELECT tgtype, tgrelid::regclass, tgconstraint
+FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+SELECT count(1) FROM tmp_trg_info;
+ count
+-------
+ 14
+(1 row)
+
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+-- No triggers
+SELECT count(1) FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+ count
+-------
+ 0
+(1 row)
+
+-- Changing it back to ENFORCED will recreate the necessary triggers.
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+-- Should be exactly the same number of triggers found as before
+SELECT COUNT(1) FROM pg_trigger pgt JOIN tmp_trg_info tt
+ON (pgt.tgtype = tt.tgtype AND pgt.tgrelid = tt.tgrelid AND pgt.tgconstraint = tt.tgconstraint);
+ count
+-------
+ 14
+(1 row)
+
ALTER TABLE fk_partitioned_fk DROP CONSTRAINT fk_partitioned_fk_a_b_fkey;
-- done.
DROP TABLE fk_notpartitioned_pk, fk_partitioned_fk;
@@ -1956,6 +2070,40 @@ Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
Foreign-key constraints:
TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
+DROP TABLE fk_partitioned_fk_2;
+CREATE TABLE fk_partitioned_fk_2 (b int, a int,
+ FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
+BEGIN;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should be having two constraints
+\d fk_partitioned_fk_2
+ Table "public.fk_partitioned_fk_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ b | integer | | |
+ a | integer | | |
+Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
+Foreign-key constraints:
+ "fk_partitioned_fk_2_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED
+ TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
+
+DROP TABLE fk_partitioned_fk_2;
+ROLLBACK;
+BEGIN;
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have only one constraint
+\d fk_partitioned_fk_2
+ Table "public.fk_partitioned_fk_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ b | integer | | |
+ a | integer | | |
+Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
+Foreign-key constraints:
+ TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED
+
+ROLLBACK;
DROP TABLE fk_partitioned_fk_2;
CREATE TABLE fk_partitioned_fk_4 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE) PARTITION BY RANGE (b, a);
CREATE TABLE fk_partitioned_fk_4_1 PARTITION OF fk_partitioned_fk_4 FOR VALUES FROM (1,1) TO (100,100);
diff --git a/src/test/regress/sql/constraints.sql b/src/test/regress/sql/constraints.sql
index d6742f83fb9..e61e7324768 100644
--- a/src/test/regress/sql/constraints.sql
+++ b/src/test/regress/sql/constraints.sql
@@ -534,7 +534,6 @@ SELECT * FROM unique_tbl;
-- enforcibility cannot be specified or set for unique constrain
CREATE TABLE UNIQUE_EN_TBL(i int UNIQUE ENFORCED);
CREATE TABLE UNIQUE_NOTEN_TBL(i int UNIQUE NOT ENFORCED);
--- XXX: error message is misleading here
ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORCED;
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index bc0adb8cfe9..408a01e377b 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -2,13 +2,39 @@
-- FOREIGN KEY
--
--- MATCH FULL
+-- NOT ENFORCED
--
-- First test, check and cascade
--
CREATE TABLE PKTABLE ( ptest1 int PRIMARY KEY, ptest2 text );
-CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE, ftest2 int );
+CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE NOT ENFORCED,
+ ftest2 int );
+-- Inserting into the foreign key table will not result in an error, even if
+-- there is no matching key in the referenced table.
+INSERT INTO FKTABLE VALUES (1, 2);
+INSERT INTO FKTABLE VALUES (2, 3);
+
+-- Check FKTABLE
+SELECT * FROM FKTABLE;
+
+-- Reverting it back to ENFORCED will result in failure because constraint validation will be triggered,
+-- as it was previously in a valid state.
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+
+-- Remove any existing rows that violate the constraint, then attempt to change
+-- it.
+TRUNCATE FKTABLE;
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+
+-- Any further inserts will fail due to the enforcement.
+INSERT INTO FKTABLE VALUES (3, 4);
+
+--
+-- MATCH FULL
+--
+-- First test, check and cascade
+--
-- Insert test data into PKTABLE
INSERT INTO PKTABLE VALUES (1, 'Test1');
INSERT INTO PKTABLE VALUES (2, 'Test2');
@@ -230,6 +256,25 @@ INSERT INTO FKTABLE VALUES (1, NULL);
ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL;
+-- Modifying other attributes of a constraint should not affect its enforceability, and vice versa
+ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE NOT VALID NOT ENFORCED;
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_ftest2_fkey DEFERRABLE INITIALLY DEFERRED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fktable_ftest1_ftest2_fkey';
+
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_ftest2_fkey NOT ENFORCED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fktable_ftest1_ftest2_fkey';
+
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_ftest2_fkey ENFORCED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fktable_ftest1_ftest2_fkey';
+
+-- Can change enforceability and deferrability together
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_ftest2_fkey NOT ENFORCED NOT DEFERRABLE;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fktable_ftest1_ftest2_fkey';
+
DROP TABLE FKTABLE;
DROP TABLE PKTABLE;
@@ -968,10 +1013,23 @@ INSERT INTO fktable VALUES (0, 20);
COMMIT;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT ENFORCED;
+
+BEGIN;
+
+-- doesn't match FK, no error.
+UPDATE pktable SET id = 10 WHERE id = 5;
+-- doesn't match PK, no error.
+INSERT INTO fktable VALUES (0, 20);
+
+ROLLBACK;
+
-- try additional syntax
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE;
-- illegal option
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORCED;
+CREATE TEMP TABLE fktable2 (fk int references pktable ENFORCED NOT ENFORCED);
-- test order of firing of FK triggers when several RI-induced changes need to
-- be made to the same row. This was broken by subtransaction-related
@@ -1182,11 +1240,12 @@ ALTER TABLE fk_partitioned_fk DROP COLUMN fdrop1;
CREATE TABLE fk_partitioned_fk_1 (fdrop1 int, fdrop2 int, a int, fdrop3 int, b int);
ALTER TABLE fk_partitioned_fk_1 DROP COLUMN fdrop1, DROP COLUMN fdrop2, DROP COLUMN fdrop3;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FROM (0,0) TO (1000,1000);
-ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk;
+ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
+ALTER TABLE fk_partitioned_fk_2 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
-
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
CREATE TABLE fk_partitioned_fk_3 (fdrop1 int, fdrop2 int, fdrop3 int, fdrop4 int, b int, a int)
PARTITION BY HASH (a);
ALTER TABLE fk_partitioned_fk_3 DROP COLUMN fdrop1, DROP COLUMN fdrop2,
@@ -1232,6 +1291,27 @@ UPDATE fk_notpartitioned_pk SET b = 1502 WHERE a = 1500;
UPDATE fk_notpartitioned_pk SET b = 2504 WHERE a = 2500;
-- check psql behavior
\d fk_notpartitioned_pk
+
+-- Check the exsting FK trigger
+CREATE TEMP TABLE tmp_trg_info AS SELECT tgtype, tgrelid::regclass, tgconstraint
+FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+SELECT count(1) FROM tmp_trg_info;
+
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+-- No triggers
+SELECT count(1) FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+
+-- Changing it back to ENFORCED will recreate the necessary triggers.
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+
+-- Should be exactly the same number of triggers found as before
+SELECT COUNT(1) FROM pg_trigger pgt JOIN tmp_trg_info tt
+ON (pgt.tgtype = tt.tgtype AND pgt.tgrelid = tt.tgrelid AND pgt.tgconstraint = tt.tgconstraint);
+
ALTER TABLE fk_partitioned_fk DROP CONSTRAINT fk_partitioned_fk_a_b_fkey;
-- done.
DROP TABLE fk_notpartitioned_pk, fk_partitioned_fk;
@@ -1439,6 +1519,22 @@ ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN
\d fk_partitioned_fk_2
DROP TABLE fk_partitioned_fk_2;
+CREATE TABLE fk_partitioned_fk_2 (b int, a int,
+ FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
+BEGIN;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should be having two constraints
+\d fk_partitioned_fk_2
+DROP TABLE fk_partitioned_fk_2;
+ROLLBACK;
+BEGIN;
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have only one constraint
+\d fk_partitioned_fk_2
+ROLLBACK;
+DROP TABLE fk_partitioned_fk_2;
+
CREATE TABLE fk_partitioned_fk_4 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE) PARTITION BY RANGE (b, a);
CREATE TABLE fk_partitioned_fk_4_1 PARTITION OF fk_partitioned_fk_4 FOR VALUES FROM (1,1) TO (100,100);
CREATE TABLE fk_partitioned_fk_4_2 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE SET NULL);
--
2.43.5
v13-0009-Merge-the-parent-and-child-constraints-with-diff.patchapplication/x-patch; name=v13-0009-Merge-the-parent-and-child-constraints-with-diff.patchDownload
From 01e576385599b56927c772eb929ef2d4c7e18a86 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 10 Feb 2025 10:59:28 +0530
Subject: [PATCH v13 9/9] Merge the parent and child constraints with differing
enforcibility.
If an enforced parent constraint is attached to a non-enforced child
constraint, the child constraint will be made enforced, with
validation applied if the parent constraint is validated as well.
Otherwise, a new enforced constraint (with validation, if the parent
constraint is validated) would need to be created on the child table,
which would be unnecessary if a similar constraint already exists and
can be attached.
On the other hand, having a not-enforced parent constraint with an
enforced child constraint does not cause any issues, and no changes
are required.
----
NOTE: This patch is intended to reduce the diff noise from the main
patch and is not meant to be committed separately. It should be
squashed with the main patch that adds ENFORCED/NOT ENFORCED.
----
---
src/backend/commands/tablecmds.c | 167 ++++++++++++++++++++--
src/test/regress/expected/foreign_key.out | 77 ++++++++--
src/test/regress/sql/foreign_key.sql | 37 ++++-
3 files changed, 252 insertions(+), 29 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 74b8ce82a18..936277237c3 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -10833,10 +10833,10 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
/*
* Tell Phase 3 to check that the constraint is satisfied by existing
* rows. We can skip this during table creation, when constraint is
- * specified as NOT ENFORCED, or when requested explicitly by specifying
- * NOT VALID in an ADD FOREIGN KEY command, and when we're recreating
- * a constraint following a SET DATA TYPE operation that did not
- * impugn its validity.
+ * specified as NOT ENFORCED, or when requested explicitly by
+ * specifying NOT VALID in an ADD FOREIGN KEY command, and when we're
+ * recreating a constraint following a SET DATA TYPE operation that
+ * did not impugn its validity.
*/
if (wqueue && !old_check_ok && !fkconstraint->skip_validation &&
fkconstraint->is_enforced)
@@ -11512,7 +11512,6 @@ tryAttachPartitionForeignKey(List **wqueue,
if (OidIsValid(partConstr->conparentid) ||
partConstr->condeferrable != parentConstr->condeferrable ||
partConstr->condeferred != parentConstr->condeferred ||
- partConstr->conenforced != parentConstr->conenforced ||
partConstr->confupdtype != parentConstr->confupdtype ||
partConstr->confdeltype != parentConstr->confdeltype ||
partConstr->confmatchtype != parentConstr->confmatchtype)
@@ -11557,6 +11556,8 @@ AttachPartitionForeignKey(List **wqueue,
Oid partConstrFrelid;
Oid partConstrRelid;
bool parentConstrIsEnforced;
+ bool partConstrIsEnforced;
+ bool partConstrParentIsSet;
/* Fetch the parent constraint tuple */
parentConstrTup = SearchSysCache1(CONSTROID,
@@ -11572,13 +11573,47 @@ AttachPartitionForeignKey(List **wqueue,
if (!HeapTupleIsValid(partcontup))
elog(ERROR, "cache lookup failed for constraint %u", partConstrOid);
partConstr = (Form_pg_constraint) GETSTRUCT(partcontup);
+ partConstrIsEnforced = partConstr->conenforced;
partConstrFrelid = partConstr->confrelid;
partConstrRelid = partConstr->conrelid;
+ /*
+ * The case where the parent constraint is not enforced and the child
+ * constraint is enforced is acceptable because the non-enforced parent
+ * constraint lacks triggers, eliminating any redundancy issues with the
+ * enforced child constraint. In this scenario, the child constraint
+ * remains enforced, and its trigger is retained, ensuring that
+ * referential integrity checks for the child continue as before, even
+ * with the parent constraint not enforced. The relationship between the
+ * two constraints is preserved by setting the parent constraint, which
+ * allows us to locate the child constraint. This becomes important if the
+ * parent constraint is later changed to enforced, at which point the
+ * necessary trigger will be created for the parent, and any redundancy
+ * from these triggers will be appropriately handled.
+ */
+ if (!parentConstrIsEnforced && partConstrIsEnforced)
+ {
+ ReleaseSysCache(partcontup);
+ ReleaseSysCache(parentConstrTup);
+
+ ConstraintSetParentConstraint(partConstrOid, parentConstrOid,
+ RelationGetRelid(partition));
+ CommandCounterIncrement();
+
+ return;
+ }
+
/*
* If the referenced table is partitioned, then the partition we're
* attaching now has extra pg_constraint rows and action triggers that are
* no longer needed. Remove those.
+ *
+ * Note that this must be done beforehand, particularly in situations
+ * where we might decide to change the constraint to an enforced state
+ * which will create the required triggers and add the child constraint to
+ * the validation queue. To avoid generating unnecessary triggers and
+ * adding them to the validation queue, it is crucial to eliminate any
+ * redundant constraints beforehand.
*/
if (get_rel_relkind(partConstrFrelid) == RELKIND_PARTITIONED_TABLE)
{
@@ -11590,6 +11625,46 @@ AttachPartitionForeignKey(List **wqueue,
table_close(pg_constraint, RowShareLock);
}
+ /*
+ * The case where the parent constraint is enforced and the child
+ * constraint is not enforced is not acceptable, as it would violate
+ * referential integrity. In such cases, the child constraint will first
+ * be enforced before merging it with the enforced parent constraint.
+ * Subsequently, removing action triggers, setting up constraint triggers,
+ * and handling check triggers for the parent will be managed in the usual
+ * manner, similar to how two enforced constraints are merged.
+ */
+ if (parentConstrIsEnforced && !partConstrIsEnforced)
+ {
+ AlterConstraintStmt *cmdcon = makeNode(AlterConstraintStmt);
+ Relation conrel;
+ List *otherrelids = NIL;
+
+ cmdcon->conname = NameStr(partConstr->conname);
+ cmdcon->deferrable = partConstr->condeferrable;
+ cmdcon->initdeferred = partConstr->condeferred;
+ cmdcon->alterEnforceability = true;
+ cmdcon->is_enforced = true;
+
+ conrel = table_open(ConstraintRelationId, RowExclusiveLock);
+
+ ATExecAlterConstrRecurse(wqueue, cmdcon, conrel, trigrel,
+ partConstr->conrelid, partConstr->confrelid,
+ partcontup, &otherrelids, AccessExclusiveLock,
+ InvalidOid, InvalidOid, InvalidOid, InvalidOid);
+
+ table_close(conrel, RowExclusiveLock);
+
+ CommandCounterIncrement();
+ }
+
+ /*
+ * The constraint parent shouldn't be set beforehand, or if it's already
+ * set, it should be the specified parent.
+ */
+ partConstrParentIsSet = OidIsValid(partConstr->conparentid);
+ Assert(!partConstrParentIsSet || partConstr->conparentid == parentConstrOid);
+
/*
* Will we need to validate this constraint? A valid parent constraint
* implies that all child constraints have been validated, so if this one
@@ -11609,8 +11684,10 @@ AttachPartitionForeignKey(List **wqueue,
DropForeignKeyConstraintTriggers(trigrel, partConstrOid, partConstrFrelid,
partConstrRelid);
- ConstraintSetParentConstraint(partConstrOid, parentConstrOid,
- RelationGetRelid(partition));
+ /* Skip if the parent is already set */
+ if (!partConstrParentIsSet)
+ ConstraintSetParentConstraint(partConstrOid, parentConstrOid,
+ RelationGetRelid(partition));
/*
* Like the constraint, attach partition's "check" triggers to the
@@ -12169,6 +12246,7 @@ ATExecAlterConstrRecurse(List **wqueue, AlterConstraintStmt *cmdcon,
ReferencedParentUpdTrigger,
ReferencingParentInsTrigger,
ReferencingParentUpdTrigger);
+
/*
* Tell Phase 3 to check that the constraint is satisfied by existing
* rows, but skip this step if the constraint is NOT VALID.
@@ -12273,6 +12351,17 @@ ATExecAlterConstrEnforceability(List **wqueue, AlterConstraintStmt *cmdcon,
/* Drop all the triggers */
DropForeignKeyConstraintTriggers(tgrel, conoid, InvalidOid, InvalidOid);
+
+ /*
+ * If the referenced table is partitioned, the child constraint we're
+ * changing to not-enforced may have additional pg_constraint rows and
+ * action triggers that remain untouched while this child constraint
+ * is attached to the not-enforced parent. These must now be removed.
+ * For more details, see AttachPartitionForeignKey().
+ */
+ if (OidIsValid(currcon->conparentid) &&
+ get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE)
+ RemoveInheritedConstraint(conrel, tgrel, currcon->conrelid, conoid);
}
else /* Create triggers */
{
@@ -12333,13 +12422,41 @@ ATExecAlterConstrEnforceability(List **wqueue, AlterConstraintStmt *cmdcon,
true, NULL, 1, &pkey);
while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
- ATExecAlterConstrRecurse(wqueue, cmdcon, conrel, tgrel, fkrelid,
- pkrelid, childtup, otherrelids,
- lockmode, ReferencedDelTriggerOid,
- ReferencedUpdTriggerOid,
- ReferencingInsTriggerOid,
- ReferencingUpdTriggerOid);
+ {
+ Form_pg_constraint childcon;
+ childcon = (Form_pg_constraint) GETSTRUCT(childtup);
+
+ if (childcon->conenforced)
+ {
+ /*
+ * The child constraint is attached to the parent
+ * constraint, which is already enforced. Now, as the
+ * parent constraint is being modified to be enforced,
+ * some constraints and action triggers on the child table
+ * may become redundant and need to be removed.
+ */
+ if (currcon->confrelid == pkrelid)
+ {
+ Relation rel = table_open(childcon->conrelid, lockmode);
+
+ AttachPartitionForeignKey(wqueue, rel, childcon->oid,
+ conoid,
+ ReferencingInsTriggerOid,
+ ReferencingUpdTriggerOid,
+ tgrel);
+
+ table_close(rel, NoLock);
+ }
+ }
+ else
+ ATExecAlterConstrRecurse(wqueue, cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, childtup, otherrelids,
+ lockmode, ReferencedDelTriggerOid,
+ ReferencedUpdTriggerOid,
+ ReferencingInsTriggerOid,
+ ReferencingUpdTriggerOid);
+ }
systable_endscan(pscan);
}
}
@@ -20512,7 +20629,9 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
{
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
+ HeapTuple parentContup;
Form_pg_constraint conform;
+ Oid parentConstrIsEnforced;
contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
if (!HeapTupleIsValid(contup))
@@ -20531,12 +20650,34 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
continue;
}
+ /* Get the enforcibility of the parent constraint */
+ parentContup = SearchSysCache1(CONSTROID,
+ ObjectIdGetDatum(conform->conparentid));
+ if (!HeapTupleIsValid(parentContup))
+ elog(ERROR, "cache lookup failed for constraint %u",
+ conform->conparentid);
+ parentConstrIsEnforced =
+ ((Form_pg_constraint) GETSTRUCT(parentContup))->conenforced;
+ ReleaseSysCache(parentContup);
+
/*
* The constraint on this table must be marked no longer a child of
* the parent's constraint, as do its check triggers.
*/
ConstraintSetParentConstraint(fk->conoid, InvalidOid, InvalidOid);
+ /*
+ * Unsetting the parent is sufficient when the parent constraint is
+ * not enforced and the child constraint is enforced, as we link them
+ * by setting the constraint parent, while leaving the rest unchanged.
+ * For more details, see AttachPartitionForeignKey().
+ */
+ if (!parentConstrIsEnforced && fk->conenforced)
+ {
+ ReleaseSysCache(contup);
+ continue;
+ }
+
/*
* Also, look up the partition's "check" triggers corresponding to the
* enforced constraint being detached and detach them from the parent
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 3183b1cbe8f..c7ccadab26f 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -1664,7 +1664,6 @@ ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FR
ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
-ALTER TABLE fk_partitioned_fk_2 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
CREATE TABLE fk_partitioned_fk_3 (fdrop1 int, fdrop2 int, fdrop3 int, fdrop4 int, b int, a int)
@@ -1673,8 +1672,65 @@ ALTER TABLE fk_partitioned_fk_3 DROP COLUMN fdrop1, DROP COLUMN fdrop2,
DROP COLUMN fdrop3, DROP COLUMN fdrop4;
CREATE TABLE fk_partitioned_fk_3_0 PARTITION OF fk_partitioned_fk_3 FOR VALUES WITH (MODULUS 5, REMAINDER 0);
CREATE TABLE fk_partitioned_fk_3_1 PARTITION OF fk_partitioned_fk_3 FOR VALUES WITH (MODULUS 5, REMAINDER 1);
+-- Merge the not-enforced parent constraint with the enforced and not-enforced child constraints
+ALTER TABLE fk_partitioned_fk_3_0 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ENFORCED;
+ALTER TABLE fk_partitioned_fk_3_1 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk_3 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+-- Merge the non-enforced parent constraint with both the enforced and
+-- non-enforced child constraints, where the referenced table is partitioned.
+CREATE TABLE fk_partitioned_pk (a int, b int, PRIMARY KEY (a, b)) PARTITION BY RANGE (a, b);
+CREATE TABLE fk_partitioned_pk_1 PARTITION OF fk_partitioned_pk FOR VALUES FROM (0,0) TO (1000,1000);
+-- Merge the enforced parent constraint with the enforced and not-enforced child constraints.
+ALTER TABLE fk_partitioned_fk_3_0 ADD FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk ENFORCED;
+ALTER TABLE fk_partitioned_fk_3_1 ADD FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk_3 ADD FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk ENFORCED;
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+ conname | conenforced | convalidated | conrelid | confrelid
+---------------------------------+-------------+--------------+-----------------------+----------------------
+ fk_partitioned_fk_3_0_a_b_fkey | t | t | fk_partitioned_fk_3_0 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey | f | t | fk_partitioned_fk_3_1 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_a_b_fkey | f | t | fk_partitioned_fk_3 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_0_a_b_fkey1 | t | t | fk_partitioned_fk_3_0 | fk_partitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey1 | t | t | fk_partitioned_fk_3_1 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey1 | t | t | fk_partitioned_fk_3 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey2 | t | t | fk_partitioned_fk_3 | fk_partitioned_pk_1
+(7 rows)
+
+ALTER TABLE fk_partitioned_fk_3 DETACH PARTITION fk_partitioned_fk_3_0;
+ALTER TABLE fk_partitioned_fk_3 ATTACH PARTITION fk_partitioned_fk_3_0 FOR VALUES WITH (MODULUS 5, REMAINDER 0);
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+ conname | conenforced | convalidated | conrelid | confrelid
+---------------------------------+-------------+--------------+-----------------------+----------------------
+ fk_partitioned_fk_3_0_a_b_fkey | t | t | fk_partitioned_fk_3_0 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey | f | t | fk_partitioned_fk_3_1 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_a_b_fkey | f | t | fk_partitioned_fk_3 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_0_a_b_fkey1 | t | t | fk_partitioned_fk_3_0 | fk_partitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey1 | t | t | fk_partitioned_fk_3_1 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey1 | t | t | fk_partitioned_fk_3 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey2 | t | t | fk_partitioned_fk_3 | fk_partitioned_pk_1
+(7 rows)
+
+ALTER TABLE fk_partitioned_fk_3 ALTER CONSTRAINT fk_partitioned_fk_3_a_b_fkey1 NOT ENFORCED;
+-- Merging an enforced parent constraint (validated) with a not-enforced child
+-- constraint will implicitly change the child constraint to enforced and apply
+-- the validation as well.
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_3
FOR VALUES FROM (2000,2000) TO (3000,3000);
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+ conname | conenforced | convalidated | conrelid | confrelid
+---------------------------------+-------------+--------------+-----------------------+----------------------
+ fk_partitioned_fk_3_0_a_b_fkey | t | t | fk_partitioned_fk_3_0 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey | t | t | fk_partitioned_fk_3_1 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_a_b_fkey | t | t | fk_partitioned_fk_3 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_0_a_b_fkey1 | f | t | fk_partitioned_fk_3_0 | fk_partitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey1 | f | t | fk_partitioned_fk_3_1 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey1 | f | t | fk_partitioned_fk_3 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey2 | f | t | fk_partitioned_fk_3 | fk_partitioned_pk_1
+(7 rows)
+
-- Creating a foreign key with ONLY on a partitioned table referencing
-- a non-partitioned table fails.
ALTER TABLE ONLY fk_partitioned_fk ADD FOREIGN KEY (a, b)
@@ -1689,22 +1745,22 @@ INSERT INTO fk_partitioned_fk_1 (a,b) VALUES (500, 501);
ERROR: insert or update on table "fk_partitioned_fk_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
DETAIL: Key (a, b)=(500, 501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (1500, 1501);
-ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_2_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
DETAIL: Key (a, b)=(1500, 1501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk_2 (a,b) VALUES (1500, 1501);
-ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_2_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
DETAIL: Key (a, b)=(1500, 1501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (2500, 2502);
-ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_3_1_a_b_fkey"
DETAIL: Key (a, b)=(2500, 2502) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk_3 (a,b) VALUES (2500, 2502);
-ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_3_1_a_b_fkey"
DETAIL: Key (a, b)=(2500, 2502) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (2501, 2503);
-ERROR: insert or update on table "fk_partitioned_fk_3_0" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_0" violates foreign key constraint "fk_partitioned_fk_3_0_a_b_fkey"
DETAIL: Key (a, b)=(2501, 2503) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk_3 (a,b) VALUES (2501, 2503);
-ERROR: insert or update on table "fk_partitioned_fk_3_0" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_0" violates foreign key constraint "fk_partitioned_fk_3_0_a_b_fkey"
DETAIL: Key (a, b)=(2501, 2503) is not present in table "fk_notpartitioned_pk".
-- but if we insert the values that make them valid, then they work
INSERT INTO fk_notpartitioned_pk VALUES (500, 501), (1500, 1501),
@@ -1715,7 +1771,7 @@ INSERT INTO fk_partitioned_fk (a,b) VALUES (2500, 2502);
INSERT INTO fk_partitioned_fk (a,b) VALUES (2501, 2503);
-- this update fails because there is no referenced row
UPDATE fk_partitioned_fk SET a = a + 1 WHERE a = 2501;
-ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_3_1_a_b_fkey"
DETAIL: Key (a, b)=(2502, 2503) is not present in table "fk_notpartitioned_pk".
-- but we can fix it thusly:
INSERT INTO fk_notpartitioned_pk (a,b) VALUES (2502, 2503);
@@ -1858,8 +1914,6 @@ WHERE conrelid::regclass::text like 'fk_partitioned_fk%' ORDER BY oid::regclass:
DROP TABLE fk_partitioned_fk, fk_notpartitioned_pk;
-- NOT VALID foreign key on a non-partitioned table referencing a partitioned table
-CREATE TABLE fk_partitioned_pk (a int, b int, PRIMARY KEY (a, b)) PARTITION BY RANGE (a, b);
-CREATE TABLE fk_partitioned_pk_1 PARTITION OF fk_partitioned_pk FOR VALUES FROM (0,0) TO (1000,1000);
CREATE TABLE fk_notpartitioned_fk (b int, a int);
ALTER TABLE fk_notpartitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk NOT VALID;
-- Constraint will be invalid.
@@ -2075,7 +2129,7 @@ CREATE TABLE fk_partitioned_fk_2 (b int, a int,
FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
BEGIN;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
--- should be having two constraints
+-- should only have one constraint
\d fk_partitioned_fk_2
Table "public.fk_partitioned_fk_2"
Column | Type | Collation | Nullable | Default
@@ -2084,7 +2138,6 @@ ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN
a | integer | | |
Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
Foreign-key constraints:
- "fk_partitioned_fk_2_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED
TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
DROP TABLE fk_partitioned_fk_2;
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index 408a01e377b..2188e56ca3c 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -1243,18 +1243,49 @@ ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FR
ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
-ALTER TABLE fk_partitioned_fk_2 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+
CREATE TABLE fk_partitioned_fk_3 (fdrop1 int, fdrop2 int, fdrop3 int, fdrop4 int, b int, a int)
PARTITION BY HASH (a);
ALTER TABLE fk_partitioned_fk_3 DROP COLUMN fdrop1, DROP COLUMN fdrop2,
DROP COLUMN fdrop3, DROP COLUMN fdrop4;
CREATE TABLE fk_partitioned_fk_3_0 PARTITION OF fk_partitioned_fk_3 FOR VALUES WITH (MODULUS 5, REMAINDER 0);
CREATE TABLE fk_partitioned_fk_3_1 PARTITION OF fk_partitioned_fk_3 FOR VALUES WITH (MODULUS 5, REMAINDER 1);
+-- Merge the not-enforced parent constraint with the enforced and not-enforced child constraints
+ALTER TABLE fk_partitioned_fk_3_0 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ENFORCED;
+ALTER TABLE fk_partitioned_fk_3_1 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk_3 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+
+-- Merge the non-enforced parent constraint with both the enforced and
+-- non-enforced child constraints, where the referenced table is partitioned.
+CREATE TABLE fk_partitioned_pk (a int, b int, PRIMARY KEY (a, b)) PARTITION BY RANGE (a, b);
+CREATE TABLE fk_partitioned_pk_1 PARTITION OF fk_partitioned_pk FOR VALUES FROM (0,0) TO (1000,1000);
+-- Merge the enforced parent constraint with the enforced and not-enforced child constraints.
+ALTER TABLE fk_partitioned_fk_3_0 ADD FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk ENFORCED;
+ALTER TABLE fk_partitioned_fk_3_1 ADD FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk_3 ADD FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk ENFORCED;
+
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+
+ALTER TABLE fk_partitioned_fk_3 DETACH PARTITION fk_partitioned_fk_3_0;
+ALTER TABLE fk_partitioned_fk_3 ATTACH PARTITION fk_partitioned_fk_3_0 FOR VALUES WITH (MODULUS 5, REMAINDER 0);
+
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+
+ALTER TABLE fk_partitioned_fk_3 ALTER CONSTRAINT fk_partitioned_fk_3_a_b_fkey1 NOT ENFORCED;
+
+-- Merging an enforced parent constraint (validated) with a not-enforced child
+-- constraint will implicitly change the child constraint to enforced and apply
+-- the validation as well.
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_3
FOR VALUES FROM (2000,2000) TO (3000,3000);
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+
-- Creating a foreign key with ONLY on a partitioned table referencing
-- a non-partitioned table fails.
ALTER TABLE ONLY fk_partitioned_fk ADD FOREIGN KEY (a, b)
@@ -1381,8 +1412,6 @@ WHERE conrelid::regclass::text like 'fk_partitioned_fk%' ORDER BY oid::regclass:
DROP TABLE fk_partitioned_fk, fk_notpartitioned_pk;
-- NOT VALID foreign key on a non-partitioned table referencing a partitioned table
-CREATE TABLE fk_partitioned_pk (a int, b int, PRIMARY KEY (a, b)) PARTITION BY RANGE (a, b);
-CREATE TABLE fk_partitioned_pk_1 PARTITION OF fk_partitioned_pk FOR VALUES FROM (0,0) TO (1000,1000);
CREATE TABLE fk_notpartitioned_fk (b int, a int);
ALTER TABLE fk_notpartitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk NOT VALID;
@@ -1523,7 +1552,7 @@ CREATE TABLE fk_partitioned_fk_2 (b int, a int,
FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
BEGIN;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
--- should be having two constraints
+-- should only have one constraint
\d fk_partitioned_fk_2
DROP TABLE fk_partitioned_fk_2;
ROLLBACK;
--
2.43.5
On Mon, Feb 10, 2025, at 7:03 AM, Amul Sul wrote:
Attached patch implemented this behaviour. To achieve this, we have to
revert (see 0007) some committed code and relax the restriction that
the NOT ENFORCED constraint must also be NOT VALID. Now, NOT ENFORCED
and NOT VALID are independent statuses, and the psql-meta meta command
will display NOT VALID alongside the NOT ENFORCED constraint.
Previously, we hid NOT VALID for NOT ENFORCED constraints, assuming it
would be implied, but that is no longer the case.
I think this proposed state of affairs is problematic. Current queries that assume that pg_constraint.convalidated means that a constraint is validated would be broken. My suggestion at this point is that instead of adding a separate boolean column to pg_constraint we should be replacing `bool convalidated` with `char convalidity`, with new defines for all the possible states we require: enforced-and-valid ("V"alid), enforced-not-validated ("i"nvalid), not-enforced-and-not-valid (terribly "I"nvalid or maybe "U"nenforced), not-enforced-but-was-valid-before-turning-unenforced ("u"nenforced). Breaking user queries would make all apps reassess what do they actually want to know about the constraint without assumptions of how enforcement worked in existing Postgres releases.
This shouldn't be a very large change from what you currently have, I think.
• v13-0001-Add-AlterConstraintStmt-struct-for-ALTER-.-CONST.patch
I think the new node name is wrong, because it makes the code looks as if we support ALTER CONSTRAINT as a statement for this, which is false. (This is a repeat of ReplicaIdentityStmt, which I think is a mistake). I would suggest a name like ATAlterConstraint instead. Perhaps we can use that in the patch for ALTER CONSTRAINT ... SET [NO] INHERIT as well, instead of (as I suggested Suraj) creating a separate subcommand number.
On Mon, 10 Feb 2025 at 13:48, Álvaro Herrera <alvherre@alvh.no-ip.org>
wrote:
I think this proposed state of affairs is problematic. Current queries
that assume that pg_constraint.convalidated means that a constraint is
validated would be broken. My suggestion at this point is that instead of
adding a separate boolean column to pg_constraint we should be replacing
`bool convalidated` with `char convalidity`, with new defines for all the
possible states we require: enforced-and-valid ("V"alid),
enforced-not-validated ("i"nvalid), not-enforced-and-not-valid (terribly
"I"nvalid or maybe "U"nenforced),
not-enforced-but-was-valid-before-turning-unenforced ("u"nenforced).
Breaking user queries would make all apps reassess what do they actually
want to know about the constraint without assumptions of how enforcement
worked in existing Postgres releases.
I'm having a lot of trouble understanding the operational distinction
between your 'u' and 'U'. If it's not enforced, it cannot be assumed to be
valid, regardless of whether it was valid in the past. I'm not sure what I
think of a single character vs. 2 booleans, but there are only 3 sensible
states either way: valid enforced, invalid enforced, and invalid unenforced.
Additionally, if there are officially 4 status possibilities then all code
that looks for unenforced constraints has to look for both valid and
invalid unenforced constraints if we use a char; it's not as bad with 2
booleans because one can just check the "enforced" boolean.
On 2025-Feb-10, Isaac Morland wrote:
I'm having a lot of trouble understanding the operational distinction
between your 'u' and 'U'. If it's not enforced, it cannot be assumed to be
valid, regardless of whether it was valid in the past. I'm not sure what I
think of a single character vs. 2 booleans, but there are only 3 sensible
states either way: valid enforced, invalid enforced, and invalid unenforced.
I kinda agree with you and would prefer that things were that way as
well. But look at the discussion starting at
/messages/by-id/CAExHW5tV23Sw+Nznv0KpdNg_t7LrXY1WM9atiC=eKKSsKHSnuQ@mail.gmail.com
whereby it was apparently established that if you have a
NOT VALID NOT ENFORCED
constraint, and you make it enforced, then you should somehow end up
with a NOT VALID ENFORCED constraint, which says to me that we need to
store the fact that the constraint was NOT VALID to start with; and
correspondingly if it's VALID NOT ENFORCED and you enforce it, then it
ends up VALID ENFORCED. If we take this view of the world (with which,
I repeat, I disagree) then we must keep track of whether the constraint
was valid or not valid to start with. And this means that we need to
keep convalidated=true _regardless_ of whether conenforced is false.
So in this view of the world there aren't three states but four.
I would prefer there to be three states as well, but apparently I'm
outvoted on this.
Additionally, if there are officially 4 status possibilities then all code
that looks for unenforced constraints has to look for both valid and
invalid unenforced constraints if we use a char; it's not as bad with 2
booleans because one can just check the "enforced" boolean.
Well, yes. You have kinda the same issue with any other system catalog
column that's a 'char', I guess. Maybe this is more of a problem here
because it's more user-visible than most other catalogs, not sure.
--
Álvaro Herrera Breisgau, Deutschland — https://www.EnterpriseDB.com/
On Tue, 11 Feb 2025 at 08:36, Álvaro Herrera <alvherre@alvh.no-ip.org>
wrote:
On 2025-Feb-10, Isaac Morland wrote:
I'm having a lot of trouble understanding the operational distinction
between your 'u' and 'U'. If it's not enforced, it cannot be assumed tobe
valid, regardless of whether it was valid in the past. I'm not sure what
I
think of a single character vs. 2 booleans, but there are only 3 sensible
states either way: valid enforced, invalid enforced, and invalidunenforced.
I kinda agree with you and would prefer that things were that way as
well. But look at the discussion starting at/messages/by-id/CAExHW5tV23Sw+Nznv0KpdNg_t7LrXY1WM9atiC=eKKSsKHSnuQ@mail.gmail.com
whereby it was apparently established that if you have a
NOT VALID NOT ENFORCED
constraint, and you make it enforced, then you should somehow end up
with a NOT VALID ENFORCED constraint, which says to me that we need to
store the fact that the constraint was NOT VALID to start with; and
correspondingly if it's VALID NOT ENFORCED and you enforce it, then it
ends up VALID ENFORCED. If we take this view of the world (with which,
I repeat, I disagree) then we must keep track of whether the constraint
was valid or not valid to start with. And this means that we need to
keep convalidated=true _regardless_ of whether conenforced is false.
So in this view of the world there aren't three states but four.I would prefer there to be three states as well, but apparently I'm
outvoted on this.
Sounds like we agree. I think the problem is with the statement in the
linked discussion that “If the constraint is VALID and later marked as NOT
ENFORCED, changing it to ENFORCED should also keep it VALID.” This ignores
that if it is changed to NOT ENFORCED that should immediately change it to
NOT VALID if it is not already so.
Has anybody argued for how it makes any sense at all to have a constraint
that is VALID (and therefore will be assumed to be true by the planner),
yet NOT ENFORCED (and therefore may well not be true)? What next, a patch
to the planner so that it only treats as true constraints that are both
VALID and ENFORCED?
Re: the 3 or 4 values for the single character status, there is a similar
issue with relkind, where one can imagine writing "relkind IN ('r')" when
one meant "relkind IN ('r', 'v')" or something else; but on the other hand,
one can easily imagine actually wanting the first one of those. But here,
it's not at all clear to me when you would ever want to distinguish between
'u' and 'U', but it is clear to me that it would be natural to write "… =
'U'" when one actually needs to write "… IN ('u', 'U')", or perhaps "…
ILIKE 'u'" (not what I would want to see).
On Tue, Feb 11, 2025 at 9:09 PM Isaac Morland <isaac.morland@gmail.com> wrote:
On Tue, 11 Feb 2025 at 08:36, Álvaro Herrera <alvherre@alvh.no-ip.org> wrote:
On 2025-Feb-10, Isaac Morland wrote:
I'm having a lot of trouble understanding the operational distinction
between your 'u' and 'U'. If it's not enforced, it cannot be assumed to be
valid, regardless of whether it was valid in the past. I'm not sure what I
think of a single character vs. 2 booleans, but there are only 3 sensible
states either way: valid enforced, invalid enforced, and invalid unenforced.I kinda agree with you and would prefer that things were that way as
well. But look at the discussion starting at
/messages/by-id/CAExHW5tV23Sw+Nznv0KpdNg_t7LrXY1WM9atiC=eKKSsKHSnuQ@mail.gmail.com
whereby it was apparently established that if you have a
NOT VALID NOT ENFORCED
constraint, and you make it enforced, then you should somehow end up
with a NOT VALID ENFORCED constraint, which says to me that we need to
store the fact that the constraint was NOT VALID to start with; and
correspondingly if it's VALID NOT ENFORCED and you enforce it, then it
ends up VALID ENFORCED. If we take this view of the world (with which,
I repeat, I disagree) then we must keep track of whether the constraint
was valid or not valid to start with. And this means that we need to
keep convalidated=true _regardless_ of whether conenforced is false.
So in this view of the world there aren't three states but four.I would prefer there to be three states as well, but apparently I'm
outvoted on this.Sounds like we agree. I think the problem is with the statement in the linked discussion that “If the constraint is VALID and later marked as NOT ENFORCED, changing it to ENFORCED should also keep it VALID.” This ignores that if it is changed to NOT ENFORCED that should immediately change it to NOT VALID if it is not already so.
Has anybody argued for how it makes any sense at all to have a constraint that is VALID (and therefore will be assumed to be true by the planner), yet NOT ENFORCED (and therefore may well not be true)? What next, a patch to the planner so that it only treats as true constraints that are both VALID and ENFORCED?
I have been asking a different question: What's the use of
not-enforced constraints if we don't allow VALID, NOT ENFORCED state
for them? OTOH, consider an application which "knows" that the
constraint is valid for the data (either because of checks at
application level, or because the data was replicated from some other
system where the cosntraints were applied). It's a natural ask to use
the constraints for, say optimization, but don't take unnecessary
overhead of validating them. VALID, NOT ENFORCED state helps in such a
scenario. Of course an application can misuse it (just like stable
marking on a function), but well ... they will be penalised for their
misuse.
--
Best Wishes,
Ashutosh Bapat
On 2025-Feb-12, Ashutosh Bapat wrote:
I have been asking a different question: What's the use of
not-enforced constraints if we don't allow VALID, NOT ENFORCED state
for them?
That's a question for the SQL standards committee. They may serve
schema documentation purposes, for example.
https://www.postgresql.eu/events/pgconfeu2024/schedule/session/5677-exploring-postgres-databases-with-graphs/
OTOH, consider an application which "knows" that the constraint is
valid for the data (either because of checks at application level, or
because the data was replicated from some other system where the
cosntraints were applied). It's a natural ask to use the constraints
for, say optimization, but don't take unnecessary overhead of
validating them. VALID, NOT ENFORCED state helps in such a scenario.
Of course an application can misuse it (just like stable marking on a
function), but well ... they will be penalised for their misuse.
I disagree that we should see a VALID NOT ENFORCED constraint as one
that can be used for query optimization purposes. This is only going to
bring users pain, because it's far too easy to misuse and they will get
wrong query results, possibly without knowing for who knows how long.
--
Álvaro Herrera PostgreSQL Developer — https://www.EnterpriseDB.com/
"El número de instalaciones de UNIX se ha elevado a 10,
y se espera que este número aumente" (UPM, 1972)
On 11.02.25 14:36, Álvaro Herrera wrote:
On 2025-Feb-10, Isaac Morland wrote:
I'm having a lot of trouble understanding the operational distinction
between your 'u' and 'U'. If it's not enforced, it cannot be assumed to be
valid, regardless of whether it was valid in the past. I'm not sure what I
think of a single character vs. 2 booleans, but there are only 3 sensible
states either way: valid enforced, invalid enforced, and invalid unenforced.I kinda agree with you and would prefer that things were that way as
well. But look at the discussion starting at
/messages/by-id/CAExHW5tV23Sw+Nznv0KpdNg_t7LrXY1WM9atiC=eKKSsKHSnuQ@mail.gmail.com
whereby it was apparently established that if you have a
NOT VALID NOT ENFORCED
constraint, and you make it enforced, then you should somehow end up
with a NOT VALID ENFORCED constraint, which says to me that we need to
store the fact that the constraint was NOT VALID to start with; and
correspondingly if it's VALID NOT ENFORCED and you enforce it, then it
ends up VALID ENFORCED. If we take this view of the world (with which,
I repeat, I disagree) then we must keep track of whether the constraint
was valid or not valid to start with. And this means that we need to
keep convalidated=true _regardless_ of whether conenforced is false.
So in this view of the world there aren't three states but four.I would prefer there to be three states as well, but apparently I'm
outvoted on this.
Just to make this a bit more confusing, here is another interpretation
of the state NOT ENFORCED VALID (they call it DISABLE VALIDATE):
"""
DISABLE VALIDATE disables the constraint and drops the index on the
constraint, but keeps the constraint valid. This feature is most useful
in data warehousing situations, because it lets you load large amounts
of data while also saving space by not having an index. This setting
lets you load data from a nonpartitioned table into a partitioned table
using the exchange_partition_subpart clause of the ALTER TABLE statement
or using SQL*Loader. All other modifications to the table (inserts,
updates, and deletes) by other SQL statements are disallowed.
"""
On 12.02.25 12:13, Álvaro Herrera wrote:
On 2025-Feb-12, Ashutosh Bapat wrote:
I have been asking a different question: What's the use of
not-enforced constraints if we don't allow VALID, NOT ENFORCED state
for them?That's a question for the SQL standards committee. They may serve
schema documentation purposes, for example.
https://www.postgresql.eu/events/pgconfeu2024/schedule/session/5677-exploring-postgres-databases-with-graphs/OTOH, consider an application which "knows" that the constraint is
valid for the data (either because of checks at application level, or
because the data was replicated from some other system where the
cosntraints were applied). It's a natural ask to use the constraints
for, say optimization, but don't take unnecessary overhead of
validating them. VALID, NOT ENFORCED state helps in such a scenario.
Of course an application can misuse it (just like stable marking on a
function), but well ... they will be penalised for their misuse.I disagree that we should see a VALID NOT ENFORCED constraint as one
that can be used for query optimization purposes. This is only going to
bring users pain, because it's far too easy to misuse and they will get
wrong query results, possibly without knowing for who knows how long.
I've been digging into the ISO archives for some more background on the
intended meaning of this feature.
Result: "NOT ENFORCED" just means "off" or "disabled", "could contain
anything". You can use this to do data loads, or schema surgery, or
things like that. Or just if you want it for documentation.
This idea that a not-enforced constraint should contain valid data
anyway is not supported by anything I could find written down. I've
heard that in discussions, but those could have been speculations.
(I still think that could be a feature, but it's clearly not this one,
at least not in its default state.)
So considering that, I think a three-state system makes more sense.
Something like:
1) NOT ENFORCED -- no data is checked
2) NOT VALID -- existing data is unchecked, new data is checked
3) ENFORCED -- all data is checked
Transitions:
(1) - [ ALTER TABLE ... ALTER CONSTRAINT ... NOT VALID ] -> (2)
(1) - [ ALTER TABLE ... ALTER CONSTRAINT ... ENFORCED ] -> (3)
(2) - [ ALTER TABLE ... VALIDATE CONSTRAINT ... ] -> (3)
(2|3) - [ ALTER TABLE ... ALTER CONSTRAINT ... NOT ENFORCED ] -> (1)
(3) - [ ALTER TABLE ... ALTER CONSTRAINT ... NOT VALID ] -> (2)
On Wed, Feb 12, 2025 at 8:15 PM Peter Eisentraut <peter@eisentraut.org> wrote:
On 12.02.25 12:13, Álvaro Herrera wrote:
On 2025-Feb-12, Ashutosh Bapat wrote:
I have been asking a different question: What's the use of
not-enforced constraints if we don't allow VALID, NOT ENFORCED state
for them?That's a question for the SQL standards committee. They may serve
schema documentation purposes, for example.
https://www.postgresql.eu/events/pgconfeu2024/schedule/session/5677-exploring-postgres-databases-with-graphs/OTOH, consider an application which "knows" that the constraint is
valid for the data (either because of checks at application level, or
because the data was replicated from some other system where the
cosntraints were applied). It's a natural ask to use the constraints
for, say optimization, but don't take unnecessary overhead of
validating them. VALID, NOT ENFORCED state helps in such a scenario.
Of course an application can misuse it (just like stable marking on a
function), but well ... they will be penalised for their misuse.I disagree that we should see a VALID NOT ENFORCED constraint as one
that can be used for query optimization purposes. This is only going to
bring users pain, because it's far too easy to misuse and they will get
wrong query results, possibly without knowing for who knows how long.I've been digging into the ISO archives for some more background on the
intended meaning of this feature.Result: "NOT ENFORCED" just means "off" or "disabled", "could contain
anything". You can use this to do data loads, or schema surgery, or
things like that. Or just if you want it for documentation.
Hmm, so one can convert an enforced constraint to a not-enforced
constraint, load the data or make changes and then enforce it again.
Makes sense.
This idea that a not-enforced constraint should contain valid data
anyway is not supported by anything I could find written down. I've
heard that in discussions, but those could have been speculations.(I still think that could be a feature, but it's clearly not this one,
at least not in its default state.)
Thanks for the background.
So considering that, I think a three-state system makes more sense.
Something like:1) NOT ENFORCED -- no data is checked
2) NOT VALID -- existing data is unchecked, new data is checked
3) ENFORCED -- all data is checkedTransitions:
(1) - [ ALTER TABLE ... ALTER CONSTRAINT ... NOT VALID ] -> (2)
Per your notation, this means the the constraint is not enforced but
new data is checked - that seems a contradiction, how would we check
the data when the constraint is not being enforced. Or do you suggest
that we convert a NOT ENFORCED constraint to ENFORCED as a result of
converting it to NOT VALID?
(1) - [ ALTER TABLE ... ALTER CONSTRAINT ... ENFORCED ] -> (3)
Seems ok.
(2) - [ ALTER TABLE ... VALIDATE CONSTRAINT ... ] -> (3)
As a result of this a not enforced constraint would turn into an
enforced constraint. The user might have intended to just validate the
data but not enforce it to avoid paying price for the checks on new
data.
(2|3) - [ ALTER TABLE ... ALTER CONSTRAINT ... NOT ENFORCED ] -> (1)
Looks fine.
(3) - [ ALTER TABLE ... ALTER CONSTRAINT ... NOT VALID ] -> (2)
This too seems ok assuming the constraint would remain enforced.
I think, what you intend to say is clearer with 4 state system {NE, E}
* {NV, V} = {(NE, NV), (NE, V), (E, NV), (E, V)} where (NE, V) is
unreachable. Let's name them S1, S2, S3, S4 respectively.
S1 -> [ ALTER TABLE ... ALTER CONSTRAINT ... NOT VALID ] -> S1 - noop
S3 -> [ ALTER TABLE ... ALTER CONSTRAINT ... NOT VALID ] -> S3 - noop
S4 -> [ ALTER TABLE ... ALTER CONSTRAINT ... NOT VALID ] -> S3
S1->[ ALTER TABLE ... ALTER CONSTRAINT ... ENFORCED ]->S4
S3->[ ALTER TABLE ... ALTER CONSTRAINT ... ENFORCED ]->S3 - noop
S4->[ ALTER TABLE ... ALTER CONSTRAINT ... ENFORCED ]->S4 - noop
S1->[ ALTER TABLE ... VALIDATE CONSTRAINT ... ]->S1 - but this is not
noop - the existing data gets validated but no change happens to the
state of the constraint - it is not enforced on the future data and
it's not considered valid. This gives opportunity to the user to just
validate the existing data but not enforce the constraint on new data
thus avoiding some computation on the new data. Of course we will have
to update the documentation to clearly specify the result. I think
VALIDATE CONSTRAINT is postgresql extension so we are free to
interpret it in the context of ENFORCED feature.
S3->[ ALTER TABLE ... VALIDATE CONSTRAINT ... ]->S4
S4->[ ALTER TABLE ... VALIDATE CONSTRAINT ... ]->S4 - noop
S1-[ ALTER TABLE ... ALTER CONSTRAINT ... NOT ENFORCED ]->S1 - noop
S3-[ ALTER TABLE ... ALTER CONSTRAINT ... NOT ENFORCED ]->S1
S4-[[ ALTER TABLE ... ALTER CONSTRAINT ... NOT ENFORCED ]->S1
Notice that there are no edges to and from S2.
--
Best Wishes,
Ashutosh Bapat
On 2025-Feb-13, Ashutosh Bapat wrote:
So considering that, I think a three-state system makes more sense.
Something like:1) NOT ENFORCED -- no data is checked
2) NOT VALID -- existing data is unchecked, new data is checked
3) ENFORCED -- all data is checkedTransitions:
(1) - [ ALTER TABLE ... ALTER CONSTRAINT ... NOT VALID ] -> (2)
Per your notation, this means the the constraint is not enforced but
new data is checked - that seems a contradiction, how would we check
the data when the constraint is not being enforced. Or do you suggest
that we convert a NOT ENFORCED constraint to ENFORCED as a result of
converting it to NOT VALID?
I agree this one is a little weird. For this I would have the command
be
ALTER TABLE ... ALTER CONSTRAINT ... ENFORCED NOT VALID
this way it's explicit that what we want is flip the ENFORCED bit while
leaving NOT VALID as-is.
(2) - [ ALTER TABLE ... VALIDATE CONSTRAINT ... ] -> (3)
As a result of this a not enforced constraint would turn into an
enforced constraint. The user might have intended to just validate the
data but not enforce it to avoid paying price for the checks on new
data.
I'm not sure there's a use case for validating existing data without
starting to enforce the constraint. The data can become invalid
immediately after you've run the command, so why bother?
I think, what you intend to say is clearer with 4 state system {NE, E}
* {NV, V} = {(NE, NV), (NE, V), (E, NV), (E, V)} where (NE, V) is
unreachable. Let's name them S1, S2, S3, S4 respectively.
[...]
Notice that there are no edges to and from S2.
So why list it as a possible state?
--
Álvaro Herrera 48°01'N 7°57'E — https://www.EnterpriseDB.com/
"Las mujeres son como hondas: mientras más resistencia tienen,
más lejos puedes llegar con ellas" (Jonas Nightingale, Leap of Faith)
On Thu, Feb 13, 2025 at 5:27 PM Álvaro Herrera <alvherre@alvh.no-ip.org> wrote:
On 2025-Feb-13, Ashutosh Bapat wrote:
So considering that, I think a three-state system makes more sense.
Something like:1) NOT ENFORCED -- no data is checked
2) NOT VALID -- existing data is unchecked, new data is checked
3) ENFORCED -- all data is checkedTransitions:
(1) - [ ALTER TABLE ... ALTER CONSTRAINT ... NOT VALID ] -> (2)
Per your notation, this means the the constraint is not enforced but
new data is checked - that seems a contradiction, how would we check
the data when the constraint is not being enforced. Or do you suggest
that we convert a NOT ENFORCED constraint to ENFORCED as a result of
converting it to NOT VALID?I agree this one is a little weird. For this I would have the command
be
ALTER TABLE ... ALTER CONSTRAINT ... ENFORCED NOT VALID
this way it's explicit that what we want is flip the ENFORCED bit while
leaving NOT VALID as-is.(2) - [ ALTER TABLE ... VALIDATE CONSTRAINT ... ] -> (3)
As a result of this a not enforced constraint would turn into an
enforced constraint. The user might have intended to just validate the
data but not enforce it to avoid paying price for the checks on new
data.I'm not sure there's a use case for validating existing data without
starting to enforce the constraint. The data can become invalid
immediately after you've run the command, so why bother?
Validating whole table at a time is cheaper than doing it for every
row as it appears. So the ability to validate data in batches at
regular intervals instead of validating every row has some
attractiveness, esp in huge data/analytics cases. And we could
implement it without much cost. But I don't have a concrete usecase.
I think, what you intend to say is clearer with 4 state system {NE, E}
* {NV, V} = {(NE, NV), (NE, V), (E, NV), (E, V)} where (NE, V) is
unreachable. Let's name them S1, S2, S3, S4 respectively.[...]
Notice that there are no edges to and from S2.
So why list it as a possible state?
For the sake of combinatorics. :)
--
Best Wishes,
Ashutosh Bapat
On Fri, 14 Feb 2025 at 10:11, Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>
wrote:
I think, what you intend to say is clearer with 4 state system {NE, E}
* {NV, V} = {(NE, NV), (NE, V), (E, NV), (E, V)} where (NE, V) is
unreachable. Let's name them S1, S2, S3, S4 respectively.[...]
Notice that there are no edges to and from S2.
So why list it as a possible state?
For the sake of combinatorics. :)
Just because there are 2^n combinations of n boolean values does not mean
there are 2^n actual meaningful states. That's why we have CHECK
constraints.
On Fri, Feb 14, 2025 at 8:41 PM Ashutosh Bapat
<ashutosh.bapat.oss@gmail.com> wrote:
On Thu, Feb 13, 2025 at 5:27 PM Álvaro Herrera <alvherre@alvh.no-ip.org> wrote:
On 2025-Feb-13, Ashutosh Bapat wrote:
So considering that, I think a three-state system makes more sense.
Something like:1) NOT ENFORCED -- no data is checked
2) NOT VALID -- existing data is unchecked, new data is checked
3) ENFORCED -- all data is checkedTransitions:
(1) - [ ALTER TABLE ... ALTER CONSTRAINT ... NOT VALID ] -> (2)
Per your notation, this means the the constraint is not enforced but
new data is checked - that seems a contradiction, how would we check
the data when the constraint is not being enforced. Or do you suggest
that we convert a NOT ENFORCED constraint to ENFORCED as a result of
converting it to NOT VALID?I agree this one is a little weird. For this I would have the command
be
ALTER TABLE ... ALTER CONSTRAINT ... ENFORCED NOT VALID
this way it's explicit that what we want is flip the ENFORCED bit while
leaving NOT VALID as-is.(2) - [ ALTER TABLE ... VALIDATE CONSTRAINT ... ] -> (3)
As a result of this a not enforced constraint would turn into an
enforced constraint. The user might have intended to just validate the
data but not enforce it to avoid paying price for the checks on new
data.I'm not sure there's a use case for validating existing data without
starting to enforce the constraint. The data can become invalid
immediately after you've run the command, so why bother?Validating whole table at a time is cheaper than doing it for every
row as it appears. So the ability to validate data in batches at
regular intervals instead of validating every row has some
attractiveness, esp in huge data/analytics cases. And we could
implement it without much cost. But I don't have a concrete usecase.
Well, I’m not sure if it’s worth validating data in batches when we
don’t maintain their state, as this would lead to revalidating the
same data in the next validation along with newly inserted records.
Also, based on the current implementation, we can perform CHECK
constraint validation, but not FOREIGN KEY constraint validation,
since the necessary triggers for referential integrity checks haven’t
been created for NOT ENFORCED. While we can create those triggers,
it’s unclear whether we want to keep them around if they aren’t being
used for NOT ENFORCED constraints.
Regards,
Amul
On Tue, Feb 11, 2025 at 12:18 AM Álvaro Herrera <alvherre@alvh.no-ip.org> wrote:
[...]
• v13-0001-Add-AlterConstraintStmt-struct-for-ALTER-.-CONST.patch
I think the new node name is wrong, because it makes the code looks as if we support ALTER CONSTRAINT as a statement for this, which is false. (This is a repeat of ReplicaIdentityStmt, which I think is a mistake). I would suggest a name like ATAlterConstraint instead. Perhaps we can use that in the patch for ALTER CONSTRAINT ... SET [NO] INHERIT as well, instead of (as I suggested Suraj) creating a separate subcommand number.
I have renamed AlterConstraintStmt to ATAlterConstraint as per your
suggestion in the attached version. Apart from this, there are no
other changes, as the final behavior is still unclear based on the
discussions so far.
If we don’t want to keep a constraint marked as valid when it is not
enforced, we should revert to the v12 version behavior, where the
constraint is marked invalid when changed to NOT ENFORCED and remains
so even after being changed to ENFORCED, until explicitly validated
using the existing ALTER ... VALIDATE CONSTRAINT command.
However, if we want to provide an option to validate the constraint
immediately when changing it to ENFORCED, we could introduce support
for a syntax like:
ALTER TABLE ... ALTER CONSTRAINT ... [ NOT ] ENFORCED [ (WITH |
WITHOUT) VALIDATION ]
Regards,
Amul
Attachments:
v14-0001-Add-ATAlterConstraint-struct-for-ALTER-.-CONSTRA.patchapplication/x-patch; name=v14-0001-Add-ATAlterConstraint-struct-for-ALTER-.-CONSTRA.patchDownload
From 5ab3a089fc40a288f48210a728540634ad060e98 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Tue, 4 Feb 2025 09:55:18 +0530
Subject: [PATCH v14 1/9] Add ATAlterConstraint struct for ALTER .. CONSTRAINT.
Replace the use of Constraint with the new ATAlterConstraint struct,
which allows us to pass additional information, such as whether the
constraint attribute is set. This is necessary for the feature patches
that involve altering the enforceability of foreign key and check
constraints as part of the implementation.
---
src/backend/commands/tablecmds.c | 36 ++++++++++++++++----------------
src/backend/parser/gram.y | 3 +--
src/include/nodes/parsenodes.h | 14 +++++++++++++
src/tools/pgindent/typedefs.list | 1 +
4 files changed, 34 insertions(+), 20 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 72a1b64c2a2..ec50fbf0809 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -389,17 +389,17 @@ static void AlterIndexNamespaces(Relation classRel, Relation rel,
static void AlterSeqNamespaces(Relation classRel, Relation rel,
Oid oldNspOid, Oid newNspOid, ObjectAddresses *objsMoved,
LOCKMODE lockmode);
-static ObjectAddress ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd,
+static ObjectAddress ATExecAlterConstraint(Relation rel, ATAlterConstraint *cmdcon,
bool recurse, bool recursing, LOCKMODE lockmode);
-static bool ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode);
+static bool ATExecAlterConstrRecurse(ATAlterConstraint *cmdcon, Relation conrel,
+ Relation tgrel, Relation rel, HeapTuple contuple,
+ List **otherrelids, LOCKMODE lockmode);
static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
bool deferrable, bool initdeferred,
List **otherrelids);
-static void ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode);
+static void ATExecAlterChildConstr(ATAlterConstraint *cmdcon, Relation conrel,
+ Relation tgrel, Relation rel, HeapTuple contuple,
+ List **otherrelids, LOCKMODE lockmode);
static ObjectAddress ATExecValidateConstraint(List **wqueue,
Relation rel, char *constrName,
bool recurse, bool recursing, LOCKMODE lockmode);
@@ -5450,7 +5450,10 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
lockmode);
break;
case AT_AlterConstraint: /* ALTER CONSTRAINT */
- address = ATExecAlterConstraint(rel, cmd, false, false, lockmode);
+ address =
+ ATExecAlterConstraint(rel,
+ castNode(ATAlterConstraint, cmd->def),
+ false, false, lockmode);
break;
case AT_ValidateConstraint: /* VALIDATE CONSTRAINT */
address = ATExecValidateConstraint(wqueue, rel, cmd->name, cmd->recurse,
@@ -11801,10 +11804,9 @@ GetForeignKeyCheckTriggers(Relation trigrel,
* InvalidObjectAddress.
*/
static ObjectAddress
-ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
+ATExecAlterConstraint(Relation rel, ATAlterConstraint *cmdcon, bool recurse,
bool recursing, LOCKMODE lockmode)
{
- Constraint *cmdcon;
Relation conrel;
Relation tgrel;
SysScanDesc scan;
@@ -11815,8 +11817,6 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
List *otherrelids = NIL;
ListCell *lc;
- cmdcon = castNode(Constraint, cmd->def);
-
conrel = table_open(ConstraintRelationId, RowExclusiveLock);
tgrel = table_open(TriggerRelationId, RowExclusiveLock);
@@ -11939,9 +11939,9 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
* but existing releases don't do that.)
*/
static bool
-ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode)
+ATExecAlterConstrRecurse(ATAlterConstraint *cmdcon, Relation conrel,
+ Relation tgrel, Relation rel, HeapTuple contuple,
+ List **otherrelids, LOCKMODE lockmode)
{
Form_pg_constraint currcon;
Oid conoid;
@@ -12079,9 +12079,9 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
* ATExecAlterConstrRecurse.
*/
static void
-ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode)
+ATExecAlterChildConstr(ATAlterConstraint *cmdcon, Relation conrel,
+ Relation tgrel, Relation rel, HeapTuple contuple,
+ List **otherrelids, LOCKMODE lockmode)
{
Form_pg_constraint currcon;
Oid conoid;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index d3887628d46..ec4c07fab5d 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -2657,11 +2657,10 @@ alter_table_cmd:
| ALTER CONSTRAINT name ConstraintAttributeSpec
{
AlterTableCmd *n = makeNode(AlterTableCmd);
- Constraint *c = makeNode(Constraint);
+ ATAlterConstraint *c = makeNode(ATAlterConstraint);
n->subtype = AT_AlterConstraint;
n->def = (Node *) c;
- c->contype = CONSTR_FOREIGN; /* others not supported, yet */
c->conname = $3;
processCASbits($4, @4, "ALTER CONSTRAINT statement",
&c->deferrable,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 8dd421fa0ef..c0018abf16f 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2503,6 +2503,20 @@ typedef struct AlterCollationStmt
List *collname;
} AlterCollationStmt;
+/* ----------------------
+ * Alter Constraint
+ * ----------------------
+ */
+typedef struct ATAlterConstraint
+{
+ NodeTag type;
+
+ char *conname; /* Constraint name */
+
+ bool deferrable; /* DEFERRABLE? */
+ bool initdeferred; /* INITIALLY DEFERRED? */
+} ATAlterConstraint;
+
/* ----------------------
* Alter Domain
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index b6c170ac249..a501d2b4999 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -66,6 +66,7 @@ AllocSetFreeList
AllocateDesc
AllocateDescKind
AlterCollationStmt
+ATAlterConstraint
AlterDatabaseRefreshCollStmt
AlterDatabaseSetStmt
AlterDatabaseStmt
--
2.43.5
v14-0002-refactor-Split-tryAttachPartitionForeignKey.patchapplication/x-patch; name=v14-0002-refactor-Split-tryAttachPartitionForeignKey.patchDownload
From ecf2d024e0e6f14c250a72e640b6f5d94b51ed64 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Wed, 29 Jan 2025 11:35:27 +0530
Subject: [PATCH v14 2/9] refactor: Split tryAttachPartitionForeignKey()
Split tryAttachPartitionForeignKey() into three functions:
AttachPartitionForeignKey(), RemoveInheritedConstraint() and
DropForeignKeyConstraintTriggers(), so they can be reused in the next
patch.
---
src/backend/commands/tablecmds.c | 319 ++++++++++++++++++++-----------
1 file changed, 204 insertions(+), 115 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index ec50fbf0809..37e52a853cc 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -583,6 +583,14 @@ static bool tryAttachPartitionForeignKey(List **wqueue,
Oid parentInsTrigger,
Oid parentUpdTrigger,
Relation trigrel);
+static void AttachPartitionForeignKey(List **wqueue, Relation partition,
+ Oid partConstrOid, Oid parentConstrOid,
+ Oid parentInsTrigger, Oid parentUpdTrigger,
+ Relation trigrel);
+static void RemoveInheritedConstraint(Relation conrel, Relation trigrel,
+ Oid conoid, Oid conrelid);
+static void DropForeignKeyConstraintTriggers(Relation trigrel, Oid conoid,
+ Oid confrelid, Oid conrelid);
static void GetForeignKeyActionTriggers(Relation trigrel,
Oid conoid, Oid confrelid, Oid conrelid,
Oid *deleteTriggerOid,
@@ -11445,12 +11453,6 @@ tryAttachPartitionForeignKey(List **wqueue,
Form_pg_constraint parentConstr;
HeapTuple partcontup;
Form_pg_constraint partConstr;
- bool queueValidation;
- ScanKeyData key;
- SysScanDesc scan;
- HeapTuple trigtup;
- Oid insertTriggerOid,
- updateTriggerOid;
parentConstrTup = SearchSysCache1(CONSTROID,
ObjectIdGetDatum(parentConstrOid));
@@ -11495,6 +11497,59 @@ tryAttachPartitionForeignKey(List **wqueue,
return false;
}
+ ReleaseSysCache(parentConstrTup);
+ ReleaseSysCache(partcontup);
+
+ /* Looks good! Attach this constraint. */
+ AttachPartitionForeignKey(wqueue, partition, fk->conoid,
+ parentConstrOid, parentInsTrigger,
+ parentUpdTrigger, trigrel);
+
+ return true;
+}
+
+/*
+ * AttachPartitionForeignKey
+ *
+ * The subroutine for tryAttachPartitionForeignKey performs the final tasks of
+ * attaching the constraint, removing redundant triggers and entries from
+ * pg_constraint, and setting the constraint's parent.
+ */
+static void
+AttachPartitionForeignKey(List **wqueue,
+ Relation partition,
+ Oid partConstrOid,
+ Oid parentConstrOid,
+ Oid parentInsTrigger,
+ Oid parentUpdTrigger,
+ Relation trigrel)
+{
+ HeapTuple parentConstrTup;
+ Form_pg_constraint parentConstr;
+ HeapTuple partcontup;
+ Form_pg_constraint partConstr;
+ bool queueValidation;
+ Oid partConstrFrelid;
+ Oid partConstrRelid;
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
+ /* Fetch the parent constraint tuple */
+ parentConstrTup = SearchSysCache1(CONSTROID,
+ ObjectIdGetDatum(parentConstrOid));
+ if (!HeapTupleIsValid(parentConstrTup))
+ elog(ERROR, "cache lookup failed for constraint %u", parentConstrOid);
+ parentConstr = (Form_pg_constraint) GETSTRUCT(parentConstrTup);
+
+ /* Fetch the child constraint tuple */
+ partcontup = SearchSysCache1(CONSTROID,
+ ObjectIdGetDatum(partConstrOid));
+ if (!HeapTupleIsValid(partcontup))
+ elog(ERROR, "cache lookup failed for constraint %u", partConstrOid);
+ partConstr = (Form_pg_constraint) GETSTRUCT(partcontup);
+ partConstrFrelid = partConstr->confrelid;
+ partConstrRelid = partConstr->conrelid;
+
/*
* Will we need to validate this constraint? A valid parent constraint
* implies that all child constraints have been validated, so if this one
@@ -11506,50 +11561,15 @@ tryAttachPartitionForeignKey(List **wqueue,
ReleaseSysCache(parentConstrTup);
/*
- * Looks good! Attach this constraint. The action triggers in the new
- * partition become redundant -- the parent table already has equivalent
- * ones, and those will be able to reach the partition. Remove the ones
- * in the partition. We identify them because they have our constraint
- * OID, as well as being on the referenced rel.
+ * The action triggers in the new partition become redundant -- the parent
+ * table already has equivalent ones, and those will be able to reach the
+ * partition. Remove the ones in the partition. We identify them because
+ * they have our constraint OID, as well as being on the referenced rel.
*/
- ScanKeyInit(&key,
- Anum_pg_trigger_tgconstraint,
- BTEqualStrategyNumber, F_OIDEQ,
- ObjectIdGetDatum(fk->conoid));
- scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
- NULL, 1, &key);
- while ((trigtup = systable_getnext(scan)) != NULL)
- {
- Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
- ObjectAddress trigger;
+ DropForeignKeyConstraintTriggers(trigrel, partConstrOid, partConstrFrelid,
+ partConstrRelid);
- if (trgform->tgconstrrelid != fk->conrelid)
- continue;
- if (trgform->tgrelid != fk->confrelid)
- continue;
-
- /*
- * The constraint is originally set up to contain this trigger as an
- * implementation object, so there's a dependency record that links
- * the two; however, since the trigger is no longer needed, we remove
- * the dependency link in order to be able to drop the trigger while
- * keeping the constraint intact.
- */
- deleteDependencyRecordsFor(TriggerRelationId,
- trgform->oid,
- false);
- /* make dependency deletion visible to performDeletion */
- CommandCounterIncrement();
- ObjectAddressSet(trigger, TriggerRelationId,
- trgform->oid);
- performDeletion(&trigger, DROP_RESTRICT, 0);
- /* make trigger drop visible, in case the loop iterates */
- CommandCounterIncrement();
- }
-
- systable_endscan(scan);
-
- ConstraintSetParentConstraint(fk->conoid, parentConstrOid,
+ ConstraintSetParentConstraint(partConstrOid, parentConstrOid,
RelationGetRelid(partition));
/*
@@ -11557,7 +11577,7 @@ tryAttachPartitionForeignKey(List **wqueue,
* corresponding parent triggers.
*/
GetForeignKeyCheckTriggers(trigrel,
- fk->conoid, fk->confrelid, fk->conrelid,
+ partConstrOid, partConstrFrelid, partConstrRelid,
&insertTriggerOid, &updateTriggerOid);
Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
@@ -11571,72 +11591,12 @@ tryAttachPartitionForeignKey(List **wqueue,
* attaching now has extra pg_constraint rows and action triggers that are
* no longer needed. Remove those.
*/
- if (get_rel_relkind(fk->confrelid) == RELKIND_PARTITIONED_TABLE)
+ if (get_rel_relkind(partConstrFrelid) == RELKIND_PARTITIONED_TABLE)
{
Relation pg_constraint = table_open(ConstraintRelationId, RowShareLock);
- ObjectAddresses *objs;
- HeapTuple consttup;
- ScanKeyInit(&key,
- Anum_pg_constraint_conrelid,
- BTEqualStrategyNumber, F_OIDEQ,
- ObjectIdGetDatum(fk->conrelid));
-
- scan = systable_beginscan(pg_constraint,
- ConstraintRelidTypidNameIndexId,
- true, NULL, 1, &key);
- objs = new_object_addresses();
- while ((consttup = systable_getnext(scan)) != NULL)
- {
- Form_pg_constraint conform = (Form_pg_constraint) GETSTRUCT(consttup);
-
- if (conform->conparentid != fk->conoid)
- continue;
- else
- {
- ObjectAddress addr;
- SysScanDesc scan2;
- ScanKeyData key2;
- int n PG_USED_FOR_ASSERTS_ONLY;
-
- ObjectAddressSet(addr, ConstraintRelationId, conform->oid);
- add_exact_object_address(&addr, objs);
-
- /*
- * First we must delete the dependency record that binds the
- * constraint records together.
- */
- n = deleteDependencyRecordsForSpecific(ConstraintRelationId,
- conform->oid,
- DEPENDENCY_INTERNAL,
- ConstraintRelationId,
- fk->conoid);
- Assert(n == 1); /* actually only one is expected */
-
- /*
- * Now search for the triggers for this constraint and set
- * them up for deletion too
- */
- ScanKeyInit(&key2,
- Anum_pg_trigger_tgconstraint,
- BTEqualStrategyNumber, F_OIDEQ,
- ObjectIdGetDatum(conform->oid));
- scan2 = systable_beginscan(trigrel, TriggerConstraintIndexId,
- true, NULL, 1, &key2);
- while ((trigtup = systable_getnext(scan2)) != NULL)
- {
- ObjectAddressSet(addr, TriggerRelationId,
- ((Form_pg_trigger) GETSTRUCT(trigtup))->oid);
- add_exact_object_address(&addr, objs);
- }
- systable_endscan(scan2);
- }
- }
- /* make the dependency deletions visible */
- CommandCounterIncrement();
- performMultipleDeletions(objs, DROP_RESTRICT,
- PERFORM_DELETION_INTERNAL);
- systable_endscan(scan);
+ RemoveInheritedConstraint(pg_constraint, trigrel, partConstrOid,
+ partConstrRelid);
table_close(pg_constraint, RowShareLock);
}
@@ -11657,9 +11617,10 @@ tryAttachPartitionForeignKey(List **wqueue,
Relation conrel;
conrel = table_open(ConstraintRelationId, RowExclusiveLock);
- partcontup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
+
+ partcontup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(partConstrOid));
if (!HeapTupleIsValid(partcontup))
- elog(ERROR, "cache lookup failed for constraint %u", fk->conoid);
+ elog(ERROR, "cache lookup failed for constraint %u", partConstrOid);
/* Use the same lock as for AT_ValidateConstraint */
QueueFKConstraintValidation(wqueue, conrel, partition, partcontup,
@@ -11667,8 +11628,136 @@ tryAttachPartitionForeignKey(List **wqueue,
ReleaseSysCache(partcontup);
table_close(conrel, RowExclusiveLock);
}
+}
- return true;
+/*
+ * RemoveInheritedConstraint
+ *
+ * Removes the constraint and its associated trigger from the specified
+ * relation, which inherited the given constraint.
+ */
+static void
+RemoveInheritedConstraint(Relation conrel, Relation trigrel, Oid conoid,
+ Oid conrelid)
+{
+ ObjectAddresses *objs;
+ HeapTuple consttup;
+ ScanKeyData key;
+ SysScanDesc scan;
+ HeapTuple trigtup;
+
+ ScanKeyInit(&key,
+ Anum_pg_constraint_conrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conrelid));
+
+ scan = systable_beginscan(conrel,
+ ConstraintRelidTypidNameIndexId,
+ true, NULL, 1, &key);
+ objs = new_object_addresses();
+ while ((consttup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_constraint conform = (Form_pg_constraint) GETSTRUCT(consttup);
+
+ if (conform->conparentid != conoid)
+ continue;
+ else
+ {
+ ObjectAddress addr;
+ SysScanDesc scan2;
+ ScanKeyData key2;
+ int n PG_USED_FOR_ASSERTS_ONLY;
+
+ ObjectAddressSet(addr, ConstraintRelationId, conform->oid);
+ add_exact_object_address(&addr, objs);
+
+ /*
+ * First we must delete the dependency record that binds the
+ * constraint records together.
+ */
+ n = deleteDependencyRecordsForSpecific(ConstraintRelationId,
+ conform->oid,
+ DEPENDENCY_INTERNAL,
+ ConstraintRelationId,
+ conoid);
+ Assert(n == 1); /* actually only one is expected */
+
+ /*
+ * Now search for the triggers for this constraint and set them up
+ * for deletion too
+ */
+ ScanKeyInit(&key2,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conform->oid));
+ scan2 = systable_beginscan(trigrel, TriggerConstraintIndexId,
+ true, NULL, 1, &key2);
+ while ((trigtup = systable_getnext(scan2)) != NULL)
+ {
+ ObjectAddressSet(addr, TriggerRelationId,
+ ((Form_pg_trigger) GETSTRUCT(trigtup))->oid);
+ add_exact_object_address(&addr, objs);
+ }
+ systable_endscan(scan2);
+ }
+ }
+ /* make the dependency deletions visible */
+ CommandCounterIncrement();
+ performMultipleDeletions(objs, DROP_RESTRICT,
+ PERFORM_DELETION_INTERNAL);
+ systable_endscan(scan);
+}
+
+/*
+ * DropForeignKeyConstraintTriggers
+ *
+ * The subroutine for tryAttachPartitionForeignKey handles the deletion of
+ * action triggers for the foreign key constraint.
+ */
+static void
+DropForeignKeyConstraintTriggers(Relation trigrel, Oid conoid, Oid confrelid,
+ Oid conrelid)
+{
+ ScanKeyData key;
+ SysScanDesc scan;
+ HeapTuple trigtup;
+
+ ScanKeyInit(&key,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+ scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
+ NULL, 1, &key);
+ while ((trigtup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
+ ObjectAddress trigger;
+
+ if (trgform->tgconstrrelid != conrelid)
+ continue;
+ if (trgform->tgrelid != confrelid)
+ continue;
+
+ /*
+ * The constraint is originally set up to contain this trigger as an
+ * implementation object, so there's a dependency record that links
+ * the two; however, since the trigger is no longer needed, we remove
+ * the dependency link in order to be able to drop the trigger while
+ * keeping the constraint intact.
+ */
+ deleteDependencyRecordsFor(TriggerRelationId,
+ trgform->oid,
+ false);
+ /* make dependency deletion visible to performDeletion */
+ CommandCounterIncrement();
+ ObjectAddressSet(trigger, TriggerRelationId,
+ trgform->oid);
+ performDeletion(&trigger, DROP_RESTRICT, 0);
+ /* make trigger drop visible, in case the loop iterates */
+ CommandCounterIncrement();
+ }
+
+ systable_endscan(scan);
}
/*
--
2.43.5
v14-0003-Move-the-RemoveInheritedConstraint-function-call.patchapplication/x-patch; name=v14-0003-Move-the-RemoveInheritedConstraint-function-call.patchDownload
From dfb13da6362047344f104e84ca7e5a5e97ab7797 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 10 Feb 2025 09:12:06 +0530
Subject: [PATCH v14 3/9] Move the RemoveInheritedConstraint() function call
slightly earlier.
This change is harmless and does not affect the existing intended
operation. It is necessary for the feature patch operation, where we
may need to change the child constraint to enforced. In this case, we
would create the necessary triggers and queue the constraint for
validation, so it is important to remove any unnecessary constraints
before proceeding.
-- NOTE --
This is a small change that could have been included in the previous
"split tryAttachPartitionForeignKey" refactoring patch, but was kept
separate to highlight the changes.
---------
---
src/backend/commands/tablecmds.c | 30 +++++++++++++++---------------
1 file changed, 15 insertions(+), 15 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 37e52a853cc..44189c8c7f7 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -11550,6 +11550,21 @@ AttachPartitionForeignKey(List **wqueue,
partConstrFrelid = partConstr->confrelid;
partConstrRelid = partConstr->conrelid;
+ /*
+ * If the referenced table is partitioned, then the partition we're
+ * attaching now has extra pg_constraint rows and action triggers that are
+ * no longer needed. Remove those.
+ */
+ if (get_rel_relkind(partConstrFrelid) == RELKIND_PARTITIONED_TABLE)
+ {
+ Relation pg_constraint = table_open(ConstraintRelationId, RowShareLock);
+
+ RemoveInheritedConstraint(pg_constraint, trigrel, partConstrOid,
+ partConstrRelid);
+
+ table_close(pg_constraint, RowShareLock);
+ }
+
/*
* Will we need to validate this constraint? A valid parent constraint
* implies that all child constraints have been validated, so if this one
@@ -11586,21 +11601,6 @@ AttachPartitionForeignKey(List **wqueue,
TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
RelationGetRelid(partition));
- /*
- * If the referenced table is partitioned, then the partition we're
- * attaching now has extra pg_constraint rows and action triggers that are
- * no longer needed. Remove those.
- */
- if (get_rel_relkind(partConstrFrelid) == RELKIND_PARTITIONED_TABLE)
- {
- Relation pg_constraint = table_open(ConstraintRelationId, RowShareLock);
-
- RemoveInheritedConstraint(pg_constraint, trigrel, partConstrOid,
- partConstrRelid);
-
- table_close(pg_constraint, RowShareLock);
- }
-
/*
* We updated this pg_constraint row above to set its parent; validating
* it will cause its convalidated flag to change, so we need CCI here. In
--
2.43.5
v14-0004-refactor-Pass-Relid-instead-of-Relation-to-creat.patchapplication/x-patch; name=v14-0004-refactor-Pass-Relid-instead-of-Relation-to-creat.patchDownload
From 653b8a20a420fdf84c26f4587afc14f36eaa2532 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 20 Jan 2025 18:49:41 +0530
Subject: [PATCH v14 4/9] refactor: Pass Relid instead of Relation to
createForeignKeyCheckTriggers().
Currently, createForeignKeyCheckTriggers() takes a Relation type as
its first argument, but it doesn't use that argument directly.
Instead, it fetches the Relid by calling RelationGetRelid().
Therefore, it would be more consistent with other functions (e.g.,
createForeignKeyCheckTriggers()) to pass the Relid directly instead of
the whole Relation.
---
src/backend/commands/tablecmds.c | 20 ++++++++++----------
1 file changed, 10 insertions(+), 10 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 44189c8c7f7..f105b728120 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -569,7 +569,7 @@ static void createForeignKeyCheckTriggers(Oid myRelOid, Oid refRelOid,
Oid indexOid,
Oid parentInsTrigger, Oid parentUpdTrigger,
Oid *insertTrigOid, Oid *updateTrigOid);
-static void createForeignKeyActionTriggers(Relation rel, Oid refRelOid,
+static void createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid,
Constraint *fkconstraint, Oid constraintOid,
Oid indexOid,
Oid parentDelTrigger, Oid parentUpdTrigger,
@@ -10659,7 +10659,8 @@ addFkRecurseReferenced(Constraint *fkconstraint, Relation rel,
/*
* Create the action triggers that enforce the constraint.
*/
- createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
+ createForeignKeyActionTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
fkconstraint,
parentConstr, indexOid,
parentDelTrigger, parentUpdTrigger,
@@ -13034,10 +13035,11 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
* *updateTrigOid.
*/
static void
-createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstraint,
- Oid constraintOid, Oid indexOid,
- Oid parentDelTrigger, Oid parentUpdTrigger,
- Oid *deleteTrigOid, Oid *updateTrigOid)
+createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid,
+ Constraint *fkconstraint, Oid constraintOid,
+ Oid indexOid, Oid parentDelTrigger,
+ Oid parentUpdTrigger, Oid *deleteTrigOid,
+ Oid *updateTrigOid)
{
CreateTrigStmt *fk_trigger;
ObjectAddress trigAddress;
@@ -13093,8 +13095,7 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
break;
}
- trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid,
- RelationGetRelid(rel),
+ trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid, myRelOid,
constraintOid, indexOid, InvalidOid,
parentDelTrigger, NULL, true, false);
if (deleteTrigOid)
@@ -13154,8 +13155,7 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
break;
}
- trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid,
- RelationGetRelid(rel),
+ trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid, myRelOid,
constraintOid, indexOid, InvalidOid,
parentUpdTrigger, NULL, true, false);
if (updateTrigOid)
--
2.43.5
v14-0005-refactor-Change-ATExecAlterConstrRecurse-argumen.patchapplication/x-patch; name=v14-0005-refactor-Change-ATExecAlterConstrRecurse-argumen.patchDownload
From ba24ca5829e709d51696654f386ea48ee4e50c77 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Tue, 14 Jan 2025 22:35:32 +0530
Subject: [PATCH v14 5/9] refactor: Change ATExecAlterConstrRecurse() argument.
Instead of passing the Relation of the referencing relation, we will
pass a relid. Opening the relation using the relid again should not
cause significant issues. However, this approach makes recursion more
efficient, especially in the later patch where we will be recreating
the referencing and referred triggers. In addition to that passing
relid of the referred relation too.
----
NOTE: This patch is not meant to be committed separately. It should
be squashed with the main patch that adds ENFORCED/NOT ENFORCED.
----
---
src/backend/commands/tablecmds.c | 44 +++++++++++++++++---------------
1 file changed, 23 insertions(+), 21 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index f105b728120..2f50fb24ca9 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -392,14 +392,16 @@ static void AlterSeqNamespaces(Relation classRel, Relation rel,
static ObjectAddress ATExecAlterConstraint(Relation rel, ATAlterConstraint *cmdcon,
bool recurse, bool recursing, LOCKMODE lockmode);
static bool ATExecAlterConstrRecurse(ATAlterConstraint *cmdcon, Relation conrel,
- Relation tgrel, Relation rel, HeapTuple contuple,
- List **otherrelids, LOCKMODE lockmode);
+ Relation tgrel, const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode);
static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
bool deferrable, bool initdeferred,
List **otherrelids);
static void ATExecAlterChildConstr(ATAlterConstraint *cmdcon, Relation conrel,
- Relation tgrel, Relation rel, HeapTuple contuple,
- List **otherrelids, LOCKMODE lockmode);
+ Relation tgrel, const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode);
static ObjectAddress ATExecValidateConstraint(List **wqueue,
Relation rel, char *constrName,
bool recurse, bool recursing, LOCKMODE lockmode);
@@ -11996,8 +11998,9 @@ ATExecAlterConstraint(Relation rel, ATAlterConstraint *cmdcon, bool recurse,
currcon->condeferred != cmdcon->initdeferred ||
rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
- if (ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, rel, contuple,
- &otherrelids, lockmode))
+ if (ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, currcon->conrelid,
+ currcon->confrelid, contuple, &otherrelids,
+ lockmode))
ObjectAddressSet(address, ConstraintRelationId, currcon->oid);
}
@@ -12030,13 +12033,15 @@ ATExecAlterConstraint(Relation rel, ATAlterConstraint *cmdcon, bool recurse,
*/
static bool
ATExecAlterConstrRecurse(ATAlterConstraint *cmdcon, Relation conrel,
- Relation tgrel, Relation rel, HeapTuple contuple,
- List **otherrelids, LOCKMODE lockmode)
+ Relation tgrel, const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode)
{
Form_pg_constraint currcon;
Oid conoid;
Oid refrelid;
bool changed = false;
+ Relation rel;
/* since this function recurses, it could be driven to stack overflow */
check_stack_depth();
@@ -12045,6 +12050,8 @@ ATExecAlterConstrRecurse(ATAlterConstraint *cmdcon, Relation conrel,
conoid = currcon->oid;
refrelid = currcon->confrelid;
+ rel = table_open(currcon->conrelid, lockmode);
+
/*
* Update pg_constraint with the flags from cmdcon.
*
@@ -12090,8 +12097,9 @@ ATExecAlterConstrRecurse(ATAlterConstraint *cmdcon, Relation conrel,
*/
if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)
- ATExecAlterChildConstr(cmdcon, conrel, tgrel, rel, contuple,
- otherrelids, lockmode);
+ ATExecAlterChildConstr(cmdcon, conrel, tgrel, fkrelid, pkrelid,
+ contuple, otherrelids, lockmode);
+ table_close(rel, NoLock);
return changed;
}
@@ -12170,8 +12178,9 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
*/
static void
ATExecAlterChildConstr(ATAlterConstraint *cmdcon, Relation conrel,
- Relation tgrel, Relation rel, HeapTuple contuple,
- List **otherrelids, LOCKMODE lockmode)
+ Relation tgrel, const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode)
{
Form_pg_constraint currcon;
Oid conoid;
@@ -12191,15 +12200,8 @@ ATExecAlterChildConstr(ATAlterConstraint *cmdcon, Relation conrel,
true, NULL, 1, &pkey);
while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
- {
- Form_pg_constraint childcon = (Form_pg_constraint) GETSTRUCT(childtup);
- Relation childrel;
-
- childrel = table_open(childcon->conrelid, lockmode);
- ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, childrel, childtup,
- otherrelids, lockmode);
- table_close(childrel, NoLock);
- }
+ ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, fkrelid, pkrelid,
+ childtup, otherrelids, lockmode);
systable_endscan(pscan);
}
--
2.43.5
v14-0006-Remove-hastriggers-flag-check-before-fetching-FK.patchapplication/x-patch; name=v14-0006-Remove-hastriggers-flag-check-before-fetching-FK.patchDownload
From a2b64a5f28375b9c5fb05e11bd40aafc44e9470d Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 7 Oct 2024 12:40:07 +0530
Subject: [PATCH v14 6/9] Remove hastriggers flag check before fetching FK
constraints.
With NOT ENFORCED, foreign key constraints will be created without
triggers. Therefore, the criteria for fetching foreign keys based on
the presence of triggers no longer apply.
----
NOTE: This patch is intended to reduce the diff noise from the main
patch and is not meant to be committed separately. It should be
squashed with the main patch that adds ENFORCED/NOT ENFORCED.
----
---
src/backend/utils/cache/relcache.c | 5 -
src/bin/pg_dump/pg_dump.c | 8 +-
src/bin/psql/describe.c | 226 ++++++++++++++---------------
3 files changed, 108 insertions(+), 131 deletions(-)
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 398114373e9..c010de4aeca 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4661,11 +4661,6 @@ RelationGetFKeyList(Relation relation)
if (relation->rd_fkeyvalid)
return relation->rd_fkeylist;
- /* Fast path: non-partitioned tables without triggers can't have FKs */
- if (!relation->rd_rel->relhastriggers &&
- relation->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
- return NIL;
-
/*
* We build the list we intend to return (in the caller's context) while
* doing the scan. After successfully completing the scan, we copy that
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 30dfda8c3ff..1b6066b1ec5 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -7845,13 +7845,7 @@ getConstraints(Archive *fout, TableInfo tblinfo[], int numTables)
{
TableInfo *tinfo = &tblinfo[i];
- /*
- * For partitioned tables, foreign keys have no triggers so they must
- * be included anyway in case some foreign keys are defined.
- */
- if ((!tinfo->hastriggers &&
- tinfo->relkind != RELKIND_PARTITIONED_TABLE) ||
- !(tinfo->dobj.dump & DUMP_COMPONENT_DEFINITION))
+ if (!(tinfo->dobj.dump & DUMP_COMPONENT_DEFINITION))
continue;
/* OK, we need info for this table */
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 3b7ba66fad0..3f214e9805b 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2550,136 +2550,124 @@ describeOneTableDetails(const char *schemaname,
PQclear(result);
}
- /*
- * Print foreign-key constraints (there are none if no triggers,
- * except if the table is partitioned, in which case the triggers
- * appear in the partitions)
- */
- if (tableinfo.hastriggers ||
- tableinfo.relkind == RELKIND_PARTITIONED_TABLE)
+ /* Print foreign-key constraints */
+ if (pset.sversion >= 120000 &&
+ (tableinfo.ispartition || tableinfo.relkind == RELKIND_PARTITIONED_TABLE))
{
- if (pset.sversion >= 120000 &&
- (tableinfo.ispartition || tableinfo.relkind == RELKIND_PARTITIONED_TABLE))
+ /*
+ * Put the constraints defined in this table first, followed
+ * by the constraints defined in ancestor partitioned tables.
+ */
+ printfPQExpBuffer(&buf,
+ "SELECT conrelid = '%s'::pg_catalog.regclass AS sametable,\n"
+ " conname,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef,\n"
+ " conrelid::pg_catalog.regclass AS ontable\n"
+ " FROM pg_catalog.pg_constraint,\n"
+ " pg_catalog.pg_partition_ancestors('%s')\n"
+ " WHERE conrelid = relid AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
+ "ORDER BY sametable DESC, conname;",
+ oid, oid);
+ }
+ else
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT true as sametable, conname,\n"
+ " pg_catalog.pg_get_constraintdef(r.oid, true) as condef,\n"
+ " conrelid::pg_catalog.regclass AS ontable\n"
+ "FROM pg_catalog.pg_constraint r\n"
+ "WHERE r.conrelid = '%s' AND r.contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n",
+ oid);
+
+ if (pset.sversion >= 120000)
+ appendPQExpBufferStr(&buf, " AND conparentid = 0\n");
+ appendPQExpBufferStr(&buf, "ORDER BY conname");
+ }
+
+ result = PSQLexec(buf.data);
+ if (!result)
+ goto error_return;
+ else
+ tuples = PQntuples(result);
+
+ if (tuples > 0)
+ {
+ int i_sametable = PQfnumber(result, "sametable"),
+ i_conname = PQfnumber(result, "conname"),
+ i_condef = PQfnumber(result, "condef"),
+ i_ontable = PQfnumber(result, "ontable");
+
+ printTableAddFooter(&cont, _("Foreign-key constraints:"));
+ for (i = 0; i < tuples; i++)
{
/*
- * Put the constraints defined in this table first, followed
- * by the constraints defined in ancestor partitioned tables.
+ * Print untranslated constraint name and definition. Use
+ * a "TABLE tab" prefix when the constraint is defined in
+ * a parent partitioned table.
*/
- printfPQExpBuffer(&buf,
- "SELECT conrelid = '%s'::pg_catalog.regclass AS sametable,\n"
- " conname,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef,\n"
- " conrelid::pg_catalog.regclass AS ontable\n"
- " FROM pg_catalog.pg_constraint,\n"
- " pg_catalog.pg_partition_ancestors('%s')\n"
- " WHERE conrelid = relid AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
- "ORDER BY sametable DESC, conname;",
- oid, oid);
- }
- else
- {
- printfPQExpBuffer(&buf,
- "SELECT true as sametable, conname,\n"
- " pg_catalog.pg_get_constraintdef(r.oid, true) as condef,\n"
- " conrelid::pg_catalog.regclass AS ontable\n"
- "FROM pg_catalog.pg_constraint r\n"
- "WHERE r.conrelid = '%s' AND r.contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n",
- oid);
-
- if (pset.sversion >= 120000)
- appendPQExpBufferStr(&buf, " AND conparentid = 0\n");
- appendPQExpBufferStr(&buf, "ORDER BY conname");
- }
-
- result = PSQLexec(buf.data);
- if (!result)
- goto error_return;
- else
- tuples = PQntuples(result);
-
- if (tuples > 0)
- {
- int i_sametable = PQfnumber(result, "sametable"),
- i_conname = PQfnumber(result, "conname"),
- i_condef = PQfnumber(result, "condef"),
- i_ontable = PQfnumber(result, "ontable");
-
- printTableAddFooter(&cont, _("Foreign-key constraints:"));
- for (i = 0; i < tuples; i++)
- {
- /*
- * Print untranslated constraint name and definition. Use
- * a "TABLE tab" prefix when the constraint is defined in
- * a parent partitioned table.
- */
- if (strcmp(PQgetvalue(result, i, i_sametable), "f") == 0)
- printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
- PQgetvalue(result, i, i_ontable),
- PQgetvalue(result, i, i_conname),
- PQgetvalue(result, i, i_condef));
- else
- printfPQExpBuffer(&buf, " \"%s\" %s",
- PQgetvalue(result, i, i_conname),
- PQgetvalue(result, i, i_condef));
-
- printTableAddFooter(&cont, buf.data);
- }
- }
- PQclear(result);
- }
-
- /* print incoming foreign-key references */
- if (tableinfo.hastriggers ||
- tableinfo.relkind == RELKIND_PARTITIONED_TABLE)
- {
- if (pset.sversion >= 120000)
- {
- printfPQExpBuffer(&buf,
- "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
- " FROM pg_catalog.pg_constraint c\n"
- " WHERE confrelid IN (SELECT pg_catalog.pg_partition_ancestors('%s')\n"
- " UNION ALL VALUES ('%s'::pg_catalog.regclass))\n"
- " AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
- "ORDER BY conname;",
- oid, oid);
- }
- else
- {
- printfPQExpBuffer(&buf,
- "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
- " FROM pg_catalog.pg_constraint\n"
- " WHERE confrelid = %s AND contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n"
- "ORDER BY conname;",
- oid);
- }
-
- result = PSQLexec(buf.data);
- if (!result)
- goto error_return;
- else
- tuples = PQntuples(result);
-
- if (tuples > 0)
- {
- int i_conname = PQfnumber(result, "conname"),
- i_ontable = PQfnumber(result, "ontable"),
- i_condef = PQfnumber(result, "condef");
-
- printTableAddFooter(&cont, _("Referenced by:"));
- for (i = 0; i < tuples; i++)
- {
+ if (strcmp(PQgetvalue(result, i, i_sametable), "f") == 0)
printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
PQgetvalue(result, i, i_ontable),
PQgetvalue(result, i, i_conname),
PQgetvalue(result, i, i_condef));
+ else
+ printfPQExpBuffer(&buf, " \"%s\" %s",
+ PQgetvalue(result, i, i_conname),
+ PQgetvalue(result, i, i_condef));
- printTableAddFooter(&cont, buf.data);
- }
+ printTableAddFooter(&cont, buf.data);
}
- PQclear(result);
}
+ PQclear(result);
+
+ /* print incoming foreign-key references */
+ if (pset.sversion >= 120000)
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
+ " FROM pg_catalog.pg_constraint c\n"
+ " WHERE confrelid IN (SELECT pg_catalog.pg_partition_ancestors('%s')\n"
+ " UNION ALL VALUES ('%s'::pg_catalog.regclass))\n"
+ " AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
+ "ORDER BY conname;",
+ oid, oid);
+ }
+ else
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
+ " FROM pg_catalog.pg_constraint\n"
+ " WHERE confrelid = %s AND contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n"
+ "ORDER BY conname;",
+ oid);
+ }
+
+ result = PSQLexec(buf.data);
+ if (!result)
+ goto error_return;
+ else
+ tuples = PQntuples(result);
+
+ if (tuples > 0)
+ {
+ int i_conname = PQfnumber(result, "conname"),
+ i_ontable = PQfnumber(result, "ontable"),
+ i_condef = PQfnumber(result, "condef");
+
+ printTableAddFooter(&cont, _("Referenced by:"));
+ for (i = 0; i < tuples; i++)
+ {
+ printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
+ PQgetvalue(result, i, i_ontable),
+ PQgetvalue(result, i, i_conname),
+ PQgetvalue(result, i, i_condef));
+
+ printTableAddFooter(&cont, buf.data);
+ }
+ }
+ PQclear(result);
/* print any row-level policies */
if (pset.sversion >= 90500)
--
2.43.5
v14-0007-Ease-the-restriction-that-a-NOT-ENFORCED-constra.patchapplication/x-patch; name=v14-0007-Ease-the-restriction-that-a-NOT-ENFORCED-constra.patchDownload
From ba0d8f20857d6a4fe388d9ce15109aa14e3407ca Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Fri, 7 Feb 2025 09:41:20 +0530
Subject: [PATCH v14 7/9] Ease the restriction that a NOT ENFORCED constraint
must be INVALID.
In the initial support for NOT ENFORCED check constraints in commit
ca87c415e2fccf81cec6fd45698dde9fae0ab570, we introduced a restriction
that a NOT ENFORCED constraint must be NOT VALID. However, we still
mark a NOT ENFORCED constraint as NOT VALID when adding it to an
already existing table, as validation cannot be performed on a NOT
ENFORCED constraint. Conversely, when the constraint is created along
with the table, it is acceptable to mark the constraint as valid.
---
src/backend/catalog/heap.c | 3 +-
src/backend/catalog/pg_constraint.c | 2 -
src/backend/commands/tablecmds.c | 19 ++---
src/backend/optimizer/util/plancat.c | 9 +--
src/backend/parser/gram.y | 9 ---
src/backend/parser/parse_utilcmd.c | 13 ++--
src/backend/utils/adt/ruleutils.c | 6 +-
src/test/regress/expected/alter_table.out | 4 +-
src/test/regress/expected/inherit.out | 88 ++++++++++++++---------
src/test/regress/sql/alter_table.sql | 3 +-
src/test/regress/sql/inherit.sql | 13 +++-
11 files changed, 84 insertions(+), 85 deletions(-)
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 956f196fc95..913e63fe93f 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -2740,7 +2740,7 @@ MergeWithExistingConstraint(Relation rel, const char *ccname, Node *expr,
* If the child constraint is "not valid" then cannot merge with a
* valid parent constraint.
*/
- if (is_initially_valid && con->conenforced && !con->convalidated)
+ if (is_initially_valid && !con->convalidated)
ereport(ERROR,
(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
errmsg("constraint \"%s\" conflicts with NOT VALID constraint on relation \"%s\"",
@@ -2803,7 +2803,6 @@ MergeWithExistingConstraint(Relation rel, const char *ccname, Node *expr,
{
Assert(is_local);
con->conenforced = true;
- con->convalidated = true;
}
CatalogTupleUpdate(conDesc, &tup->t_self, tup);
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index ac80652baf2..60bc54a96b6 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -102,8 +102,6 @@ CreateConstraintEntry(const char *constraintName,
/* Only CHECK constraint can be not enforced */
Assert(isEnforced || constraintType == CONSTRAINT_CHECK);
- /* NOT ENFORCED constraint must be NOT VALID */
- Assert(isEnforced || !isValidated);
conDesc = table_open(ConstraintRelationId, RowExclusiveLock);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 2f50fb24ca9..044dd4023a4 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -3166,10 +3166,7 @@ MergeCheckConstraint(List *constraints, const char *name, Node *expr, bool is_en
* marked as ENFORCED because one of the parents is ENFORCED.
*/
if (!ccon->is_enforced && is_enforced)
- {
ccon->is_enforced = true;
- ccon->skip_validation = false;
- }
return constraints;
}
@@ -3190,7 +3187,6 @@ MergeCheckConstraint(List *constraints, const char *name, Node *expr, bool is_en
newcon->expr = expr;
newcon->inhcount = 1;
newcon->is_enforced = is_enforced;
- newcon->skip_validation = !is_enforced;
return lappend(constraints, newcon);
}
@@ -16897,8 +16893,7 @@ MergeConstraintsIntoExisting(Relation child_rel, Relation parent_rel)
* If the child constraint is "not valid" then cannot merge with a
* valid parent constraint
*/
- if (parent_con->convalidated && child_con->conenforced &&
- !child_con->convalidated)
+ if (parent_con->convalidated && !child_con->convalidated)
ereport(ERROR,
(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
errmsg("constraint \"%s\" conflicts with NOT VALID constraint on child table \"%s\"",
@@ -19266,18 +19261,12 @@ ConstraintImpliedByRelConstraint(Relation scanrel, List *testConstraint, List *p
Node *cexpr;
/*
- * If this constraint hasn't been fully validated yet, we must ignore
- * it here.
+ * If this constraint hasn't been fully validated yet or is not
+ * enforced, we must ignore it here.
*/
- if (!constr->check[i].ccvalid)
+ if (!constr->check[i].ccvalid || !constr->check[i].ccenforced)
continue;
- /*
- * NOT ENFORCED constraints are always marked as invalid, which should
- * have been ignored.
- */
- Assert(constr->check[i].ccenforced);
-
cexpr = stringToNode(constr->check[i].ccbin);
/*
diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c
index 71abb01f655..233a5d8da98 100644
--- a/src/backend/optimizer/util/plancat.c
+++ b/src/backend/optimizer/util/plancat.c
@@ -1302,22 +1302,15 @@ get_relation_constraints(PlannerInfo *root,
* ignore it here. Also ignore if NO INHERIT and we weren't told
* that that's safe.
*/
- if (!constr->check[i].ccvalid)
+ if (!constr->check[i].ccvalid || !constr->check[i].ccenforced)
continue;
- /*
- * NOT ENFORCED constraints are always marked as invalid, which
- * should have been ignored.
- */
- Assert(constr->check[i].ccenforced);
-
/*
* Also ignore if NO INHERIT and we weren't told that that's safe.
*/
if (constr->check[i].ccnoinherit && !include_noinherit)
continue;
-
cexpr = stringToNode(constr->check[i].ccbin);
/*
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index ec4c07fab5d..589f56693cd 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -19558,15 +19558,6 @@ processCASbits(int cas_bits, int location, const char *constrType,
errmsg("%s constraints cannot be marked NOT ENFORCED",
constrType),
parser_errposition(location)));
-
- /*
- * NB: The validated status is irrelevant when the constraint is set to
- * NOT ENFORCED, but for consistency, it should be set accordingly.
- * This ensures that if the constraint is later changed to ENFORCED, it
- * will automatically be in the correct NOT VALIDATED state.
- */
- if (not_valid)
- *not_valid = true;
}
if (cas_bits & CAS_ENFORCED)
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index eb7716cd84c..9053c898e06 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -1452,7 +1452,6 @@ expandTableLikeClause(RangeVar *heapRel, TableLikeClause *table_like_clause)
char *ccname = constr->check[ccnum].ccname;
char *ccbin = constr->check[ccnum].ccbin;
bool ccenforced = constr->check[ccnum].ccenforced;
- bool ccvalid = constr->check[ccnum].ccvalid;
bool ccnoinherit = constr->check[ccnum].ccnoinherit;
Node *ccbin_node;
bool found_whole_row;
@@ -1483,13 +1482,13 @@ expandTableLikeClause(RangeVar *heapRel, TableLikeClause *table_like_clause)
n->conname = pstrdup(ccname);
n->location = -1;
n->is_enforced = ccenforced;
- n->initially_valid = ccvalid;
n->is_no_inherit = ccnoinherit;
n->raw_expr = NULL;
n->cooked_expr = nodeToString(ccbin_node);
/* We can skip validation, since the new table should be empty. */
n->skip_validation = true;
+ n->initially_valid = true;
atsubcmd = makeNode(AlterTableCmd);
atsubcmd->subtype = AT_AddConstraint;
@@ -2942,11 +2941,9 @@ transformCheckConstraints(CreateStmtContext *cxt, bool skipValidation)
return;
/*
- * When creating a new table (but not a foreign table), we can safely skip
- * the validation of check constraints and mark them as valid based on the
- * constraint enforcement flag, since NOT ENFORCED constraints must always
- * be marked as NOT VALID. (This will override any user-supplied NOT VALID
- * flag.)
+ * If creating a new table (but not a foreign table), we can safely skip
+ * validation of check constraints, and nonetheless mark them valid. (This
+ * will override any user-supplied NOT VALID flag.)
*/
if (skipValidation)
{
@@ -2955,7 +2952,7 @@ transformCheckConstraints(CreateStmtContext *cxt, bool skipValidation)
Constraint *constraint = (Constraint *) lfirst(ckclist);
constraint->skip_validation = true;
- constraint->initially_valid = constraint->is_enforced;
+ constraint->initially_valid = true;
}
}
}
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 54dad975553..41751181f61 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -2594,12 +2594,10 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
appendStringInfoString(&buf, " DEFERRABLE");
if (conForm->condeferred)
appendStringInfoString(&buf, " INITIALLY DEFERRED");
-
- /* Validated status is irrelevant when the constraint is NOT ENFORCED. */
+ if (!conForm->convalidated)
+ appendStringInfoString(&buf, " NOT VALID");
if (!conForm->conenforced)
appendStringInfoString(&buf, " NOT ENFORCED");
- else if (!conForm->convalidated)
- appendStringInfoString(&buf, " NOT VALID");
/* Cleanup */
systable_endscan(scandesc);
diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out
index 362f38856d2..54bdb3f2250 100644
--- a/src/test/regress/expected/alter_table.out
+++ b/src/test/regress/expected/alter_table.out
@@ -507,7 +507,9 @@ ALTER TABLE attmp3 validate constraint attmpconstr;
ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten CHECK (b > 10); -- fail
ERROR: check constraint "b_greater_than_ten" of relation "attmp3" is violated by some row
ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten CHECK (b > 10) NOT VALID; -- succeeds
-ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten_not_enforced CHECK (b > 10) NOT ENFORCED; -- succeeds
+ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten_not_enforced CHECK (b > 10) NOT ENFORCED; -- fail
+ERROR: check constraint "b_greater_than_ten_not_enforced" of relation "attmp3" is violated by some row
+ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten_not_enforced CHECK (b > 10) NOT VALID NOT ENFORCED; -- succeeds
ALTER TABLE attmp3 VALIDATE CONSTRAINT b_greater_than_ten; -- fails
ERROR: check constraint "b_greater_than_ten" of relation "attmp3" is violated by some row
DELETE FROM attmp3 WHERE NOT b > 10;
diff --git a/src/test/regress/expected/inherit.out b/src/test/regress/expected/inherit.out
index 420b6ae5996..f34555e7daa 100644
--- a/src/test/regress/expected/inherit.out
+++ b/src/test/regress/expected/inherit.out
@@ -1332,6 +1332,18 @@ NOTICE: merging constraint "inh_check_constraint5" with inherited definition
alter table p1 add constraint inh_check_constraint6 check (f1 < 10) not enforced;
alter table p1_c1 add constraint inh_check_constraint6 check (f1 < 10) enforced;
NOTICE: merging constraint "inh_check_constraint6" with inherited definition
+-- however, cannot merge a not valid enforced child constraint with a valid
+-- not-enforced parent constraint.
+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;
+ERROR: constraint "inh_check_constraint9" conflicts with NOT VALID constraint on relation "p1_c1"
+-- but, allowed if the parent constraint is also invalid.
+alter table p1 add constraint inh_check_constraint9 check (f1 < 10) not valid not enforced;
+NOTICE: merging constraint "inh_check_constraint9" with inherited definition
+-- the invalid state of the child constraint will be ignored here.
+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
create table p1_c2(f1 int constraint inh_check_constraint4 check (f1 < 10)) inherits(p1);
NOTICE: merging column "f1" with inherited definition
NOTICE: merging constraint "inh_check_constraint4" with inherited definition
@@ -1353,42 +1365,50 @@ create table p1_fail(f1 int constraint inh_check_constraint6 check (f1 < 10) not
NOTICE: merging multiple inherited definitions of column "f1"
NOTICE: merging column "f1" with inherited definition
ERROR: constraint "inh_check_constraint6" conflicts with NOT ENFORCED constraint on relation "p1_fail"
-select conrelid::regclass::text as relname, conname, conislocal, coninhcount, conenforced
+select conrelid::regclass::text as relname, conname, conislocal, coninhcount, conenforced, convalidated
from pg_constraint where conname like 'inh\_check\_constraint%'
order by 1, 2;
- relname | conname | conislocal | coninhcount | conenforced
----------+-----------------------+------------+-------------+-------------
- p1 | inh_check_constraint1 | t | 0 | t
- p1 | inh_check_constraint2 | t | 0 | t
- p1 | inh_check_constraint3 | t | 0 | f
- p1 | inh_check_constraint4 | t | 0 | f
- p1 | inh_check_constraint5 | t | 0 | f
- p1 | inh_check_constraint6 | t | 0 | f
- p1 | inh_check_constraint8 | t | 0 | t
- p1_c1 | inh_check_constraint1 | t | 1 | t
- p1_c1 | inh_check_constraint2 | t | 1 | t
- p1_c1 | inh_check_constraint3 | t | 1 | f
- p1_c1 | inh_check_constraint4 | t | 1 | f
- p1_c1 | inh_check_constraint5 | t | 1 | t
- p1_c1 | inh_check_constraint6 | t | 1 | t
- p1_c1 | inh_check_constraint7 | t | 0 | f
- p1_c1 | inh_check_constraint8 | f | 1 | t
- p1_c2 | inh_check_constraint1 | f | 1 | t
- p1_c2 | inh_check_constraint2 | f | 1 | t
- p1_c2 | inh_check_constraint3 | f | 1 | f
- p1_c2 | inh_check_constraint4 | t | 1 | t
- p1_c2 | inh_check_constraint5 | f | 1 | f
- p1_c2 | inh_check_constraint6 | f | 1 | f
- p1_c2 | inh_check_constraint8 | f | 1 | t
- p1_c3 | inh_check_constraint1 | f | 2 | t
- p1_c3 | inh_check_constraint2 | f | 2 | t
- p1_c3 | inh_check_constraint3 | f | 2 | f
- p1_c3 | inh_check_constraint4 | f | 2 | f
- p1_c3 | inh_check_constraint5 | f | 2 | t
- p1_c3 | inh_check_constraint6 | f | 2 | t
- p1_c3 | inh_check_constraint7 | f | 1 | f
- p1_c3 | inh_check_constraint8 | f | 2 | t
-(30 rows)
+ relname | conname | conislocal | coninhcount | conenforced | convalidated
+---------+------------------------+------------+-------------+-------------+--------------
+ p1 | inh_check_constraint1 | t | 0 | t | t
+ p1 | inh_check_constraint10 | t | 0 | f | t
+ p1 | inh_check_constraint2 | t | 0 | t | t
+ p1 | inh_check_constraint3 | t | 0 | f | t
+ p1 | inh_check_constraint4 | t | 0 | f | t
+ p1 | inh_check_constraint5 | t | 0 | f | t
+ p1 | inh_check_constraint6 | t | 0 | f | t
+ 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_constraint2 | t | 1 | t | t
+ p1_c1 | inh_check_constraint3 | t | 1 | f | t
+ p1_c1 | inh_check_constraint4 | t | 1 | f | t
+ p1_c1 | inh_check_constraint5 | t | 1 | t | t
+ p1_c1 | inh_check_constraint6 | t | 1 | t | t
+ p1_c1 | inh_check_constraint7 | t | 0 | f | t
+ p1_c1 | inh_check_constraint8 | f | 1 | t | t
+ p1_c1 | inh_check_constraint9 | t | 1 | t | f
+ p1_c2 | inh_check_constraint1 | f | 1 | t | t
+ p1_c2 | inh_check_constraint10 | f | 1 | f | t
+ p1_c2 | inh_check_constraint2 | f | 1 | t | t
+ p1_c2 | inh_check_constraint3 | f | 1 | f | t
+ p1_c2 | inh_check_constraint4 | t | 1 | t | t
+ p1_c2 | inh_check_constraint5 | f | 1 | f | t
+ p1_c2 | inh_check_constraint6 | f | 1 | f | t
+ p1_c2 | inh_check_constraint8 | f | 1 | t | t
+ p1_c2 | inh_check_constraint9 | f | 1 | f | t
+ p1_c3 | inh_check_constraint1 | f | 2 | t | t
+ p1_c3 | inh_check_constraint10 | f | 2 | t | t
+ p1_c3 | inh_check_constraint2 | f | 2 | t | t
+ p1_c3 | inh_check_constraint3 | f | 2 | f | t
+ p1_c3 | inh_check_constraint4 | f | 2 | f | t
+ p1_c3 | inh_check_constraint5 | f | 2 | t | t
+ p1_c3 | inh_check_constraint6 | f | 2 | t | t
+ p1_c3 | inh_check_constraint7 | f | 1 | f | t
+ p1_c3 | inh_check_constraint8 | f | 2 | t | t
+ p1_c3 | inh_check_constraint9 | f | 2 | t | t
+(38 rows)
drop table p1 cascade;
NOTICE: drop cascades to 3 other objects
diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql
index 84e93ef575e..a9268de14b5 100644
--- a/src/test/regress/sql/alter_table.sql
+++ b/src/test/regress/sql/alter_table.sql
@@ -387,7 +387,8 @@ ALTER TABLE attmp3 validate constraint attmpconstr;
-- Try a non-verified CHECK constraint
ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten CHECK (b > 10); -- fail
ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten CHECK (b > 10) NOT VALID; -- succeeds
-ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten_not_enforced CHECK (b > 10) NOT ENFORCED; -- succeeds
+ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten_not_enforced CHECK (b > 10) NOT ENFORCED; -- fail
+ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten_not_enforced CHECK (b > 10) NOT VALID NOT ENFORCED; -- succeeds
ALTER TABLE attmp3 VALIDATE CONSTRAINT b_greater_than_ten; -- fails
DELETE FROM attmp3 WHERE NOT b > 10;
ALTER TABLE attmp3 VALIDATE CONSTRAINT b_greater_than_ten; -- succeeds
diff --git a/src/test/regress/sql/inherit.sql b/src/test/regress/sql/inherit.sql
index 30fba16231c..34104d9002f 100644
--- a/src/test/regress/sql/inherit.sql
+++ b/src/test/regress/sql/inherit.sql
@@ -481,6 +481,17 @@ alter table p1 add constraint inh_check_constraint5 check (f1 < 10) not enforced
alter table p1 add constraint inh_check_constraint6 check (f1 < 10) not enforced;
alter table p1_c1 add constraint inh_check_constraint6 check (f1 < 10) enforced;
+-- however, cannot merge a not valid enforced child constraint with a valid
+-- not-enforced parent constraint.
+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;
+-- but, allowed if the parent constraint is also invalid.
+alter table p1 add constraint inh_check_constraint9 check (f1 < 10) not valid not enforced;
+
+-- the invalid state of the child constraint will be ignored here.
+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;
+
create table p1_c2(f1 int constraint inh_check_constraint4 check (f1 < 10)) inherits(p1);
-- but reverse is not allowed
@@ -498,7 +509,7 @@ create table p1_c3() inherits(p1, p1_c1);
-- but not allowed if the child constraint is explicitly asked to be NOT ENFORCED
create table p1_fail(f1 int constraint inh_check_constraint6 check (f1 < 10) not enforced) inherits(p1, p1_c1);
-select conrelid::regclass::text as relname, conname, conislocal, coninhcount, conenforced
+select conrelid::regclass::text as relname, conname, conislocal, coninhcount, conenforced, convalidated
from pg_constraint where conname like 'inh\_check\_constraint%'
order by 1, 2;
--
2.43.5
v14-0008-Add-support-for-NOT-ENFORCED-in-foreign-key-cons.patchapplication/x-patch; name=v14-0008-Add-support-for-NOT-ENFORCED-in-foreign-key-cons.patchDownload
From 544cc8baddb18ce96602a26e0fbc71cbe9e8d2d9 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 20 Jan 2025 21:45:07 +0530
Subject: [PATCH v14 8/9] Add support for NOT ENFORCED in foreign key
constraints.
Typically, when a foreign key (FK) constraint is created on a table,
action and check triggers are added to maintain data integrity. With
this patch, if a constraint is marked as NOT ENFORCED, integrity
checks are no longer required, making these triggers unnecessary.
Consequently, when creating a NOT ENFORCED FK constraint, triggers
will not be created, and the constraint will be marked as NOT VALID.
Similarly, if an existing FK constraint is changed to NOT ENFORCED,
the associated triggers will be dropped, and the constraint will also
be marked as NOT VALID. Conversely, if a NOT ENFORCED FK constraint is
changed to ENFORCED, the necessary triggers will be created, and the
constraint will remain in the NOT VALID state.
----
NOTE: In this patch, the tryAttachPartitionForeignKey() function will
not merge the constraint if the enforcibility differs. This will be
addressed in the next patch.
----
---
doc/src/sgml/advanced.sgml | 13 +
doc/src/sgml/catalogs.sgml | 2 +-
doc/src/sgml/ddl.sgml | 12 +-
doc/src/sgml/ref/alter_table.sgml | 5 +-
doc/src/sgml/ref/create_table.sgml | 2 +-
src/backend/catalog/pg_constraint.c | 5 +-
src/backend/catalog/sql_features.txt | 2 +-
src/backend/commands/tablecmds.c | 489 ++++++++++++++++------
src/backend/parser/gram.y | 11 +-
src/backend/parser/parse_utilcmd.c | 6 +-
src/backend/utils/cache/relcache.c | 1 +
src/include/nodes/parsenodes.h | 6 +
src/include/utils/rel.h | 3 +
src/test/regress/expected/constraints.out | 11 +-
src/test/regress/expected/foreign_key.out | 158 ++++++-
src/test/regress/sql/constraints.sql | 1 -
src/test/regress/sql/foreign_key.sql | 104 ++++-
17 files changed, 679 insertions(+), 152 deletions(-)
diff --git a/doc/src/sgml/advanced.sgml b/doc/src/sgml/advanced.sgml
index e15a3323dfb..14ed11e1f4d 100644
--- a/doc/src/sgml/advanced.sgml
+++ b/doc/src/sgml/advanced.sgml
@@ -125,6 +125,19 @@ ERROR: insert or update on table "weather" violates foreign key constraint "wea
DETAIL: Key (city)=(Berkeley) is not present in table "cities".
</screen>
</para>
+ <para>
+ If, for some reason, you do not want to enforce this constraint and wish to
+ avoid the error, you can modify the foreign key constraint to <literal>NOT ENFORCED</literal>,
+ or specify it in the <link linkend="sql-createtable"><command>CREATE TABLE</command></link>.
+ </para>
+ <para>
+ Let's change the constraint to <literal>NOT ENFORCED</literal>, and when you
+ attempt to reinsert the same record, it will not result in any error.
+<programlisting>
+ALTER TABLE weather ALTER CONSTRAINT weather_city_fkey NOT ENFORCED;
+INSERT INTO weather VALUES ('Berkeley', 45, 53, 0.0, '1994-11-28');
+</programlisting>
+ </para>
<para>
The behavior of foreign keys can be finely tuned to your
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index ee59a7e15d0..3831c68eeb1 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2599,7 +2599,7 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l
</para>
<para>
Is the constraint enforced?
- Currently, can be false only for CHECK constraints
+ Currently, can be false only for foreign keys and CHECK constraints
</para></entry>
</row>
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index ae156b6b1cd..90f4264f6d7 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -1081,11 +1081,13 @@ CREATE TABLE example (
</indexterm>
<para>
- A foreign key constraint specifies that the values in a column (or
- a group of columns) must match the values appearing in some row
- of another table.
- We say this maintains the <firstterm>referential
- integrity</firstterm> between two related tables.
+ A foreign key constraint with <literal>ENFORCED</literal>, the default
+ setting, specifies that the values in a column (or a group of columns)
+ must match the values appearing in some row of another table. We say this
+ maintains the <firstterm>referential integrity</firstterm> between two
+ related tables. If a foreign key constraint is created with <literal>NOT ENFORCED</literal>,
+ the check for aforementioned <firstterm>referential integrity</firstterm>
+ will not be performed.
</para>
<para>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 8e56b8e59b0..3d7cd0842de 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -58,7 +58,7 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable>
ALTER [ COLUMN ] <replaceable class="parameter">column_name</replaceable> SET COMPRESSION <replaceable class="parameter">compression_method</replaceable>
ADD <replaceable class="parameter">table_constraint</replaceable> [ NOT VALID ]
ADD <replaceable class="parameter">table_constraint_using_index</replaceable>
- ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+ ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]
VALIDATE CONSTRAINT <replaceable class="parameter">constraint_name</replaceable>
DROP CONSTRAINT [ IF EXISTS ] <replaceable class="parameter">constraint_name</replaceable> [ RESTRICT | CASCADE ]
DISABLE TRIGGER [ <replaceable class="parameter">trigger_name</replaceable> | ALL | USER ]
@@ -568,7 +568,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
This form validates a foreign key or check constraint that was
previously created as <literal>NOT VALID</literal>, by scanning the
table to ensure there are no rows for which the constraint is not
- satisfied. Nothing happens if the constraint is already marked valid.
+ satisfied. If the constraint is not enforced, an error is thrown.
+ Nothing happens if the constraint is already marked valid.
(See <xref linkend="sql-altertable-notes"/> below for an explanation
of the usefulness of this command.)
</para>
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index 0a3e520f215..99bffe6b749 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -1405,7 +1405,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</para>
<para>
- This is currently only supported for <literal>CHECK</literal>
+ This is currently only supported for foreign key and <literal>CHECK</literal>
constraints.
</para>
</listitem>
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index 60bc54a96b6..aed9c0c8d9f 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -100,8 +100,9 @@ CreateConstraintEntry(const char *constraintName,
ObjectAddresses *addrs_auto;
ObjectAddresses *addrs_normal;
- /* Only CHECK constraint can be not enforced */
- Assert(isEnforced || constraintType == CONSTRAINT_CHECK);
+ /* Only CHECK or FOREIGN KEY constraint can be not enforced */
+ Assert(isEnforced || constraintType == CONSTRAINT_CHECK ||
+ constraintType == CONSTRAINT_FOREIGN);
conDesc = table_open(ConstraintRelationId, RowExclusiveLock);
diff --git a/src/backend/catalog/sql_features.txt b/src/backend/catalog/sql_features.txt
index 2f250d2c57b..d49471bbdf2 100644
--- a/src/backend/catalog/sql_features.txt
+++ b/src/backend/catalog/sql_features.txt
@@ -281,7 +281,7 @@ F461 Named character sets NO
F471 Scalar subquery values YES
F481 Expanded NULL predicate YES
F491 Constraint management YES
-F492 Optional table constraint enforcement NO check constraints only
+F492 Optional table constraint enforcement NO foreign key and check constraints only
F501 Features and conformance views YES
F501 Features and conformance views 01 SQL_FEATURES view YES
F501 Features and conformance views 02 SQL_SIZING view YES
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 044dd4023a4..18d7cb26c7f 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -389,17 +389,33 @@ static void AlterIndexNamespaces(Relation classRel, Relation rel,
static void AlterSeqNamespaces(Relation classRel, Relation rel,
Oid oldNspOid, Oid newNspOid, ObjectAddresses *objsMoved,
LOCKMODE lockmode);
-static ObjectAddress ATExecAlterConstraint(Relation rel, ATAlterConstraint *cmdcon,
- bool recurse, bool recursing, LOCKMODE lockmode);
-static bool ATExecAlterConstrRecurse(ATAlterConstraint *cmdcon, Relation conrel,
- Relation tgrel, const Oid fkrelid, const Oid pkrelid,
+static ObjectAddress ATExecAlterConstraint(List **wqueue, Relation rel,
+ AlterConstraintStmt *cmdcon,
+ bool recurse, bool recursing,
+ LOCKMODE lockmode);
+static bool ATExecAlterConstrRecurse(List **wqueue, AlterConstraintStmt *cmdcon,
+ Relation conrel, Relation tgrel,
+ const Oid fkrelid, const Oid pkrelid,
HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode);
-static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
+ LOCKMODE lockmode, Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger);
+static void ATExecAlterConstrEnforceability(List **wqueue, AlterConstraintStmt *cmdcon,
+ Relation conrel, Relation tgrel,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode,
+ Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger);
+static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Oid fkrel,
bool deferrable, bool initdeferred,
List **otherrelids);
-static void ATExecAlterChildConstr(ATAlterConstraint *cmdcon, Relation conrel,
- Relation tgrel, const Oid fkrelid, const Oid pkrelid,
+static void ATExecAlterChildConstr(List **wqueue, AlterConstraintStmt *cmdcon,
+ Relation conrel, Relation tgrel,
+ const Oid fkrelid, const Oid pkrelid,
HeapTuple contuple, List **otherrelids,
LOCKMODE lockmode);
static ObjectAddress ATExecValidateConstraint(List **wqueue,
@@ -5457,7 +5473,7 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
break;
case AT_AlterConstraint: /* ALTER CONSTRAINT */
address =
- ATExecAlterConstraint(rel,
+ ATExecAlterConstraint(wqueue, rel,
castNode(ATAlterConstraint, cmd->def),
false, false, lockmode);
break;
@@ -10530,7 +10546,7 @@ addFkConstraint(addFkConstraintSides fkside,
CONSTRAINT_FOREIGN,
fkconstraint->deferrable,
fkconstraint->initdeferred,
- true, /* Is Enforced */
+ fkconstraint->is_enforced,
fkconstraint->initially_valid,
parentConstr,
RelationGetRelid(rel),
@@ -10648,21 +10664,23 @@ addFkRecurseReferenced(Constraint *fkconstraint, Relation rel,
Oid parentDelTrigger, Oid parentUpdTrigger,
bool with_period)
{
- Oid deleteTriggerOid,
- updateTriggerOid;
+ Oid deleteTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
Assert(CheckRelationLockedByMe(pkrel, ShareRowExclusiveLock, true));
Assert(CheckRelationLockedByMe(rel, ShareRowExclusiveLock, true));
/*
- * Create the action triggers that enforce the constraint.
+ * Create action triggers to enforce the constraint, or skip them if the
+ * constraint is not enforced.
*/
- createForeignKeyActionTriggers(RelationGetRelid(rel),
- RelationGetRelid(pkrel),
- fkconstraint,
- parentConstr, indexOid,
- parentDelTrigger, parentUpdTrigger,
- &deleteTriggerOid, &updateTriggerOid);
+ if (fkconstraint->is_enforced)
+ createForeignKeyActionTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
+ fkconstraint,
+ parentConstr, indexOid,
+ parentDelTrigger, parentUpdTrigger,
+ &deleteTriggerOid, &updateTriggerOid);
/*
* If the referenced table is partitioned, recurse on ourselves to handle
@@ -10783,8 +10801,8 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
Oid parentInsTrigger, Oid parentUpdTrigger,
bool with_period)
{
- Oid insertTriggerOid,
- updateTriggerOid;
+ Oid insertTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
Assert(OidIsValid(parentConstr));
Assert(CheckRelationLockedByMe(rel, ShareRowExclusiveLock, true));
@@ -10796,29 +10814,32 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
errmsg("foreign key constraints are not supported on foreign tables")));
/*
- * Add the check triggers to it and, if necessary, schedule it to be
- * checked in Phase 3.
+ * Add check triggers if the constraint is enforced, and if needed,
+ * schedule them to be checked in Phase 3.
*
* If the relation is partitioned, drill down to do it to its partitions.
*/
- createForeignKeyCheckTriggers(RelationGetRelid(rel),
- RelationGetRelid(pkrel),
- fkconstraint,
- parentConstr,
- indexOid,
- parentInsTrigger, parentUpdTrigger,
- &insertTriggerOid, &updateTriggerOid);
+ if (fkconstraint->is_enforced)
+ createForeignKeyCheckTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
+ fkconstraint,
+ parentConstr,
+ indexOid,
+ parentInsTrigger, parentUpdTrigger,
+ &insertTriggerOid, &updateTriggerOid);
if (rel->rd_rel->relkind == RELKIND_RELATION)
{
/*
* Tell Phase 3 to check that the constraint is satisfied by existing
- * rows. We can skip this during table creation, when requested
- * explicitly by specifying NOT VALID in an ADD FOREIGN KEY command,
- * and when we're recreating a constraint following a SET DATA TYPE
- * operation that did not impugn its validity.
+ * rows. We can skip this during table creation, when constraint is
+ * specified as NOT ENFORCED, or when requested explicitly by specifying
+ * NOT VALID in an ADD FOREIGN KEY command, and when we're recreating
+ * a constraint following a SET DATA TYPE operation that did not
+ * impugn its validity.
*/
- if (wqueue && !old_check_ok && !fkconstraint->skip_validation)
+ if (wqueue && !old_check_ok && !fkconstraint->skip_validation &&
+ fkconstraint->is_enforced)
{
NewConstraint *newcon;
AlteredTableInfo *tab;
@@ -11049,8 +11070,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
AttrNumber confdelsetcols[INDEX_MAX_KEYS];
Constraint *fkconstraint;
ObjectAddress address;
- Oid deleteTriggerOid,
- updateTriggerOid;
+ Oid deleteTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(constrOid));
if (!HeapTupleIsValid(tuple))
@@ -11110,6 +11131,7 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
fkconstraint->fk_del_set_cols = NIL;
fkconstraint->old_conpfeqop = NIL;
fkconstraint->old_pktable_oid = InvalidOid;
+ fkconstraint->is_enforced = constrForm->conenforced;
fkconstraint->skip_validation = false;
fkconstraint->initially_valid = true;
@@ -11139,9 +11161,10 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
* parent OIDs for similar triggers that will be created on the
* partition in addFkRecurseReferenced().
*/
- GetForeignKeyActionTriggers(trigrel, constrOid,
- constrForm->confrelid, constrForm->conrelid,
- &deleteTriggerOid, &updateTriggerOid);
+ if (constrForm->conenforced)
+ GetForeignKeyActionTriggers(trigrel, constrOid,
+ constrForm->confrelid, constrForm->conrelid,
+ &deleteTriggerOid, &updateTriggerOid);
/* Add this constraint ... */
address = addFkConstraint(addFkReferencedSide,
@@ -11274,8 +11297,8 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
Oid indexOid;
ObjectAddress address;
ListCell *lc;
- Oid insertTriggerOid,
- updateTriggerOid;
+ Oid insertTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
bool with_period;
tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(parentConstrOid));
@@ -11307,17 +11330,18 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
mapped_conkey[i] = attmap->attnums[conkey[i] - 1];
/*
- * Get the "check" triggers belonging to the constraint to pass as
- * parent OIDs for similar triggers that will be created on the
- * partition in addFkRecurseReferencing(). They are also passed to
- * tryAttachPartitionForeignKey() below to simply assign as parents to
- * the partition's existing "check" triggers, that is, if the
- * corresponding constraints is deemed attachable to the parent
- * constraint.
+ * Get the "check" triggers belonging to the constraint, if it is
+ * enforced, to pass as parent OIDs for similar triggers that will be
+ * created on the partition in addFkRecurseReferencing(). They are
+ * also passed to tryAttachPartitionForeignKey() below to simply
+ * assign as parents to the partition's existing "check" triggers,
+ * that is, if the corresponding constraints is deemed attachable to
+ * the parent constraint.
*/
- GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
- constrForm->confrelid, constrForm->conrelid,
- &insertTriggerOid, &updateTriggerOid);
+ if (constrForm->conenforced)
+ GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
+ constrForm->confrelid, constrForm->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
/*
* Before creating a new constraint, see whether any existing FKs are
@@ -11370,6 +11394,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
fkconstraint->fk_del_set_cols = NIL;
fkconstraint->old_conpfeqop = NIL;
fkconstraint->old_pktable_oid = InvalidOid;
+ fkconstraint->is_enforced = constrForm->conenforced;
fkconstraint->skip_validation = false;
fkconstraint->initially_valid = constrForm->convalidated;
for (int i = 0; i < numfks; i++)
@@ -11487,6 +11512,7 @@ tryAttachPartitionForeignKey(List **wqueue,
if (OidIsValid(partConstr->conparentid) ||
partConstr->condeferrable != parentConstr->condeferrable ||
partConstr->condeferred != parentConstr->condeferred ||
+ partConstr->conenforced != parentConstr->conenforced ||
partConstr->confupdtype != parentConstr->confupdtype ||
partConstr->confdeltype != parentConstr->confdeltype ||
partConstr->confmatchtype != parentConstr->confmatchtype)
@@ -11530,8 +11556,7 @@ AttachPartitionForeignKey(List **wqueue,
bool queueValidation;
Oid partConstrFrelid;
Oid partConstrRelid;
- Oid insertTriggerOid,
- updateTriggerOid;
+ bool parentConstrIsEnforced;
/* Fetch the parent constraint tuple */
parentConstrTup = SearchSysCache1(CONSTROID,
@@ -11539,6 +11564,7 @@ AttachPartitionForeignKey(List **wqueue,
if (!HeapTupleIsValid(parentConstrTup))
elog(ERROR, "cache lookup failed for constraint %u", parentConstrOid);
parentConstr = (Form_pg_constraint) GETSTRUCT(parentConstrTup);
+ parentConstrIsEnforced = parentConstr->conenforced;
/* Fetch the child constraint tuple */
partcontup = SearchSysCache1(CONSTROID,
@@ -11588,17 +11614,24 @@ AttachPartitionForeignKey(List **wqueue,
/*
* Like the constraint, attach partition's "check" triggers to the
- * corresponding parent triggers.
+ * corresponding parent triggers if the constraint is enforced.
+ * Non-enforced constraints do not have these triggers.
*/
- GetForeignKeyCheckTriggers(trigrel,
- partConstrOid, partConstrFrelid, partConstrRelid,
- &insertTriggerOid, &updateTriggerOid);
- Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
- TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
- RelationGetRelid(partition));
- Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
- TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
- RelationGetRelid(partition));
+ if (parentConstrIsEnforced)
+ {
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
+ GetForeignKeyCheckTriggers(trigrel,
+ partConstrOid, partConstrFrelid, partConstrRelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
+ RelationGetRelid(partition));
+ Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
+ RelationGetRelid(partition));
+ }
/*
* We updated this pg_constraint row above to set its parent; validating
@@ -11712,6 +11745,10 @@ RemoveInheritedConstraint(Relation conrel, Relation trigrel, Oid conoid,
*
* The subroutine for tryAttachPartitionForeignKey handles the deletion of
* action triggers for the foreign key constraint.
+ *
+ * If valid confrelid and conrelid values are not provided, the respective
+ * trigger check will be skipped, and the trigger will be considered for
+ * removal.
*/
static void
DropForeignKeyConstraintTriggers(Relation trigrel, Oid conoid, Oid confrelid,
@@ -11732,10 +11769,27 @@ DropForeignKeyConstraintTriggers(Relation trigrel, Oid conoid, Oid confrelid,
Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
ObjectAddress trigger;
- if (trgform->tgconstrrelid != conrelid)
+ /* Invalid if trigger is not for a referential integrity constraint */
+ if (!OidIsValid(trgform->tgconstrrelid))
continue;
- if (trgform->tgrelid != confrelid)
+ if (OidIsValid(conrelid) && trgform->tgconstrrelid != conrelid)
continue;
+ if (OidIsValid(confrelid) && trgform->tgrelid != confrelid)
+ continue;
+
+ /* We should be droping trigger related to foreign ke constraint */
+ Assert(trgform->tgfoid == F_RI_FKEY_CHECK_INS ||
+ trgform->tgfoid == F_RI_FKEY_CHECK_UPD ||
+ trgform->tgfoid == F_RI_FKEY_CASCADE_DEL ||
+ trgform->tgfoid == F_RI_FKEY_CASCADE_UPD ||
+ trgform->tgfoid == F_RI_FKEY_RESTRICT_DEL ||
+ trgform->tgfoid == F_RI_FKEY_RESTRICT_UPD ||
+ trgform->tgfoid == F_RI_FKEY_SETNULL_DEL ||
+ trgform->tgfoid == F_RI_FKEY_SETNULL_UPD ||
+ trgform->tgfoid == F_RI_FKEY_SETDEFAULT_DEL ||
+ trgform->tgfoid == F_RI_FKEY_SETDEFAULT_UPD ||
+ trgform->tgfoid == F_RI_FKEY_NOACTION_DEL ||
+ trgform->tgfoid == F_RI_FKEY_NOACTION_UPD);
/*
* The constraint is originally set up to contain this trigger as an
@@ -11892,8 +11946,8 @@ GetForeignKeyCheckTriggers(Relation trigrel,
* InvalidObjectAddress.
*/
static ObjectAddress
-ATExecAlterConstraint(Relation rel, ATAlterConstraint *cmdcon, bool recurse,
- bool recursing, LOCKMODE lockmode)
+ATExecAlterConstraint(List **wqueue, Relation rel, AlterConstraintStmt *cmdcon,
+ bool recurse, bool recursing, LOCKMODE lockmode)
{
Relation conrel;
Relation tgrel;
@@ -11935,10 +11989,19 @@ ATExecAlterConstraint(Relation rel, ATAlterConstraint *cmdcon, bool recurse,
currcon = (Form_pg_constraint) GETSTRUCT(contuple);
if (currcon->contype != CONSTRAINT_FOREIGN)
- ereport(ERROR,
- (errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("constraint \"%s\" of relation \"%s\" is not a foreign key constraint",
- cmdcon->conname, RelationGetRelationName(rel))));
+ {
+ if (cmdcon->alterEnforceability)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot alter enforceability of constraint \"%s\" of relation \"%s\"",
+ cmdcon->conname, RelationGetRelationName(rel)),
+ errdetail("Enforceability can only be altered for foreign key constraints.")));
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("constraint \"%s\" of relation \"%s\" is not a foreign key constraint",
+ cmdcon->conname, RelationGetRelationName(rel))));
+ }
/*
* If it's not the topmost constraint, raise an error.
@@ -11990,13 +12053,14 @@ ATExecAlterConstraint(Relation rel, ATAlterConstraint *cmdcon, bool recurse,
* processed regardless, in case they had the constraint locally changed.
*/
address = InvalidObjectAddress;
- if (currcon->condeferrable != cmdcon->deferrable ||
- currcon->condeferred != cmdcon->initdeferred ||
+ if (cmdcon->alterDeferrability || cmdcon->alterEnforceability ||
rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
- if (ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, currcon->conrelid,
- currcon->confrelid, contuple, &otherrelids,
- lockmode))
+ if (ATExecAlterConstrRecurse(wqueue, cmdcon, conrel, tgrel,
+ currcon->conrelid, currcon->confrelid,
+ contuple, &otherrelids, lockmode,
+ InvalidOid, InvalidOid, InvalidOid,
+ InvalidOid))
ObjectAddressSet(address, ConstraintRelationId, currcon->oid);
}
@@ -12028,10 +12092,14 @@ ATExecAlterConstraint(Relation rel, ATAlterConstraint *cmdcon, bool recurse,
* but existing releases don't do that.)
*/
static bool
-ATExecAlterConstrRecurse(ATAlterConstraint *cmdcon, Relation conrel,
- Relation tgrel, const Oid fkrelid, const Oid pkrelid,
- HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode)
+ATExecAlterConstrRecurse(List **wqueue, AlterConstraintStmt *cmdcon,
+ Relation conrel, Relation tgrel, const Oid fkrelid,
+ const Oid pkrelid, HeapTuple contuple,
+ List **otherrelids, LOCKMODE lockmode,
+ Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger)
{
Form_pg_constraint currcon;
Oid conoid;
@@ -12055,15 +12123,24 @@ ATExecAlterConstrRecurse(ATAlterConstraint *cmdcon, Relation conrel,
* silently do nothing.
*/
if (currcon->condeferrable != cmdcon->deferrable ||
- currcon->condeferred != cmdcon->initdeferred)
+ currcon->condeferred != cmdcon->initdeferred ||
+ currcon->conenforced != cmdcon->is_enforced)
{
HeapTuple copyTuple;
Form_pg_constraint copy_con;
copyTuple = heap_copytuple(contuple);
copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
- copy_con->condeferrable = cmdcon->deferrable;
- copy_con->condeferred = cmdcon->initdeferred;
+
+ if (cmdcon->alterDeferrability)
+ {
+ copy_con->condeferrable = cmdcon->deferrable;
+ copy_con->condeferred = cmdcon->initdeferred;
+ }
+
+ if (cmdcon->alterEnforceability)
+ copy_con->conenforced = cmdcon->is_enforced;
+
CatalogTupleUpdate(conrel, ©Tuple->t_self, copyTuple);
InvokeObjectPostAlterHook(ConstraintRelationId,
@@ -12074,32 +12151,200 @@ ATExecAlterConstrRecurse(ATAlterConstraint *cmdcon, Relation conrel,
/* Make new constraint flags visible to others */
CacheInvalidateRelcache(rel);
+ }
+ /*
+ * Note that even if deferrability is requested to be altered along with
+ * enforceability, we don't need to call AlterConstrTriggerDeferrability()
+ * explicitly. Changing enforceability involves either creating or
+ * dropping the trigger, during which the deferrability will be set
+ * accordingly.
+ */
+ if (cmdcon->alterEnforceability &&
+ currcon->conenforced != cmdcon->is_enforced)
+ {
+ ATExecAlterConstrEnforceability(wqueue, cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, contuple, otherrelids, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
+ /*
+ * Tell Phase 3 to check that the constraint is satisfied by existing
+ * rows, but skip this step if the constraint is NOT VALID.
+ */
+ if (cmdcon->is_enforced && currcon->convalidated &&
+ rel->rd_rel->relkind == RELKIND_RELATION)
+ {
+ AlteredTableInfo *tab;
+ NewConstraint *newcon;
+ Constraint *fkconstraint;
+
+ /* Queue validation for phase 3 */
+ fkconstraint = makeNode(Constraint);
+ /* for now this is all we need */
+ fkconstraint->conname = pstrdup(NameStr(currcon->conname));
+
+ newcon = (NewConstraint *) palloc0(sizeof(NewConstraint));
+ newcon->name = fkconstraint->conname;
+ newcon->contype = CONSTR_FOREIGN;
+ newcon->refrelid = currcon->confrelid;
+ newcon->refindid = currcon->conindid;
+ newcon->conid = conoid;
+ newcon->qual = (Node *) fkconstraint;
+
+ /* Find or create work queue entry for this table */
+ tab = ATGetQueueEntry(wqueue, rel);
+ tab->constraints = lappend(tab->constraints, newcon);
+ }
+ }
+ else
+ {
/*
* Now we need to update the multiple entries in pg_trigger that
* implement the constraint.
*/
- AlterConstrTriggerDeferrability(conoid, tgrel, rel, cmdcon->deferrable,
- cmdcon->initdeferred, otherrelids);
+ if (cmdcon->alterDeferrability &&
+ (currcon->condeferrable != cmdcon->deferrable ||
+ currcon->condeferred != cmdcon->initdeferred))
+ AlterConstrTriggerDeferrability(conoid, tgrel,
+ RelationGetRelid(rel),
+ cmdcon->deferrable,
+ cmdcon->initdeferred,
+ otherrelids);
+
+ /*
+ * If the table at either end of the constraint is partitioned, we
+ * need to recurse and handle every constraint that is a child of this
+ * one.
+ *
+ * (This assumes that the recurse flag is forcibly set for partitioned
+ * tables, and not set for legacy inheritance, though we don't check
+ * for that here.)
+ */
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)
+ ATExecAlterChildConstr(wqueue, cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, contuple, otherrelids, lockmode);
}
- /*
- * If the table at either end of the constraint is partitioned, we need to
- * recurse and handle every constraint that is a child of this one.
- *
- * (This assumes that the recurse flag is forcibly set for partitioned
- * tables, and not set for legacy inheritance, though we don't check for
- * that here.)
- */
- if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
- get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)
- ATExecAlterChildConstr(cmdcon, conrel, tgrel, fkrelid, pkrelid,
- contuple, otherrelids, lockmode);
table_close(rel, NoLock);
return changed;
}
+/*
+ * A subroutine of ATExecAlterConstrRecurse that updates the enforceability of
+ * a foreign key constraint. Depending on whether the constraint is being set
+ * to enforced or not enforced, it creates or drops the trigger accordingly.
+ *
+ * The arguments to this function have the same meaning as the arguments to
+ * ATExecAlterConstrRecurse.
+ */
+static void
+ATExecAlterConstrEnforceability(List **wqueue, AlterConstraintStmt *cmdcon,
+ Relation conrel, Relation tgrel,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode,
+ Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger)
+{
+ Form_pg_constraint currcon;
+ Oid conoid;
+
+ currcon = (Form_pg_constraint) GETSTRUCT(contuple);
+ conoid = currcon->oid;
+
+ /* Drop triggers */
+ if (!cmdcon->is_enforced)
+ {
+ /*
+ * When setting a constraint to NOT ENFORCED, the constraint triggers
+ * need to be dropped. Therefore, we must process the child relations
+ * first, followed by the parent, to account for dependencies.
+ */
+ if (get_rel_relkind(currcon->conrelid) == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE)
+ ATExecAlterChildConstr(wqueue, cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, contuple, otherrelids, lockmode);
+
+ /* Drop all the triggers */
+ DropForeignKeyConstraintTriggers(tgrel, conoid, InvalidOid, InvalidOid);
+ }
+ else /* Create triggers */
+ {
+ Oid ReferencedDelTriggerOid = InvalidOid,
+ ReferencedUpdTriggerOid = InvalidOid,
+ ReferencingInsTriggerOid = InvalidOid,
+ ReferencingUpdTriggerOid = InvalidOid;
+
+ /* Prepare the minimal information required for trigger creation. */
+ Constraint *fkconstraint = makeNode(Constraint);
+
+ fkconstraint->fk_matchtype = currcon->confmatchtype;
+ fkconstraint->fk_upd_action = currcon->confupdtype;
+ fkconstraint->fk_del_action = currcon->confdeltype;
+
+ /* Create referenced triggers */
+ if (currcon->conrelid == fkrelid)
+ createForeignKeyActionTriggers(currcon->conrelid,
+ currcon->confrelid,
+ fkconstraint,
+ conoid,
+ currcon->conindid,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ &ReferencedDelTriggerOid,
+ &ReferencedUpdTriggerOid);
+
+ /* Create referencing triggers */
+ if (currcon->confrelid == pkrelid)
+ createForeignKeyCheckTriggers(currcon->conrelid,
+ pkrelid,
+ fkconstraint,
+ conoid,
+ currcon->conindid,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger,
+ &ReferencingInsTriggerOid,
+ &ReferencingUpdTriggerOid);
+
+ /*
+ * If the table at either end of the constraint is partitioned, we
+ * need to recurse and create triggers for each constraint that is a
+ * child of this one.
+ */
+ if (get_rel_relkind(currcon->conrelid) == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE)
+ {
+ ScanKeyData pkey;
+ SysScanDesc pscan;
+ HeapTuple childtup;
+
+ ScanKeyInit(&pkey,
+ Anum_pg_constraint_conparentid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+
+ pscan = systable_beginscan(conrel, ConstraintParentIndexId,
+ true, NULL, 1, &pkey);
+
+ while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
+ ATExecAlterConstrRecurse(wqueue, cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, childtup, otherrelids,
+ lockmode, ReferencedDelTriggerOid,
+ ReferencedUpdTriggerOid,
+ ReferencingInsTriggerOid,
+ ReferencingUpdTriggerOid);
+
+ systable_endscan(pscan);
+ }
+ }
+}
+
/*
* A subroutine of ATExecAlterConstrRecurse that updated constraint trigger's
* deferrability.
@@ -12108,7 +12353,7 @@ ATExecAlterConstrRecurse(ATAlterConstraint *cmdcon, Relation conrel,
* ATExecAlterConstrRecurse.
*/
static void
-AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
+AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Oid fkrel,
bool deferrable, bool initdeferred,
List **otherrelids)
{
@@ -12134,7 +12379,7 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
* other rels that don't have a trigger whose properties change, but
* let's be conservative.)
*/
- if (tgform->tgrelid != RelationGetRelid(rel))
+ if (tgform->tgrelid != fkrel)
*otherrelids = list_append_unique_oid(*otherrelids,
tgform->tgrelid);
@@ -12173,10 +12418,10 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
* ATExecAlterConstrRecurse.
*/
static void
-ATExecAlterChildConstr(ATAlterConstraint *cmdcon, Relation conrel,
- Relation tgrel, const Oid fkrelid, const Oid pkrelid,
- HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode)
+ATExecAlterChildConstr(List **wqueue, AlterConstraintStmt *cmdcon,
+ Relation conrel, Relation tgrel, const Oid fkrelid,
+ const Oid pkrelid, HeapTuple contuple,
+ List **otherrelids, LOCKMODE lockmode)
{
Form_pg_constraint currcon;
Oid conoid;
@@ -12196,8 +12441,9 @@ ATExecAlterChildConstr(ATAlterConstraint *cmdcon, Relation conrel,
true, NULL, 1, &pkey);
while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
- ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, fkrelid, pkrelid,
- childtup, otherrelids, lockmode);
+ ATExecAlterConstrRecurse(wqueue, cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, childtup, otherrelids, lockmode,
+ InvalidOid, InvalidOid, InvalidOid, InvalidOid);
systable_endscan(pscan);
}
@@ -20267,8 +20513,6 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
Form_pg_constraint conform;
- Oid insertTriggerOid,
- updateTriggerOid;
contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
if (!HeapTupleIsValid(contup))
@@ -20295,17 +20539,25 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
/*
* Also, look up the partition's "check" triggers corresponding to the
- * constraint being detached and detach them from the parent triggers.
+ * enforced constraint being detached and detach them from the parent
+ * triggers. Non-enforced constraints do not have these triggers;
+ * therefore, this step is not needed.
*/
- GetForeignKeyCheckTriggers(trigrel,
- fk->conoid, fk->confrelid, fk->conrelid,
- &insertTriggerOid, &updateTriggerOid);
- Assert(OidIsValid(insertTriggerOid));
- TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
- RelationGetRelid(partRel));
- Assert(OidIsValid(updateTriggerOid));
- TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
- RelationGetRelid(partRel));
+ if (fk->conenforced)
+ {
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ Assert(OidIsValid(updateTriggerOid));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ }
/*
* Lastly, create the action triggers on the referenced table, using
@@ -20345,6 +20597,7 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
fkconstraint->conname = pstrdup(NameStr(conform->conname));
fkconstraint->deferrable = conform->condeferrable;
fkconstraint->initdeferred = conform->condeferred;
+ fkconstraint->is_enforced = conform->conenforced;
fkconstraint->skip_validation = true;
fkconstraint->initially_valid = true;
/* a few irrelevant fields omitted here */
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 589f56693cd..9c8d7baf33b 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -2665,7 +2665,12 @@ alter_table_cmd:
processCASbits($4, @4, "ALTER CONSTRAINT statement",
&c->deferrable,
&c->initdeferred,
- NULL, NULL, NULL, yyscanner);
+ &c->is_enforced, NULL, NULL, yyscanner);
+ c->alterDeferrability =
+ ($4 & (CAS_NOT_DEFERRABLE | CAS_DEFERRABLE |
+ CAS_INITIALLY_IMMEDIATE | CAS_INITIALLY_IMMEDIATE)) != 0;
+ c->alterEnforceability =
+ ($4 & (CAS_NOT_ENFORCED | CAS_ENFORCED)) != 0;
$$ = (Node *) n;
}
/* ALTER TABLE <name> VALIDATE CONSTRAINT ... */
@@ -4315,8 +4320,8 @@ ConstraintElem:
n->fk_del_set_cols = ($11)->deleteAction->cols;
processCASbits($12, @12, "FOREIGN KEY",
&n->deferrable, &n->initdeferred,
- NULL, &n->skip_validation, NULL,
- yyscanner);
+ &n->is_enforced, &n->skip_validation,
+ NULL, yyscanner);
n->initially_valid = !n->skip_validation;
$$ = (Node *) n;
}
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 9053c898e06..d568eba5cac 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -3977,7 +3977,8 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
case CONSTR_ATTR_ENFORCED:
if (lastprimarycon == NULL ||
- lastprimarycon->contype != CONSTR_CHECK)
+ (lastprimarycon->contype != CONSTR_CHECK &&
+ lastprimarycon->contype != CONSTR_FOREIGN))
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("misplaced ENFORCED clause"),
@@ -3993,7 +3994,8 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
case CONSTR_ATTR_NOT_ENFORCED:
if (lastprimarycon == NULL ||
- lastprimarycon->contype != CONSTR_CHECK)
+ (lastprimarycon->contype != CONSTR_CHECK &&
+ lastprimarycon->contype != CONSTR_FOREIGN))
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("misplaced NOT ENFORCED clause"),
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index c010de4aeca..dd956b0bf4a 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4692,6 +4692,7 @@ RelationGetFKeyList(Relation relation)
info->conoid = constraint->oid;
info->conrelid = constraint->conrelid;
info->confrelid = constraint->confrelid;
+ info->conenforced = constraint->conenforced;
DeconstructFkConstraintRow(htup, &info->nkeys,
info->conkey,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index c0018abf16f..13827a6191b 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2511,10 +2511,16 @@ typedef struct ATAlterConstraint
{
NodeTag type;
+ bool alterDeferrability; /* True when any of the following flags
+ * are specifically set for modification */
char *conname; /* Constraint name */
bool deferrable; /* DEFERRABLE? */
bool initdeferred; /* INITIALLY DEFERRED? */
+
+ bool alterEnforceability; /* True when following flag is
+ * specifically set for modification */
+ bool is_enforced; /* ENFORCED? */
} ATAlterConstraint;
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index db3e504c3d2..dba0fb70146 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -284,6 +284,9 @@ typedef struct ForeignKeyCacheInfo
/* number of columns in the foreign key */
int nkeys;
+ /* Is enforced ? */
+ bool conenforced;
+
/*
* these arrays each have nkeys valid entries:
*/
diff --git a/src/test/regress/expected/constraints.out b/src/test/regress/expected/constraints.out
index 692a69fe457..75d40d06b6e 100644
--- a/src/test/regress/expected/constraints.out
+++ b/src/test/regress/expected/constraints.out
@@ -744,15 +744,12 @@ CREATE TABLE UNIQUE_NOTEN_TBL(i int UNIQUE NOT ENFORCED);
ERROR: misplaced NOT ENFORCED clause
LINE 1: CREATE TABLE UNIQUE_NOTEN_TBL(i int UNIQUE NOT ENFORCED);
^
--- XXX: error message is misleading here
ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
-ERROR: ALTER CONSTRAINT statement constraints cannot be marked ENFORCED
-LINE 1: ...TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
- ^
+ERROR: cannot alter enforceability of constraint "unique_tbl_i_key" of relation "unique_tbl"
+DETAIL: Enforceability can only be altered for foreign key constraints.
ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORCED;
-ERROR: ALTER CONSTRAINT statement constraints cannot be marked NOT ENFORCED
-LINE 1: ...ABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORC...
- ^
+ERROR: cannot alter enforceability of constraint "unique_tbl_i_key" of relation "unique_tbl"
+DETAIL: Enforceability can only be altered for foreign key constraints.
DROP TABLE unique_tbl;
--
-- EXCLUDE constraints
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 374dcb266e7..3183b1cbe8f 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -1,12 +1,43 @@
--
-- FOREIGN KEY
--
--- MATCH FULL
+-- NOT ENFORCED
--
-- First test, check and cascade
--
CREATE TABLE PKTABLE ( ptest1 int PRIMARY KEY, ptest2 text );
-CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE, ftest2 int );
+CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE NOT ENFORCED,
+ ftest2 int );
+-- Inserting into the foreign key table will not result in an error, even if
+-- there is no matching key in the referenced table.
+INSERT INTO FKTABLE VALUES (1, 2);
+INSERT INTO FKTABLE VALUES (2, 3);
+-- Check FKTABLE
+SELECT * FROM FKTABLE;
+ ftest1 | ftest2
+--------+--------
+ 1 | 2
+ 2 | 3
+(2 rows)
+
+-- Reverting it back to ENFORCED will result in failure because constraint validation will be triggered,
+-- as it was previously in a valid state.
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_fkey"
+DETAIL: Key (ftest1)=(1) is not present in table "pktable".
+-- Remove any existing rows that violate the constraint, then attempt to change
+-- it.
+TRUNCATE FKTABLE;
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+-- Any further inserts will fail due to the enforcement.
+INSERT INTO FKTABLE VALUES (3, 4);
+ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_fkey"
+DETAIL: Key (ftest1)=(3) is not present in table "pktable".
+--
+-- MATCH FULL
+--
+-- First test, check and cascade
+--
-- Insert test data into PKTABLE
INSERT INTO PKTABLE VALUES (1, 'Test1');
INSERT INTO PKTABLE VALUES (2, 'Test2');
@@ -351,6 +382,41 @@ INSERT INTO FKTABLE VALUES (1, NULL);
ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL;
ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_ftest2_fkey"
DETAIL: MATCH FULL does not allow mixing of null and nonnull key values.
+-- Modifying other attributes of a constraint should not affect its enforceability, and vice versa
+ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE NOT VALID NOT ENFORCED;
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_ftest2_fkey DEFERRABLE INITIALLY DEFERRED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fktable_ftest1_ftest2_fkey';
+ condeferrable | condeferred | conenforced | convalidated
+---------------+-------------+-------------+--------------
+ t | t | f | f
+(1 row)
+
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_ftest2_fkey NOT ENFORCED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fktable_ftest1_ftest2_fkey';
+ condeferrable | condeferred | conenforced | convalidated
+---------------+-------------+-------------+--------------
+ t | t | f | f
+(1 row)
+
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_ftest2_fkey ENFORCED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fktable_ftest1_ftest2_fkey';
+ condeferrable | condeferred | conenforced | convalidated
+---------------+-------------+-------------+--------------
+ t | t | t | f
+(1 row)
+
+-- Can change enforceability and deferrability together
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_ftest2_fkey NOT ENFORCED NOT DEFERRABLE;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fktable_ftest1_ftest2_fkey';
+ condeferrable | condeferred | conenforced | convalidated
+---------------+-------------+-------------+--------------
+ f | f | f | f
+(1 row)
+
DROP TABLE FKTABLE;
DROP TABLE PKTABLE;
-- MATCH SIMPLE
@@ -1276,6 +1342,13 @@ INSERT INTO fktable VALUES (0, 20);
ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_fk_fkey"
DETAIL: Key (fk)=(20) is not present in table "pktable".
COMMIT;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT ENFORCED;
+BEGIN;
+-- doesn't match FK, no error.
+UPDATE pktable SET id = 10 WHERE id = 5;
+-- doesn't match PK, no error.
+INSERT INTO fktable VALUES (0, 20);
+ROLLBACK;
-- try additional syntax
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE;
-- illegal option
@@ -1283,6 +1356,14 @@ ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE INITIALLY DE
ERROR: constraint declared INITIALLY DEFERRED must be DEFERRABLE
LINE 1: ...e ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE INITIALLY ...
^
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORCED;
+ERROR: conflicting constraint properties
+LINE 1: ...fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORC...
+ ^
+CREATE TEMP TABLE fktable2 (fk int references pktable ENFORCED NOT ENFORCED);
+ERROR: multiple ENFORCED/NOT ENFORCED clauses not allowed
+LINE 1: ...ABLE fktable2 (fk int references pktable ENFORCED NOT ENFORC...
+ ^
-- test order of firing of FK triggers when several RI-induced changes need to
-- be made to the same row. This was broken by subtransaction-related
-- changes in 8.0.
@@ -1580,10 +1661,12 @@ ALTER TABLE fk_partitioned_fk DROP COLUMN fdrop1;
CREATE TABLE fk_partitioned_fk_1 (fdrop1 int, fdrop2 int, a int, fdrop3 int, b int);
ALTER TABLE fk_partitioned_fk_1 DROP COLUMN fdrop1, DROP COLUMN fdrop2, DROP COLUMN fdrop3;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FROM (0,0) TO (1000,1000);
-ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk;
+ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
+ALTER TABLE fk_partitioned_fk_2 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
CREATE TABLE fk_partitioned_fk_3 (fdrop1 int, fdrop2 int, fdrop3 int, fdrop4 int, b int, a int)
PARTITION BY HASH (a);
ALTER TABLE fk_partitioned_fk_3 DROP COLUMN fdrop1, DROP COLUMN fdrop2,
@@ -1606,10 +1689,10 @@ INSERT INTO fk_partitioned_fk_1 (a,b) VALUES (500, 501);
ERROR: insert or update on table "fk_partitioned_fk_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
DETAIL: Key (a, b)=(500, 501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (1500, 1501);
-ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_2_a_b_fkey"
DETAIL: Key (a, b)=(1500, 1501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk_2 (a,b) VALUES (1500, 1501);
-ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_2_a_b_fkey"
DETAIL: Key (a, b)=(1500, 1501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (2500, 2502);
ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
@@ -1659,6 +1742,37 @@ Indexes:
Referenced by:
TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b)
+-- Check the exsting FK trigger
+CREATE TEMP TABLE tmp_trg_info AS SELECT tgtype, tgrelid::regclass, tgconstraint
+FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+SELECT count(1) FROM tmp_trg_info;
+ count
+-------
+ 14
+(1 row)
+
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+-- No triggers
+SELECT count(1) FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+ count
+-------
+ 0
+(1 row)
+
+-- Changing it back to ENFORCED will recreate the necessary triggers.
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+-- Should be exactly the same number of triggers found as before
+SELECT COUNT(1) FROM pg_trigger pgt JOIN tmp_trg_info tt
+ON (pgt.tgtype = tt.tgtype AND pgt.tgrelid = tt.tgrelid AND pgt.tgconstraint = tt.tgconstraint);
+ count
+-------
+ 14
+(1 row)
+
ALTER TABLE fk_partitioned_fk DROP CONSTRAINT fk_partitioned_fk_a_b_fkey;
-- done.
DROP TABLE fk_notpartitioned_pk, fk_partitioned_fk;
@@ -1956,6 +2070,40 @@ Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
Foreign-key constraints:
TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
+DROP TABLE fk_partitioned_fk_2;
+CREATE TABLE fk_partitioned_fk_2 (b int, a int,
+ FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
+BEGIN;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should be having two constraints
+\d fk_partitioned_fk_2
+ Table "public.fk_partitioned_fk_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ b | integer | | |
+ a | integer | | |
+Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
+Foreign-key constraints:
+ "fk_partitioned_fk_2_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED
+ TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
+
+DROP TABLE fk_partitioned_fk_2;
+ROLLBACK;
+BEGIN;
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have only one constraint
+\d fk_partitioned_fk_2
+ Table "public.fk_partitioned_fk_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ b | integer | | |
+ a | integer | | |
+Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
+Foreign-key constraints:
+ TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED
+
+ROLLBACK;
DROP TABLE fk_partitioned_fk_2;
CREATE TABLE fk_partitioned_fk_4 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE) PARTITION BY RANGE (b, a);
CREATE TABLE fk_partitioned_fk_4_1 PARTITION OF fk_partitioned_fk_4 FOR VALUES FROM (1,1) TO (100,100);
diff --git a/src/test/regress/sql/constraints.sql b/src/test/regress/sql/constraints.sql
index d6742f83fb9..e61e7324768 100644
--- a/src/test/regress/sql/constraints.sql
+++ b/src/test/regress/sql/constraints.sql
@@ -534,7 +534,6 @@ SELECT * FROM unique_tbl;
-- enforcibility cannot be specified or set for unique constrain
CREATE TABLE UNIQUE_EN_TBL(i int UNIQUE ENFORCED);
CREATE TABLE UNIQUE_NOTEN_TBL(i int UNIQUE NOT ENFORCED);
--- XXX: error message is misleading here
ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORCED;
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index bc0adb8cfe9..408a01e377b 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -2,13 +2,39 @@
-- FOREIGN KEY
--
--- MATCH FULL
+-- NOT ENFORCED
--
-- First test, check and cascade
--
CREATE TABLE PKTABLE ( ptest1 int PRIMARY KEY, ptest2 text );
-CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE, ftest2 int );
+CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE NOT ENFORCED,
+ ftest2 int );
+-- Inserting into the foreign key table will not result in an error, even if
+-- there is no matching key in the referenced table.
+INSERT INTO FKTABLE VALUES (1, 2);
+INSERT INTO FKTABLE VALUES (2, 3);
+
+-- Check FKTABLE
+SELECT * FROM FKTABLE;
+
+-- Reverting it back to ENFORCED will result in failure because constraint validation will be triggered,
+-- as it was previously in a valid state.
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+
+-- Remove any existing rows that violate the constraint, then attempt to change
+-- it.
+TRUNCATE FKTABLE;
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+
+-- Any further inserts will fail due to the enforcement.
+INSERT INTO FKTABLE VALUES (3, 4);
+
+--
+-- MATCH FULL
+--
+-- First test, check and cascade
+--
-- Insert test data into PKTABLE
INSERT INTO PKTABLE VALUES (1, 'Test1');
INSERT INTO PKTABLE VALUES (2, 'Test2');
@@ -230,6 +256,25 @@ INSERT INTO FKTABLE VALUES (1, NULL);
ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL;
+-- Modifying other attributes of a constraint should not affect its enforceability, and vice versa
+ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE NOT VALID NOT ENFORCED;
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_ftest2_fkey DEFERRABLE INITIALLY DEFERRED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fktable_ftest1_ftest2_fkey';
+
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_ftest2_fkey NOT ENFORCED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fktable_ftest1_ftest2_fkey';
+
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_ftest2_fkey ENFORCED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fktable_ftest1_ftest2_fkey';
+
+-- Can change enforceability and deferrability together
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_ftest2_fkey NOT ENFORCED NOT DEFERRABLE;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fktable_ftest1_ftest2_fkey';
+
DROP TABLE FKTABLE;
DROP TABLE PKTABLE;
@@ -968,10 +1013,23 @@ INSERT INTO fktable VALUES (0, 20);
COMMIT;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT ENFORCED;
+
+BEGIN;
+
+-- doesn't match FK, no error.
+UPDATE pktable SET id = 10 WHERE id = 5;
+-- doesn't match PK, no error.
+INSERT INTO fktable VALUES (0, 20);
+
+ROLLBACK;
+
-- try additional syntax
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE;
-- illegal option
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORCED;
+CREATE TEMP TABLE fktable2 (fk int references pktable ENFORCED NOT ENFORCED);
-- test order of firing of FK triggers when several RI-induced changes need to
-- be made to the same row. This was broken by subtransaction-related
@@ -1182,11 +1240,12 @@ ALTER TABLE fk_partitioned_fk DROP COLUMN fdrop1;
CREATE TABLE fk_partitioned_fk_1 (fdrop1 int, fdrop2 int, a int, fdrop3 int, b int);
ALTER TABLE fk_partitioned_fk_1 DROP COLUMN fdrop1, DROP COLUMN fdrop2, DROP COLUMN fdrop3;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FROM (0,0) TO (1000,1000);
-ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk;
+ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
+ALTER TABLE fk_partitioned_fk_2 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
-
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
CREATE TABLE fk_partitioned_fk_3 (fdrop1 int, fdrop2 int, fdrop3 int, fdrop4 int, b int, a int)
PARTITION BY HASH (a);
ALTER TABLE fk_partitioned_fk_3 DROP COLUMN fdrop1, DROP COLUMN fdrop2,
@@ -1232,6 +1291,27 @@ UPDATE fk_notpartitioned_pk SET b = 1502 WHERE a = 1500;
UPDATE fk_notpartitioned_pk SET b = 2504 WHERE a = 2500;
-- check psql behavior
\d fk_notpartitioned_pk
+
+-- Check the exsting FK trigger
+CREATE TEMP TABLE tmp_trg_info AS SELECT tgtype, tgrelid::regclass, tgconstraint
+FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+SELECT count(1) FROM tmp_trg_info;
+
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+-- No triggers
+SELECT count(1) FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+
+-- Changing it back to ENFORCED will recreate the necessary triggers.
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+
+-- Should be exactly the same number of triggers found as before
+SELECT COUNT(1) FROM pg_trigger pgt JOIN tmp_trg_info tt
+ON (pgt.tgtype = tt.tgtype AND pgt.tgrelid = tt.tgrelid AND pgt.tgconstraint = tt.tgconstraint);
+
ALTER TABLE fk_partitioned_fk DROP CONSTRAINT fk_partitioned_fk_a_b_fkey;
-- done.
DROP TABLE fk_notpartitioned_pk, fk_partitioned_fk;
@@ -1439,6 +1519,22 @@ ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN
\d fk_partitioned_fk_2
DROP TABLE fk_partitioned_fk_2;
+CREATE TABLE fk_partitioned_fk_2 (b int, a int,
+ FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
+BEGIN;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should be having two constraints
+\d fk_partitioned_fk_2
+DROP TABLE fk_partitioned_fk_2;
+ROLLBACK;
+BEGIN;
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have only one constraint
+\d fk_partitioned_fk_2
+ROLLBACK;
+DROP TABLE fk_partitioned_fk_2;
+
CREATE TABLE fk_partitioned_fk_4 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE) PARTITION BY RANGE (b, a);
CREATE TABLE fk_partitioned_fk_4_1 PARTITION OF fk_partitioned_fk_4 FOR VALUES FROM (1,1) TO (100,100);
CREATE TABLE fk_partitioned_fk_4_2 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE SET NULL);
--
2.43.5
v14-0009-Merge-the-parent-and-child-constraints-with-diff.patchapplication/x-patch; name=v14-0009-Merge-the-parent-and-child-constraints-with-diff.patchDownload
From d47e1602c8ae60af14c9ebd839cb7b45374fca22 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 10 Feb 2025 10:59:28 +0530
Subject: [PATCH v14 9/9] Merge the parent and child constraints with differing
enforcibility.
If an enforced parent constraint is attached to a non-enforced child
constraint, the child constraint will be made enforced, with
validation applied if the parent constraint is validated as well.
Otherwise, a new enforced constraint (with validation, if the parent
constraint is validated) would need to be created on the child table,
which would be unnecessary if a similar constraint already exists and
can be attached.
On the other hand, having a not-enforced parent constraint with an
enforced child constraint does not cause any issues, and no changes
are required.
----
NOTE: This patch is intended to reduce the diff noise from the main
patch and is not meant to be committed separately. It should be
squashed with the main patch that adds ENFORCED/NOT ENFORCED.
----
---
src/backend/commands/tablecmds.c | 183 +++++++++++++++++++---
src/test/regress/expected/foreign_key.out | 77 +++++++--
src/test/regress/sql/foreign_key.sql | 37 ++++-
3 files changed, 260 insertions(+), 37 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 18d7cb26c7f..8f8bad2fe05 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -390,10 +390,10 @@ static void AlterSeqNamespaces(Relation classRel, Relation rel,
Oid oldNspOid, Oid newNspOid, ObjectAddresses *objsMoved,
LOCKMODE lockmode);
static ObjectAddress ATExecAlterConstraint(List **wqueue, Relation rel,
- AlterConstraintStmt *cmdcon,
+ ATAlterConstraint *cmdcon,
bool recurse, bool recursing,
LOCKMODE lockmode);
-static bool ATExecAlterConstrRecurse(List **wqueue, AlterConstraintStmt *cmdcon,
+static bool ATExecAlterConstrRecurse(List **wqueue, ATAlterConstraint *cmdcon,
Relation conrel, Relation tgrel,
const Oid fkrelid, const Oid pkrelid,
HeapTuple contuple, List **otherrelids,
@@ -401,7 +401,7 @@ static bool ATExecAlterConstrRecurse(List **wqueue, AlterConstraintStmt *cmdcon,
Oid ReferencedParentUpdTrigger,
Oid ReferencingParentInsTrigger,
Oid ReferencingParentUpdTrigger);
-static void ATExecAlterConstrEnforceability(List **wqueue, AlterConstraintStmt *cmdcon,
+static void ATExecAlterConstrEnforceability(List **wqueue, ATAlterConstraint *cmdcon,
Relation conrel, Relation tgrel,
const Oid fkrelid, const Oid pkrelid,
HeapTuple contuple, List **otherrelids,
@@ -413,7 +413,7 @@ static void ATExecAlterConstrEnforceability(List **wqueue, AlterConstraintStmt *
static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Oid fkrel,
bool deferrable, bool initdeferred,
List **otherrelids);
-static void ATExecAlterChildConstr(List **wqueue, AlterConstraintStmt *cmdcon,
+static void ATExecAlterChildConstr(List **wqueue, ATAlterConstraint *cmdcon,
Relation conrel, Relation tgrel,
const Oid fkrelid, const Oid pkrelid,
HeapTuple contuple, List **otherrelids,
@@ -10833,10 +10833,10 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
/*
* Tell Phase 3 to check that the constraint is satisfied by existing
* rows. We can skip this during table creation, when constraint is
- * specified as NOT ENFORCED, or when requested explicitly by specifying
- * NOT VALID in an ADD FOREIGN KEY command, and when we're recreating
- * a constraint following a SET DATA TYPE operation that did not
- * impugn its validity.
+ * specified as NOT ENFORCED, or when requested explicitly by
+ * specifying NOT VALID in an ADD FOREIGN KEY command, and when we're
+ * recreating a constraint following a SET DATA TYPE operation that
+ * did not impugn its validity.
*/
if (wqueue && !old_check_ok && !fkconstraint->skip_validation &&
fkconstraint->is_enforced)
@@ -11512,7 +11512,6 @@ tryAttachPartitionForeignKey(List **wqueue,
if (OidIsValid(partConstr->conparentid) ||
partConstr->condeferrable != parentConstr->condeferrable ||
partConstr->condeferred != parentConstr->condeferred ||
- partConstr->conenforced != parentConstr->conenforced ||
partConstr->confupdtype != parentConstr->confupdtype ||
partConstr->confdeltype != parentConstr->confdeltype ||
partConstr->confmatchtype != parentConstr->confmatchtype)
@@ -11557,6 +11556,8 @@ AttachPartitionForeignKey(List **wqueue,
Oid partConstrFrelid;
Oid partConstrRelid;
bool parentConstrIsEnforced;
+ bool partConstrIsEnforced;
+ bool partConstrParentIsSet;
/* Fetch the parent constraint tuple */
parentConstrTup = SearchSysCache1(CONSTROID,
@@ -11572,13 +11573,47 @@ AttachPartitionForeignKey(List **wqueue,
if (!HeapTupleIsValid(partcontup))
elog(ERROR, "cache lookup failed for constraint %u", partConstrOid);
partConstr = (Form_pg_constraint) GETSTRUCT(partcontup);
+ partConstrIsEnforced = partConstr->conenforced;
partConstrFrelid = partConstr->confrelid;
partConstrRelid = partConstr->conrelid;
+ /*
+ * The case where the parent constraint is not enforced and the child
+ * constraint is enforced is acceptable because the non-enforced parent
+ * constraint lacks triggers, eliminating any redundancy issues with the
+ * enforced child constraint. In this scenario, the child constraint
+ * remains enforced, and its trigger is retained, ensuring that
+ * referential integrity checks for the child continue as before, even
+ * with the parent constraint not enforced. The relationship between the
+ * two constraints is preserved by setting the parent constraint, which
+ * allows us to locate the child constraint. This becomes important if the
+ * parent constraint is later changed to enforced, at which point the
+ * necessary trigger will be created for the parent, and any redundancy
+ * from these triggers will be appropriately handled.
+ */
+ if (!parentConstrIsEnforced && partConstrIsEnforced)
+ {
+ ReleaseSysCache(partcontup);
+ ReleaseSysCache(parentConstrTup);
+
+ ConstraintSetParentConstraint(partConstrOid, parentConstrOid,
+ RelationGetRelid(partition));
+ CommandCounterIncrement();
+
+ return;
+ }
+
/*
* If the referenced table is partitioned, then the partition we're
* attaching now has extra pg_constraint rows and action triggers that are
* no longer needed. Remove those.
+ *
+ * Note that this must be done beforehand, particularly in situations
+ * where we might decide to change the constraint to an enforced state
+ * which will create the required triggers and add the child constraint to
+ * the validation queue. To avoid generating unnecessary triggers and
+ * adding them to the validation queue, it is crucial to eliminate any
+ * redundant constraints beforehand.
*/
if (get_rel_relkind(partConstrFrelid) == RELKIND_PARTITIONED_TABLE)
{
@@ -11590,6 +11625,46 @@ AttachPartitionForeignKey(List **wqueue,
table_close(pg_constraint, RowShareLock);
}
+ /*
+ * The case where the parent constraint is enforced and the child
+ * constraint is not enforced is not acceptable, as it would violate
+ * referential integrity. In such cases, the child constraint will first
+ * be enforced before merging it with the enforced parent constraint.
+ * Subsequently, removing action triggers, setting up constraint triggers,
+ * and handling check triggers for the parent will be managed in the usual
+ * manner, similar to how two enforced constraints are merged.
+ */
+ if (parentConstrIsEnforced && !partConstrIsEnforced)
+ {
+ ATAlterConstraint *cmdcon = makeNode(ATAlterConstraint);
+ Relation conrel;
+ List *otherrelids = NIL;
+
+ cmdcon->conname = NameStr(partConstr->conname);
+ cmdcon->deferrable = partConstr->condeferrable;
+ cmdcon->initdeferred = partConstr->condeferred;
+ cmdcon->alterEnforceability = true;
+ cmdcon->is_enforced = true;
+
+ conrel = table_open(ConstraintRelationId, RowExclusiveLock);
+
+ ATExecAlterConstrRecurse(wqueue, cmdcon, conrel, trigrel,
+ partConstr->conrelid, partConstr->confrelid,
+ partcontup, &otherrelids, AccessExclusiveLock,
+ InvalidOid, InvalidOid, InvalidOid, InvalidOid);
+
+ table_close(conrel, RowExclusiveLock);
+
+ CommandCounterIncrement();
+ }
+
+ /*
+ * The constraint parent shouldn't be set beforehand, or if it's already
+ * set, it should be the specified parent.
+ */
+ partConstrParentIsSet = OidIsValid(partConstr->conparentid);
+ Assert(!partConstrParentIsSet || partConstr->conparentid == parentConstrOid);
+
/*
* Will we need to validate this constraint? A valid parent constraint
* implies that all child constraints have been validated, so if this one
@@ -11609,8 +11684,10 @@ AttachPartitionForeignKey(List **wqueue,
DropForeignKeyConstraintTriggers(trigrel, partConstrOid, partConstrFrelid,
partConstrRelid);
- ConstraintSetParentConstraint(partConstrOid, parentConstrOid,
- RelationGetRelid(partition));
+ /* Skip if the parent is already set */
+ if (!partConstrParentIsSet)
+ ConstraintSetParentConstraint(partConstrOid, parentConstrOid,
+ RelationGetRelid(partition));
/*
* Like the constraint, attach partition's "check" triggers to the
@@ -11946,7 +12023,7 @@ GetForeignKeyCheckTriggers(Relation trigrel,
* InvalidObjectAddress.
*/
static ObjectAddress
-ATExecAlterConstraint(List **wqueue, Relation rel, AlterConstraintStmt *cmdcon,
+ATExecAlterConstraint(List **wqueue, Relation rel, ATAlterConstraint *cmdcon,
bool recurse, bool recursing, LOCKMODE lockmode)
{
Relation conrel;
@@ -12092,7 +12169,7 @@ ATExecAlterConstraint(List **wqueue, Relation rel, AlterConstraintStmt *cmdcon,
* but existing releases don't do that.)
*/
static bool
-ATExecAlterConstrRecurse(List **wqueue, AlterConstraintStmt *cmdcon,
+ATExecAlterConstrRecurse(List **wqueue, ATAlterConstraint *cmdcon,
Relation conrel, Relation tgrel, const Oid fkrelid,
const Oid pkrelid, HeapTuple contuple,
List **otherrelids, LOCKMODE lockmode,
@@ -12169,6 +12246,7 @@ ATExecAlterConstrRecurse(List **wqueue, AlterConstraintStmt *cmdcon,
ReferencedParentUpdTrigger,
ReferencingParentInsTrigger,
ReferencingParentUpdTrigger);
+
/*
* Tell Phase 3 to check that the constraint is satisfied by existing
* rows, but skip this step if the constraint is NOT VALID.
@@ -12242,7 +12320,7 @@ ATExecAlterConstrRecurse(List **wqueue, AlterConstraintStmt *cmdcon,
* ATExecAlterConstrRecurse.
*/
static void
-ATExecAlterConstrEnforceability(List **wqueue, AlterConstraintStmt *cmdcon,
+ATExecAlterConstrEnforceability(List **wqueue, ATAlterConstraint *cmdcon,
Relation conrel, Relation tgrel,
const Oid fkrelid, const Oid pkrelid,
HeapTuple contuple, List **otherrelids,
@@ -12273,6 +12351,17 @@ ATExecAlterConstrEnforceability(List **wqueue, AlterConstraintStmt *cmdcon,
/* Drop all the triggers */
DropForeignKeyConstraintTriggers(tgrel, conoid, InvalidOid, InvalidOid);
+
+ /*
+ * If the referenced table is partitioned, the child constraint we're
+ * changing to not-enforced may have additional pg_constraint rows and
+ * action triggers that remain untouched while this child constraint
+ * is attached to the not-enforced parent. These must now be removed.
+ * For more details, see AttachPartitionForeignKey().
+ */
+ if (OidIsValid(currcon->conparentid) &&
+ get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE)
+ RemoveInheritedConstraint(conrel, tgrel, currcon->conrelid, conoid);
}
else /* Create triggers */
{
@@ -12333,13 +12422,41 @@ ATExecAlterConstrEnforceability(List **wqueue, AlterConstraintStmt *cmdcon,
true, NULL, 1, &pkey);
while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
- ATExecAlterConstrRecurse(wqueue, cmdcon, conrel, tgrel, fkrelid,
- pkrelid, childtup, otherrelids,
- lockmode, ReferencedDelTriggerOid,
- ReferencedUpdTriggerOid,
- ReferencingInsTriggerOid,
- ReferencingUpdTriggerOid);
+ {
+ Form_pg_constraint childcon;
+ childcon = (Form_pg_constraint) GETSTRUCT(childtup);
+
+ if (childcon->conenforced)
+ {
+ /*
+ * The child constraint is attached to the parent
+ * constraint, which is already enforced. Now, as the
+ * parent constraint is being modified to be enforced,
+ * some constraints and action triggers on the child table
+ * may become redundant and need to be removed.
+ */
+ if (currcon->confrelid == pkrelid)
+ {
+ Relation rel = table_open(childcon->conrelid, lockmode);
+
+ AttachPartitionForeignKey(wqueue, rel, childcon->oid,
+ conoid,
+ ReferencingInsTriggerOid,
+ ReferencingUpdTriggerOid,
+ tgrel);
+
+ table_close(rel, NoLock);
+ }
+ }
+ else
+ ATExecAlterConstrRecurse(wqueue, cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, childtup, otherrelids,
+ lockmode, ReferencedDelTriggerOid,
+ ReferencedUpdTriggerOid,
+ ReferencingInsTriggerOid,
+ ReferencingUpdTriggerOid);
+ }
systable_endscan(pscan);
}
}
@@ -12418,7 +12535,7 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Oid fkrel,
* ATExecAlterConstrRecurse.
*/
static void
-ATExecAlterChildConstr(List **wqueue, AlterConstraintStmt *cmdcon,
+ATExecAlterChildConstr(List **wqueue, ATAlterConstraint *cmdcon,
Relation conrel, Relation tgrel, const Oid fkrelid,
const Oid pkrelid, HeapTuple contuple,
List **otherrelids, LOCKMODE lockmode)
@@ -20512,7 +20629,9 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
{
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
+ HeapTuple parentContup;
Form_pg_constraint conform;
+ Oid parentConstrIsEnforced;
contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
if (!HeapTupleIsValid(contup))
@@ -20531,12 +20650,34 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
continue;
}
+ /* Get the enforcibility of the parent constraint */
+ parentContup = SearchSysCache1(CONSTROID,
+ ObjectIdGetDatum(conform->conparentid));
+ if (!HeapTupleIsValid(parentContup))
+ elog(ERROR, "cache lookup failed for constraint %u",
+ conform->conparentid);
+ parentConstrIsEnforced =
+ ((Form_pg_constraint) GETSTRUCT(parentContup))->conenforced;
+ ReleaseSysCache(parentContup);
+
/*
* The constraint on this table must be marked no longer a child of
* the parent's constraint, as do its check triggers.
*/
ConstraintSetParentConstraint(fk->conoid, InvalidOid, InvalidOid);
+ /*
+ * Unsetting the parent is sufficient when the parent constraint is
+ * not enforced and the child constraint is enforced, as we link them
+ * by setting the constraint parent, while leaving the rest unchanged.
+ * For more details, see AttachPartitionForeignKey().
+ */
+ if (!parentConstrIsEnforced && fk->conenforced)
+ {
+ ReleaseSysCache(contup);
+ continue;
+ }
+
/*
* Also, look up the partition's "check" triggers corresponding to the
* enforced constraint being detached and detach them from the parent
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 3183b1cbe8f..c7ccadab26f 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -1664,7 +1664,6 @@ ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FR
ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
-ALTER TABLE fk_partitioned_fk_2 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
CREATE TABLE fk_partitioned_fk_3 (fdrop1 int, fdrop2 int, fdrop3 int, fdrop4 int, b int, a int)
@@ -1673,8 +1672,65 @@ ALTER TABLE fk_partitioned_fk_3 DROP COLUMN fdrop1, DROP COLUMN fdrop2,
DROP COLUMN fdrop3, DROP COLUMN fdrop4;
CREATE TABLE fk_partitioned_fk_3_0 PARTITION OF fk_partitioned_fk_3 FOR VALUES WITH (MODULUS 5, REMAINDER 0);
CREATE TABLE fk_partitioned_fk_3_1 PARTITION OF fk_partitioned_fk_3 FOR VALUES WITH (MODULUS 5, REMAINDER 1);
+-- Merge the not-enforced parent constraint with the enforced and not-enforced child constraints
+ALTER TABLE fk_partitioned_fk_3_0 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ENFORCED;
+ALTER TABLE fk_partitioned_fk_3_1 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk_3 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+-- Merge the non-enforced parent constraint with both the enforced and
+-- non-enforced child constraints, where the referenced table is partitioned.
+CREATE TABLE fk_partitioned_pk (a int, b int, PRIMARY KEY (a, b)) PARTITION BY RANGE (a, b);
+CREATE TABLE fk_partitioned_pk_1 PARTITION OF fk_partitioned_pk FOR VALUES FROM (0,0) TO (1000,1000);
+-- Merge the enforced parent constraint with the enforced and not-enforced child constraints.
+ALTER TABLE fk_partitioned_fk_3_0 ADD FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk ENFORCED;
+ALTER TABLE fk_partitioned_fk_3_1 ADD FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk_3 ADD FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk ENFORCED;
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+ conname | conenforced | convalidated | conrelid | confrelid
+---------------------------------+-------------+--------------+-----------------------+----------------------
+ fk_partitioned_fk_3_0_a_b_fkey | t | t | fk_partitioned_fk_3_0 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey | f | t | fk_partitioned_fk_3_1 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_a_b_fkey | f | t | fk_partitioned_fk_3 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_0_a_b_fkey1 | t | t | fk_partitioned_fk_3_0 | fk_partitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey1 | t | t | fk_partitioned_fk_3_1 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey1 | t | t | fk_partitioned_fk_3 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey2 | t | t | fk_partitioned_fk_3 | fk_partitioned_pk_1
+(7 rows)
+
+ALTER TABLE fk_partitioned_fk_3 DETACH PARTITION fk_partitioned_fk_3_0;
+ALTER TABLE fk_partitioned_fk_3 ATTACH PARTITION fk_partitioned_fk_3_0 FOR VALUES WITH (MODULUS 5, REMAINDER 0);
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+ conname | conenforced | convalidated | conrelid | confrelid
+---------------------------------+-------------+--------------+-----------------------+----------------------
+ fk_partitioned_fk_3_0_a_b_fkey | t | t | fk_partitioned_fk_3_0 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey | f | t | fk_partitioned_fk_3_1 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_a_b_fkey | f | t | fk_partitioned_fk_3 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_0_a_b_fkey1 | t | t | fk_partitioned_fk_3_0 | fk_partitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey1 | t | t | fk_partitioned_fk_3_1 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey1 | t | t | fk_partitioned_fk_3 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey2 | t | t | fk_partitioned_fk_3 | fk_partitioned_pk_1
+(7 rows)
+
+ALTER TABLE fk_partitioned_fk_3 ALTER CONSTRAINT fk_partitioned_fk_3_a_b_fkey1 NOT ENFORCED;
+-- Merging an enforced parent constraint (validated) with a not-enforced child
+-- constraint will implicitly change the child constraint to enforced and apply
+-- the validation as well.
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_3
FOR VALUES FROM (2000,2000) TO (3000,3000);
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+ conname | conenforced | convalidated | conrelid | confrelid
+---------------------------------+-------------+--------------+-----------------------+----------------------
+ fk_partitioned_fk_3_0_a_b_fkey | t | t | fk_partitioned_fk_3_0 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey | t | t | fk_partitioned_fk_3_1 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_a_b_fkey | t | t | fk_partitioned_fk_3 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_0_a_b_fkey1 | f | t | fk_partitioned_fk_3_0 | fk_partitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey1 | f | t | fk_partitioned_fk_3_1 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey1 | f | t | fk_partitioned_fk_3 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey2 | f | t | fk_partitioned_fk_3 | fk_partitioned_pk_1
+(7 rows)
+
-- Creating a foreign key with ONLY on a partitioned table referencing
-- a non-partitioned table fails.
ALTER TABLE ONLY fk_partitioned_fk ADD FOREIGN KEY (a, b)
@@ -1689,22 +1745,22 @@ INSERT INTO fk_partitioned_fk_1 (a,b) VALUES (500, 501);
ERROR: insert or update on table "fk_partitioned_fk_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
DETAIL: Key (a, b)=(500, 501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (1500, 1501);
-ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_2_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
DETAIL: Key (a, b)=(1500, 1501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk_2 (a,b) VALUES (1500, 1501);
-ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_2_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
DETAIL: Key (a, b)=(1500, 1501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (2500, 2502);
-ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_3_1_a_b_fkey"
DETAIL: Key (a, b)=(2500, 2502) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk_3 (a,b) VALUES (2500, 2502);
-ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_3_1_a_b_fkey"
DETAIL: Key (a, b)=(2500, 2502) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (2501, 2503);
-ERROR: insert or update on table "fk_partitioned_fk_3_0" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_0" violates foreign key constraint "fk_partitioned_fk_3_0_a_b_fkey"
DETAIL: Key (a, b)=(2501, 2503) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk_3 (a,b) VALUES (2501, 2503);
-ERROR: insert or update on table "fk_partitioned_fk_3_0" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_0" violates foreign key constraint "fk_partitioned_fk_3_0_a_b_fkey"
DETAIL: Key (a, b)=(2501, 2503) is not present in table "fk_notpartitioned_pk".
-- but if we insert the values that make them valid, then they work
INSERT INTO fk_notpartitioned_pk VALUES (500, 501), (1500, 1501),
@@ -1715,7 +1771,7 @@ INSERT INTO fk_partitioned_fk (a,b) VALUES (2500, 2502);
INSERT INTO fk_partitioned_fk (a,b) VALUES (2501, 2503);
-- this update fails because there is no referenced row
UPDATE fk_partitioned_fk SET a = a + 1 WHERE a = 2501;
-ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_3_1_a_b_fkey"
DETAIL: Key (a, b)=(2502, 2503) is not present in table "fk_notpartitioned_pk".
-- but we can fix it thusly:
INSERT INTO fk_notpartitioned_pk (a,b) VALUES (2502, 2503);
@@ -1858,8 +1914,6 @@ WHERE conrelid::regclass::text like 'fk_partitioned_fk%' ORDER BY oid::regclass:
DROP TABLE fk_partitioned_fk, fk_notpartitioned_pk;
-- NOT VALID foreign key on a non-partitioned table referencing a partitioned table
-CREATE TABLE fk_partitioned_pk (a int, b int, PRIMARY KEY (a, b)) PARTITION BY RANGE (a, b);
-CREATE TABLE fk_partitioned_pk_1 PARTITION OF fk_partitioned_pk FOR VALUES FROM (0,0) TO (1000,1000);
CREATE TABLE fk_notpartitioned_fk (b int, a int);
ALTER TABLE fk_notpartitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk NOT VALID;
-- Constraint will be invalid.
@@ -2075,7 +2129,7 @@ CREATE TABLE fk_partitioned_fk_2 (b int, a int,
FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
BEGIN;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
--- should be having two constraints
+-- should only have one constraint
\d fk_partitioned_fk_2
Table "public.fk_partitioned_fk_2"
Column | Type | Collation | Nullable | Default
@@ -2084,7 +2138,6 @@ ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN
a | integer | | |
Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
Foreign-key constraints:
- "fk_partitioned_fk_2_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED
TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
DROP TABLE fk_partitioned_fk_2;
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index 408a01e377b..2188e56ca3c 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -1243,18 +1243,49 @@ ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FR
ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
-ALTER TABLE fk_partitioned_fk_2 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+
CREATE TABLE fk_partitioned_fk_3 (fdrop1 int, fdrop2 int, fdrop3 int, fdrop4 int, b int, a int)
PARTITION BY HASH (a);
ALTER TABLE fk_partitioned_fk_3 DROP COLUMN fdrop1, DROP COLUMN fdrop2,
DROP COLUMN fdrop3, DROP COLUMN fdrop4;
CREATE TABLE fk_partitioned_fk_3_0 PARTITION OF fk_partitioned_fk_3 FOR VALUES WITH (MODULUS 5, REMAINDER 0);
CREATE TABLE fk_partitioned_fk_3_1 PARTITION OF fk_partitioned_fk_3 FOR VALUES WITH (MODULUS 5, REMAINDER 1);
+-- Merge the not-enforced parent constraint with the enforced and not-enforced child constraints
+ALTER TABLE fk_partitioned_fk_3_0 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ENFORCED;
+ALTER TABLE fk_partitioned_fk_3_1 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk_3 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+
+-- Merge the non-enforced parent constraint with both the enforced and
+-- non-enforced child constraints, where the referenced table is partitioned.
+CREATE TABLE fk_partitioned_pk (a int, b int, PRIMARY KEY (a, b)) PARTITION BY RANGE (a, b);
+CREATE TABLE fk_partitioned_pk_1 PARTITION OF fk_partitioned_pk FOR VALUES FROM (0,0) TO (1000,1000);
+-- Merge the enforced parent constraint with the enforced and not-enforced child constraints.
+ALTER TABLE fk_partitioned_fk_3_0 ADD FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk ENFORCED;
+ALTER TABLE fk_partitioned_fk_3_1 ADD FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk_3 ADD FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk ENFORCED;
+
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+
+ALTER TABLE fk_partitioned_fk_3 DETACH PARTITION fk_partitioned_fk_3_0;
+ALTER TABLE fk_partitioned_fk_3 ATTACH PARTITION fk_partitioned_fk_3_0 FOR VALUES WITH (MODULUS 5, REMAINDER 0);
+
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+
+ALTER TABLE fk_partitioned_fk_3 ALTER CONSTRAINT fk_partitioned_fk_3_a_b_fkey1 NOT ENFORCED;
+
+-- Merging an enforced parent constraint (validated) with a not-enforced child
+-- constraint will implicitly change the child constraint to enforced and apply
+-- the validation as well.
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_3
FOR VALUES FROM (2000,2000) TO (3000,3000);
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+
-- Creating a foreign key with ONLY on a partitioned table referencing
-- a non-partitioned table fails.
ALTER TABLE ONLY fk_partitioned_fk ADD FOREIGN KEY (a, b)
@@ -1381,8 +1412,6 @@ WHERE conrelid::regclass::text like 'fk_partitioned_fk%' ORDER BY oid::regclass:
DROP TABLE fk_partitioned_fk, fk_notpartitioned_pk;
-- NOT VALID foreign key on a non-partitioned table referencing a partitioned table
-CREATE TABLE fk_partitioned_pk (a int, b int, PRIMARY KEY (a, b)) PARTITION BY RANGE (a, b);
-CREATE TABLE fk_partitioned_pk_1 PARTITION OF fk_partitioned_pk FOR VALUES FROM (0,0) TO (1000,1000);
CREATE TABLE fk_notpartitioned_fk (b int, a int);
ALTER TABLE fk_notpartitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk NOT VALID;
@@ -1523,7 +1552,7 @@ CREATE TABLE fk_partitioned_fk_2 (b int, a int,
FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
BEGIN;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
--- should be having two constraints
+-- should only have one constraint
\d fk_partitioned_fk_2
DROP TABLE fk_partitioned_fk_2;
ROLLBACK;
--
2.43.5
Hello,
On 2025-Feb-17, Amul Sul wrote:
I have renamed AlterConstraintStmt to ATAlterConstraint as per your
suggestion in the attached version. Apart from this, there are no
other changes, as the final behavior is still unclear based on the
discussions so far.
Okay, thanks. I've taken your alterDeferrability from your later patch
(renamed to just "deferrability"). Also, IMO the routine structure
needs a bit of a revamp. What do you think of the attached, which is
mostly your 0001? (Actually, now that I look, it seems I made more or
less the same changes you'd be doing in 0008, so this should be okay.)
--
Álvaro Herrera 48°01'N 7°57'E — https://www.EnterpriseDB.com/
Attachments:
0001-Add-ATAlterConstraint-struct-for-ALTER-.-CONSTRAINT.patchtext/x-diff; charset=utf-8Download
From 5b0cf18a0facd702b8006f38469344ea888c6de0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=81lvaro=20Herrera?= <alvherre@alvh.no-ip.org>
Date: Mon, 17 Feb 2025 18:20:30 +0100
Subject: [PATCH] Add ATAlterConstraint struct for ALTER .. CONSTRAINT.
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Replace the use of Constraint with the new ATAlterConstraint struct,
which allows us to pass additional information, such as whether the
constraint attribute is set. This is necessary for future patches that
involve altering constraints in other ways.
Author: Amul Sul <sulamul@gmail.com>
Author: Ãlvaro Herrera <alvherre@alvh.no-ip.org>
Discussion: https://postgr.es/m/CAAJ_b94bfgPV-8Mw_HwSBeheVwaK9=5s+7+KbBj_NpwXQFgDGg@mail.gmail.com
---
src/backend/commands/tablecmds.c | 160 ++++++++++++++++---------------
src/backend/parser/gram.y | 4 +-
src/include/nodes/parsenodes.h | 25 +++--
src/tools/pgindent/typedefs.list | 1 +
4 files changed, 102 insertions(+), 88 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 72a1b64c2a2..072a6129963 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -389,17 +389,17 @@ static void AlterIndexNamespaces(Relation classRel, Relation rel,
static void AlterSeqNamespaces(Relation classRel, Relation rel,
Oid oldNspOid, Oid newNspOid, ObjectAddresses *objsMoved,
LOCKMODE lockmode);
-static ObjectAddress ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd,
+static ObjectAddress ATExecAlterConstraint(Relation rel, ATAlterConstraint *cmdcon,
bool recurse, bool recursing, LOCKMODE lockmode);
-static bool ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode);
+static bool ATExecAlterConstrDeferrability(ATAlterConstraint *cmdcon, Relation conrel,
+ Relation tgrel, Relation rel, HeapTuple contuple,
+ List **otherrelids, LOCKMODE lockmode);
+static void ATExecAlterConstrDeferrabilityRecurse(ATAlterConstraint *cmdcon, Relation conrel,
+ Relation tgrel, Relation rel, HeapTuple contuple,
+ List **otherrelids, LOCKMODE lockmode);
static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
bool deferrable, bool initdeferred,
List **otherrelids);
-static void ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode);
static ObjectAddress ATExecValidateConstraint(List **wqueue,
Relation rel, char *constrName,
bool recurse, bool recursing, LOCKMODE lockmode);
@@ -5450,7 +5450,9 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
lockmode);
break;
case AT_AlterConstraint: /* ALTER CONSTRAINT */
- address = ATExecAlterConstraint(rel, cmd, false, false, lockmode);
+ address = ATExecAlterConstraint(rel, castNode(ATAlterConstraint,
+ cmd->def),
+ false, false, lockmode);
break;
case AT_ValidateConstraint: /* VALIDATE CONSTRAINT */
address = ATExecValidateConstraint(wqueue, rel, cmd->name, cmd->recurse,
@@ -11801,10 +11803,9 @@ GetForeignKeyCheckTriggers(Relation trigrel,
* InvalidObjectAddress.
*/
static ObjectAddress
-ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
+ATExecAlterConstraint(Relation rel, ATAlterConstraint *cmdcon, bool recurse,
bool recursing, LOCKMODE lockmode)
{
- Constraint *cmdcon;
Relation conrel;
Relation tgrel;
SysScanDesc scan;
@@ -11813,9 +11814,6 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
Form_pg_constraint currcon;
ObjectAddress address;
List *otherrelids = NIL;
- ListCell *lc;
-
- cmdcon = castNode(Constraint, cmd->def);
conrel = table_open(ConstraintRelationId, RowExclusiveLock);
tgrel = table_open(TriggerRelationId, RowExclusiveLock);
@@ -11896,28 +11894,32 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
errhint("You may alter the constraint it derives from instead.")));
}
+ address = InvalidObjectAddress;
+
/*
* Do the actual catalog work. We can skip changing if already in the
* desired state, but not if a partitioned table: partitions need to be
* processed regardless, in case they had the constraint locally changed.
*/
- address = InvalidObjectAddress;
- if (currcon->condeferrable != cmdcon->deferrable ||
- currcon->condeferred != cmdcon->initdeferred ||
- rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ if (cmdcon->deferrability)
{
- if (ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, rel, contuple,
- &otherrelids, lockmode))
- ObjectAddressSet(address, ConstraintRelationId, currcon->oid);
- }
+ if (currcon->condeferrable != cmdcon->deferrable ||
+ currcon->condeferred != cmdcon->initdeferred ||
+ rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ if (ATExecAlterConstrDeferrability(cmdcon, conrel, tgrel, rel, contuple,
+ &otherrelids, lockmode))
+ ObjectAddressSet(address, ConstraintRelationId, currcon->oid);
+ }
- /*
- * ATExecAlterConstrRecurse already invalidated relcache for the relations
- * having the constraint itself; here we also invalidate for relations
- * that have any triggers that are part of the constraint.
- */
- foreach(lc, otherrelids)
- CacheInvalidateRelcacheByRelid(lfirst_oid(lc));
+ /*
+ * ATExecAlterConstrDeferrability already invalidated relcache for the
+ * relations having the constraint itself; here we also invalidate for
+ * relations that have any triggers that are part of the constraint.
+ */
+ foreach_oid(relid, otherrelids)
+ CacheInvalidateRelcacheByRelid(relid);
+ }
systable_endscan(scan);
@@ -11939,9 +11941,9 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
* but existing releases don't do that.)
*/
static bool
-ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode)
+ATExecAlterConstrDeferrability(ATAlterConstraint *cmdcon, Relation conrel,
+ Relation tgrel, Relation rel, HeapTuple contuple,
+ List **otherrelids, LOCKMODE lockmode)
{
Form_pg_constraint currcon;
Oid conoid;
@@ -12000,18 +12002,61 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
*/
if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)
- ATExecAlterChildConstr(cmdcon, conrel, tgrel, rel, contuple,
- otherrelids, lockmode);
+ ATExecAlterConstrDeferrabilityRecurse(cmdcon, conrel, tgrel, rel, contuple,
+ otherrelids, lockmode);
return changed;
}
/*
- * A subroutine of ATExecAlterConstrRecurse that updated constraint trigger's
- * deferrability.
+ * Invokes ATExecAlterConstrDeferrability for each constraint that is a child
+ * of the specified constraint.
*
* The arguments to this function have the same meaning as the arguments to
- * ATExecAlterConstrRecurse.
+ * ATExecAlterConstrDeferrability.
+ */
+static void
+ATExecAlterConstrDeferrabilityRecurse(ATAlterConstraint *cmdcon, Relation conrel,
+ Relation tgrel, Relation rel, HeapTuple contuple,
+ List **otherrelids, LOCKMODE lockmode)
+{
+ Form_pg_constraint currcon;
+ Oid conoid;
+ ScanKeyData pkey;
+ SysScanDesc pscan;
+ HeapTuple childtup;
+
+ currcon = (Form_pg_constraint) GETSTRUCT(contuple);
+ conoid = currcon->oid;
+
+ ScanKeyInit(&pkey,
+ Anum_pg_constraint_conparentid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+
+ pscan = systable_beginscan(conrel, ConstraintParentIndexId,
+ true, NULL, 1, &pkey);
+
+ while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
+ {
+ Form_pg_constraint childcon = (Form_pg_constraint) GETSTRUCT(childtup);
+ Relation childrel;
+
+ childrel = table_open(childcon->conrelid, lockmode);
+ ATExecAlterConstrDeferrability(cmdcon, conrel, tgrel, childrel, childtup,
+ otherrelids, lockmode);
+ table_close(childrel, NoLock);
+ }
+
+ systable_endscan(pscan);
+}
+
+/*
+ * A subroutine of ATExecAlterConstrDeferrability that updated constraint
+ * trigger's deferrability.
+ *
+ * The arguments to this function have the same meaning as the arguments to
+ * ATExecAlterConstrDeferrability.
*/
static void
AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
@@ -12071,49 +12116,6 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
systable_endscan(tgscan);
}
-/*
- * Invokes ATExecAlterConstrRecurse for each constraint that is a child of the
- * specified constraint.
- *
- * The arguments to this function have the same meaning as the arguments to
- * ATExecAlterConstrRecurse.
- */
-static void
-ATExecAlterChildConstr(Constraint *cmdcon, Relation conrel, Relation tgrel,
- Relation rel, HeapTuple contuple, List **otherrelids,
- LOCKMODE lockmode)
-{
- Form_pg_constraint currcon;
- Oid conoid;
- ScanKeyData pkey;
- SysScanDesc pscan;
- HeapTuple childtup;
-
- currcon = (Form_pg_constraint) GETSTRUCT(contuple);
- conoid = currcon->oid;
-
- ScanKeyInit(&pkey,
- Anum_pg_constraint_conparentid,
- BTEqualStrategyNumber, F_OIDEQ,
- ObjectIdGetDatum(conoid));
-
- pscan = systable_beginscan(conrel, ConstraintParentIndexId,
- true, NULL, 1, &pkey);
-
- while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
- {
- Form_pg_constraint childcon = (Form_pg_constraint) GETSTRUCT(childtup);
- Relation childrel;
-
- childrel = table_open(childcon->conrelid, lockmode);
- ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, childrel, childtup,
- otherrelids, lockmode);
- table_close(childrel, NoLock);
- }
-
- systable_endscan(pscan);
-}
-
/*
* ALTER TABLE VALIDATE CONSTRAINT
*
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index d3887628d46..268db669c83 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -2657,12 +2657,12 @@ alter_table_cmd:
| ALTER CONSTRAINT name ConstraintAttributeSpec
{
AlterTableCmd *n = makeNode(AlterTableCmd);
- Constraint *c = makeNode(Constraint);
+ ATAlterConstraint *c = makeNode(ATAlterConstraint);
n->subtype = AT_AlterConstraint;
n->def = (Node *) c;
- c->contype = CONSTR_FOREIGN; /* others not supported, yet */
c->conname = $3;
+ c->deferrability = true;
processCASbits($4, @4, "ALTER CONSTRAINT statement",
&c->deferrable,
&c->initdeferred,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 8dd421fa0ef..eec7b22d0b1 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2469,13 +2469,6 @@ typedef enum AlterTableType
AT_ReAddStatistics, /* internal to commands/tablecmds.c */
} AlterTableType;
-typedef struct ReplicaIdentityStmt
-{
- NodeTag type;
- char identity_type;
- char *name;
-} ReplicaIdentityStmt;
-
typedef struct AlterTableCmd /* one subcommand of an ALTER TABLE */
{
NodeTag type;
@@ -2492,6 +2485,24 @@ typedef struct AlterTableCmd /* one subcommand of an ALTER TABLE */
bool recurse; /* exec-time recursion */
} AlterTableCmd;
+/* Ad-hoc node for AT_AlterConstraint */
+typedef struct ATAlterConstraint
+{
+ NodeTag type;
+ char *conname; /* Constraint name */
+ bool deferrability; /* changing deferrability properties? */
+ bool deferrable; /* DEFERRABLE? */
+ bool initdeferred; /* INITIALLY DEFERRED? */
+} ATAlterConstraint;
+
+/* Ad-hoc node for AT_ReplicaIdentity */
+typedef struct ReplicaIdentityStmt
+{
+ NodeTag type;
+ char identity_type;
+ char *name;
+} ReplicaIdentityStmt;
+
/* ----------------------
* Alter Collation
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index bce4214503d..00d6a5aebac 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -157,6 +157,7 @@ ArrayType
AsyncQueueControl
AsyncQueueEntry
AsyncRequest
+ATAlterConstraint
AttInMetadata
AttStatsSlot
AttoptCacheEntry
--
2.39.5
On Tue, Feb 18, 2025 at 12:47 AM Álvaro Herrera <alvherre@alvh.no-ip.org> wrote:
Hello,
On 2025-Feb-17, Amul Sul wrote:
I have renamed AlterConstraintStmt to ATAlterConstraint as per your
suggestion in the attached version. Apart from this, there are no
other changes, as the final behavior is still unclear based on the
discussions so far.Okay, thanks. I've taken your alterDeferrability from your later patch
(renamed to just "deferrability"). Also, IMO the routine structure
needs a bit of a revamp. What do you think of the attached, which is
mostly your 0001? (Actually, now that I look, it seems I made more or
less the same changes you'd be doing in 0008, so this should be okay.)
The patch looks quite reasonable, but I’m concerned that renaming
ATExecAlterConstrRecurse() and ATExecAlterChildConstr() exclusively
for deferrability might require the enforceability patch to duplicate
these functions, even though some operations (e.g., pg_constraint
updates and recursion on child constraints) could have been reused.
Regards,
Amul
On 2025-Feb-18, Amul Sul wrote:
The patch looks quite reasonable, but I’m concerned that renaming
ATExecAlterConstrRecurse() and ATExecAlterChildConstr() exclusively
for deferrability might require the enforceability patch to duplicate
these functions, even though some operations (e.g., pg_constraint
updates and recursion on child constraints) could have been reused.
True. I'll give another look to your 0008 and Suraj's patch for
inheritability change, to avoid repetitive boilerplate as much as
possible.
--
Álvaro Herrera 48°01'N 7°57'E — https://www.EnterpriseDB.com/
On Tue, Feb 18, 2025 at 2:13 PM Álvaro Herrera <alvherre@alvh.no-ip.org> wrote:
On 2025-Feb-18, Amul Sul wrote:
The patch looks quite reasonable, but I’m concerned that renaming
ATExecAlterConstrRecurse() and ATExecAlterChildConstr() exclusively
for deferrability might require the enforceability patch to duplicate
these functions, even though some operations (e.g., pg_constraint
updates and recursion on child constraints) could have been reused.True. I'll give another look to your 0008 and Suraj's patch for
inheritability change, to avoid repetitive boilerplate as much as
possible.
Thanks, Álvaro, for committing the 0001 patch -- it really helps.
Attached is the rebased patch set against the latest master head,
which also includes a *new* refactoring patch (0001). In this patch,
I’ve re-added ATExecAlterChildConstr(), which is required for the main
feature patch (0008) to handle recursion from different places while
altering enforceability.
Regards,
Amul
Attachments:
v15-0009-Merge-the-parent-and-child-constraints-with-diff.patchapplication/octet-stream; name=v15-0009-Merge-the-parent-and-child-constraints-with-diff.patchDownload
From 62d3d9fcd37e1d1133d1403515784025baeb2010 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 10 Feb 2025 10:59:28 +0530
Subject: [PATCH v15 9/9] Merge the parent and child constraints with differing
enforcibility.
If an enforced parent constraint is attached to a non-enforced child
constraint, the child constraint will be made enforced, with
validation applied if the parent constraint is validated as well.
Otherwise, a new enforced constraint (with validation, if the parent
constraint is validated) would need to be created on the child table,
which would be unnecessary if a similar constraint already exists and
can be attached.
On the other hand, having a not-enforced parent constraint with an
enforced child constraint does not cause any issues, and no changes
are required.
----
NOTE: This patch is intended to reduce the diff noise from the main
patch and is not meant to be committed separately. It should be
squashed with the main patch that adds ENFORCED/NOT ENFORCED.
----
---
src/backend/commands/tablecmds.c | 177 ++++++++++++++++++++--
src/test/regress/expected/foreign_key.out | 77 ++++++++--
src/test/regress/sql/foreign_key.sql | 37 ++++-
3 files changed, 261 insertions(+), 30 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 9947f29b2f6..98a01af10e8 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -11514,7 +11514,6 @@ tryAttachPartitionForeignKey(List **wqueue,
if (OidIsValid(partConstr->conparentid) ||
partConstr->condeferrable != parentConstr->condeferrable ||
partConstr->condeferred != parentConstr->condeferred ||
- partConstr->conenforced != parentConstr->conenforced ||
partConstr->confupdtype != parentConstr->confupdtype ||
partConstr->confdeltype != parentConstr->confdeltype ||
partConstr->confmatchtype != parentConstr->confmatchtype)
@@ -11559,6 +11558,8 @@ AttachPartitionForeignKey(List **wqueue,
Oid partConstrFrelid;
Oid partConstrRelid;
bool parentConstrIsEnforced;
+ bool partConstrIsEnforced;
+ bool partConstrParentIsSet;
/* Fetch the parent constraint tuple */
parentConstrTup = SearchSysCache1(CONSTROID,
@@ -11574,13 +11575,47 @@ AttachPartitionForeignKey(List **wqueue,
if (!HeapTupleIsValid(partcontup))
elog(ERROR, "cache lookup failed for constraint %u", partConstrOid);
partConstr = (Form_pg_constraint) GETSTRUCT(partcontup);
+ partConstrIsEnforced = partConstr->conenforced;
partConstrFrelid = partConstr->confrelid;
partConstrRelid = partConstr->conrelid;
+ /*
+ * The case where the parent constraint is not enforced and the child
+ * constraint is enforced is acceptable because the non-enforced parent
+ * constraint lacks triggers, eliminating any redundancy issues with the
+ * enforced child constraint. In this scenario, the child constraint
+ * remains enforced, and its trigger is retained, ensuring that
+ * referential integrity checks for the child continue as before, even
+ * with the parent constraint not enforced. The relationship between the
+ * two constraints is preserved by setting the parent constraint, which
+ * allows us to locate the child constraint. This becomes important if the
+ * parent constraint is later changed to enforced, at which point the
+ * necessary trigger will be created for the parent, and any redundancy
+ * from these triggers will be appropriately handled.
+ */
+ if (!parentConstrIsEnforced && partConstrIsEnforced)
+ {
+ ReleaseSysCache(partcontup);
+ ReleaseSysCache(parentConstrTup);
+
+ ConstraintSetParentConstraint(partConstrOid, parentConstrOid,
+ RelationGetRelid(partition));
+ CommandCounterIncrement();
+
+ return;
+ }
+
/*
* If the referenced table is partitioned, then the partition we're
* attaching now has extra pg_constraint rows and action triggers that are
* no longer needed. Remove those.
+ *
+ * Note that this must be done beforehand, particularly in situations
+ * where we might decide to change the constraint to an enforced state
+ * which will create the required triggers and add the child constraint to
+ * the validation queue. To avoid generating unnecessary triggers and
+ * adding them to the validation queue, it is crucial to eliminate any
+ * redundant constraints beforehand.
*/
if (get_rel_relkind(partConstrFrelid) == RELKIND_PARTITIONED_TABLE)
{
@@ -11592,6 +11627,48 @@ AttachPartitionForeignKey(List **wqueue,
table_close(pg_constraint, RowShareLock);
}
+ /*
+ * The case where the parent constraint is enforced and the child
+ * constraint is not enforced is not acceptable, as it would violate
+ * referential integrity. In such cases, the child constraint will first
+ * be enforced before merging it with the enforced parent constraint.
+ * Subsequently, removing action triggers, setting up constraint triggers,
+ * and handling check triggers for the parent will be managed in the usual
+ * manner, similar to how two enforced constraints are merged.
+ */
+ if (parentConstrIsEnforced && !partConstrIsEnforced)
+ {
+ ATAlterConstraint *cmdcon = makeNode(ATAlterConstraint);
+ Relation conrel;
+ List *otherrelids = NIL;
+
+ cmdcon->conname = NameStr(partConstr->conname);
+ cmdcon->deferrable = partConstr->condeferrable;
+ cmdcon->initdeferred = partConstr->condeferred;
+ cmdcon->alterEnforceability = true;
+ cmdcon->is_enforced = true;
+
+ conrel = table_open(ConstraintRelationId, RowExclusiveLock);
+
+ ATExecAlterConstraintInternal(wqueue, cmdcon, conrel, trigrel,
+ partConstr->conrelid,
+ partConstr->confrelid,
+ partcontup, true, &otherrelids,
+ AccessExclusiveLock, InvalidOid,
+ InvalidOid, InvalidOid, InvalidOid);
+
+ table_close(conrel, RowExclusiveLock);
+
+ CommandCounterIncrement();
+ }
+
+ /*
+ * The constraint parent shouldn't be set beforehand, or if it's already
+ * set, it should be the specified parent.
+ */
+ partConstrParentIsSet = OidIsValid(partConstr->conparentid);
+ Assert(!partConstrParentIsSet || partConstr->conparentid == parentConstrOid);
+
/*
* Will we need to validate this constraint? A valid parent constraint
* implies that all child constraints have been validated, so if this one
@@ -11611,8 +11688,10 @@ AttachPartitionForeignKey(List **wqueue,
DropForeignKeyConstraintTriggers(trigrel, partConstrOid, partConstrFrelid,
partConstrRelid);
- ConstraintSetParentConstraint(partConstrOid, parentConstrOid,
- RelationGetRelid(partition));
+ /* Skip if the parent is already set */
+ if (!partConstrParentIsSet)
+ ConstraintSetParentConstraint(partConstrOid, parentConstrOid,
+ RelationGetRelid(partition));
/*
* Like the constraint, attach partition's "check" triggers to the
@@ -12226,11 +12305,17 @@ ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon,
}
/*
- * If the table at either end of the constraint is partitioned, we need to
- * handle every constraint that is a child of this one.
+ * If either table involved in the constraint is partitioned, we need to
+ * handle every child constraint associated with it. This must be done
+ * regardless of whether the constraint entry has been modified.
+ *
+ * For example, if the parent constraint is marked as NOT ENFORCED and an
+ * ALTER command attempts to set it as NOT ENFORCED again, there is no
+ * change for the parent constraint itself. However, we still need to
+ * recurse through all child constraints, as a NOT ENFORCED parent
+ * constraint may have ENFORCED child constraints.
*/
- if (recurse && changed &&
- (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
+ if (recurse && (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
(OidIsValid(refrelid) &&
get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)))
ATExecAlterChildConstr(wqueue, cmdcon, conrel, tgrel, fkrelid, pkrelid,
@@ -12282,6 +12367,17 @@ ATExecAlterConstrEnforceability(List **wqueue, ATAlterConstraint *cmdcon,
/* Drop all the triggers */
DropForeignKeyConstraintTriggers(tgrel, conoid, InvalidOid, InvalidOid);
+
+ /*
+ * If the referenced table is partitioned, the child constraint we're
+ * changing to not-enforced may have additional pg_constraint rows and
+ * action triggers that remain untouched while this child constraint
+ * is attached to the not-enforced parent. These must now be removed.
+ * For more details, see AttachPartitionForeignKey().
+ */
+ if (OidIsValid(currcon->conparentid) &&
+ get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE)
+ RemoveInheritedConstraint(conrel, tgrel, currcon->conrelid, conoid);
}
else /* Create triggers */
{
@@ -12342,13 +12438,42 @@ ATExecAlterConstrEnforceability(List **wqueue, ATAlterConstraint *cmdcon,
true, NULL, 1, &pkey);
while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
- ATExecAlterConstraintInternal(wqueue, cmdcon, conrel, tgrel,
- fkrelid, pkrelid, childtup, false,
- otherrelids, lockmode,
- ReferencedDelTriggerOid,
- ReferencedUpdTriggerOid,
- ReferencingInsTriggerOid,
- ReferencingUpdTriggerOid);
+ {
+ Form_pg_constraint childcon;
+
+ childcon = (Form_pg_constraint) GETSTRUCT(childtup);
+
+ if (childcon->conenforced)
+ {
+ /*
+ * The child constraint is attached to the parent
+ * constraint, which is already enforced. Now, as the
+ * parent constraint is being modified to be enforced,
+ * some constraints and action triggers on the child table
+ * may become redundant and need to be removed.
+ */
+ if (currcon->confrelid == pkrelid)
+ {
+ Relation rel = table_open(childcon->conrelid, lockmode);
+
+ AttachPartitionForeignKey(wqueue, rel, childcon->oid,
+ conoid,
+ ReferencingInsTriggerOid,
+ ReferencingUpdTriggerOid,
+ tgrel);
+
+ table_close(rel, NoLock);
+ }
+ }
+ else
+ ATExecAlterConstraintInternal(wqueue, cmdcon, conrel, tgrel,
+ fkrelid, pkrelid, childtup, false,
+ otherrelids, lockmode,
+ ReferencedDelTriggerOid,
+ ReferencedUpdTriggerOid,
+ ReferencingInsTriggerOid,
+ ReferencingUpdTriggerOid);
+ }
systable_endscan(pscan);
}
@@ -20527,7 +20652,9 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
{
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
+ HeapTuple parentContup;
Form_pg_constraint conform;
+ Oid parentConstrIsEnforced;
contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
if (!HeapTupleIsValid(contup))
@@ -20546,12 +20673,34 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
continue;
}
+ /* Get the enforcibility of the parent constraint */
+ parentContup = SearchSysCache1(CONSTROID,
+ ObjectIdGetDatum(conform->conparentid));
+ if (!HeapTupleIsValid(parentContup))
+ elog(ERROR, "cache lookup failed for constraint %u",
+ conform->conparentid);
+ parentConstrIsEnforced =
+ ((Form_pg_constraint) GETSTRUCT(parentContup))->conenforced;
+ ReleaseSysCache(parentContup);
+
/*
* The constraint on this table must be marked no longer a child of
* the parent's constraint, as do its check triggers.
*/
ConstraintSetParentConstraint(fk->conoid, InvalidOid, InvalidOid);
+ /*
+ * Unsetting the parent is sufficient when the parent constraint is
+ * not enforced and the child constraint is enforced, as we link them
+ * by setting the constraint parent, while leaving the rest unchanged.
+ * For more details, see AttachPartitionForeignKey().
+ */
+ if (!parentConstrIsEnforced && fk->conenforced)
+ {
+ ReleaseSysCache(contup);
+ continue;
+ }
+
/*
* Also, look up the partition's "check" triggers corresponding to the
* enforced constraint being detached and detach them from the parent
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 3183b1cbe8f..c7ccadab26f 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -1664,7 +1664,6 @@ ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FR
ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
-ALTER TABLE fk_partitioned_fk_2 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
CREATE TABLE fk_partitioned_fk_3 (fdrop1 int, fdrop2 int, fdrop3 int, fdrop4 int, b int, a int)
@@ -1673,8 +1672,65 @@ ALTER TABLE fk_partitioned_fk_3 DROP COLUMN fdrop1, DROP COLUMN fdrop2,
DROP COLUMN fdrop3, DROP COLUMN fdrop4;
CREATE TABLE fk_partitioned_fk_3_0 PARTITION OF fk_partitioned_fk_3 FOR VALUES WITH (MODULUS 5, REMAINDER 0);
CREATE TABLE fk_partitioned_fk_3_1 PARTITION OF fk_partitioned_fk_3 FOR VALUES WITH (MODULUS 5, REMAINDER 1);
+-- Merge the not-enforced parent constraint with the enforced and not-enforced child constraints
+ALTER TABLE fk_partitioned_fk_3_0 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ENFORCED;
+ALTER TABLE fk_partitioned_fk_3_1 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk_3 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+-- Merge the non-enforced parent constraint with both the enforced and
+-- non-enforced child constraints, where the referenced table is partitioned.
+CREATE TABLE fk_partitioned_pk (a int, b int, PRIMARY KEY (a, b)) PARTITION BY RANGE (a, b);
+CREATE TABLE fk_partitioned_pk_1 PARTITION OF fk_partitioned_pk FOR VALUES FROM (0,0) TO (1000,1000);
+-- Merge the enforced parent constraint with the enforced and not-enforced child constraints.
+ALTER TABLE fk_partitioned_fk_3_0 ADD FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk ENFORCED;
+ALTER TABLE fk_partitioned_fk_3_1 ADD FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk_3 ADD FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk ENFORCED;
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+ conname | conenforced | convalidated | conrelid | confrelid
+---------------------------------+-------------+--------------+-----------------------+----------------------
+ fk_partitioned_fk_3_0_a_b_fkey | t | t | fk_partitioned_fk_3_0 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey | f | t | fk_partitioned_fk_3_1 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_a_b_fkey | f | t | fk_partitioned_fk_3 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_0_a_b_fkey1 | t | t | fk_partitioned_fk_3_0 | fk_partitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey1 | t | t | fk_partitioned_fk_3_1 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey1 | t | t | fk_partitioned_fk_3 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey2 | t | t | fk_partitioned_fk_3 | fk_partitioned_pk_1
+(7 rows)
+
+ALTER TABLE fk_partitioned_fk_3 DETACH PARTITION fk_partitioned_fk_3_0;
+ALTER TABLE fk_partitioned_fk_3 ATTACH PARTITION fk_partitioned_fk_3_0 FOR VALUES WITH (MODULUS 5, REMAINDER 0);
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+ conname | conenforced | convalidated | conrelid | confrelid
+---------------------------------+-------------+--------------+-----------------------+----------------------
+ fk_partitioned_fk_3_0_a_b_fkey | t | t | fk_partitioned_fk_3_0 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey | f | t | fk_partitioned_fk_3_1 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_a_b_fkey | f | t | fk_partitioned_fk_3 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_0_a_b_fkey1 | t | t | fk_partitioned_fk_3_0 | fk_partitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey1 | t | t | fk_partitioned_fk_3_1 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey1 | t | t | fk_partitioned_fk_3 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey2 | t | t | fk_partitioned_fk_3 | fk_partitioned_pk_1
+(7 rows)
+
+ALTER TABLE fk_partitioned_fk_3 ALTER CONSTRAINT fk_partitioned_fk_3_a_b_fkey1 NOT ENFORCED;
+-- Merging an enforced parent constraint (validated) with a not-enforced child
+-- constraint will implicitly change the child constraint to enforced and apply
+-- the validation as well.
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_3
FOR VALUES FROM (2000,2000) TO (3000,3000);
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+ conname | conenforced | convalidated | conrelid | confrelid
+---------------------------------+-------------+--------------+-----------------------+----------------------
+ fk_partitioned_fk_3_0_a_b_fkey | t | t | fk_partitioned_fk_3_0 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey | t | t | fk_partitioned_fk_3_1 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_a_b_fkey | t | t | fk_partitioned_fk_3 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_0_a_b_fkey1 | f | t | fk_partitioned_fk_3_0 | fk_partitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey1 | f | t | fk_partitioned_fk_3_1 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey1 | f | t | fk_partitioned_fk_3 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey2 | f | t | fk_partitioned_fk_3 | fk_partitioned_pk_1
+(7 rows)
+
-- Creating a foreign key with ONLY on a partitioned table referencing
-- a non-partitioned table fails.
ALTER TABLE ONLY fk_partitioned_fk ADD FOREIGN KEY (a, b)
@@ -1689,22 +1745,22 @@ INSERT INTO fk_partitioned_fk_1 (a,b) VALUES (500, 501);
ERROR: insert or update on table "fk_partitioned_fk_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
DETAIL: Key (a, b)=(500, 501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (1500, 1501);
-ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_2_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
DETAIL: Key (a, b)=(1500, 1501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk_2 (a,b) VALUES (1500, 1501);
-ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_2_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
DETAIL: Key (a, b)=(1500, 1501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (2500, 2502);
-ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_3_1_a_b_fkey"
DETAIL: Key (a, b)=(2500, 2502) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk_3 (a,b) VALUES (2500, 2502);
-ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_3_1_a_b_fkey"
DETAIL: Key (a, b)=(2500, 2502) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (2501, 2503);
-ERROR: insert or update on table "fk_partitioned_fk_3_0" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_0" violates foreign key constraint "fk_partitioned_fk_3_0_a_b_fkey"
DETAIL: Key (a, b)=(2501, 2503) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk_3 (a,b) VALUES (2501, 2503);
-ERROR: insert or update on table "fk_partitioned_fk_3_0" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_0" violates foreign key constraint "fk_partitioned_fk_3_0_a_b_fkey"
DETAIL: Key (a, b)=(2501, 2503) is not present in table "fk_notpartitioned_pk".
-- but if we insert the values that make them valid, then they work
INSERT INTO fk_notpartitioned_pk VALUES (500, 501), (1500, 1501),
@@ -1715,7 +1771,7 @@ INSERT INTO fk_partitioned_fk (a,b) VALUES (2500, 2502);
INSERT INTO fk_partitioned_fk (a,b) VALUES (2501, 2503);
-- this update fails because there is no referenced row
UPDATE fk_partitioned_fk SET a = a + 1 WHERE a = 2501;
-ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_3_1_a_b_fkey"
DETAIL: Key (a, b)=(2502, 2503) is not present in table "fk_notpartitioned_pk".
-- but we can fix it thusly:
INSERT INTO fk_notpartitioned_pk (a,b) VALUES (2502, 2503);
@@ -1858,8 +1914,6 @@ WHERE conrelid::regclass::text like 'fk_partitioned_fk%' ORDER BY oid::regclass:
DROP TABLE fk_partitioned_fk, fk_notpartitioned_pk;
-- NOT VALID foreign key on a non-partitioned table referencing a partitioned table
-CREATE TABLE fk_partitioned_pk (a int, b int, PRIMARY KEY (a, b)) PARTITION BY RANGE (a, b);
-CREATE TABLE fk_partitioned_pk_1 PARTITION OF fk_partitioned_pk FOR VALUES FROM (0,0) TO (1000,1000);
CREATE TABLE fk_notpartitioned_fk (b int, a int);
ALTER TABLE fk_notpartitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk NOT VALID;
-- Constraint will be invalid.
@@ -2075,7 +2129,7 @@ CREATE TABLE fk_partitioned_fk_2 (b int, a int,
FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
BEGIN;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
--- should be having two constraints
+-- should only have one constraint
\d fk_partitioned_fk_2
Table "public.fk_partitioned_fk_2"
Column | Type | Collation | Nullable | Default
@@ -2084,7 +2138,6 @@ ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN
a | integer | | |
Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
Foreign-key constraints:
- "fk_partitioned_fk_2_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED
TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
DROP TABLE fk_partitioned_fk_2;
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index 408a01e377b..2188e56ca3c 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -1243,18 +1243,49 @@ ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FR
ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
-ALTER TABLE fk_partitioned_fk_2 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+
CREATE TABLE fk_partitioned_fk_3 (fdrop1 int, fdrop2 int, fdrop3 int, fdrop4 int, b int, a int)
PARTITION BY HASH (a);
ALTER TABLE fk_partitioned_fk_3 DROP COLUMN fdrop1, DROP COLUMN fdrop2,
DROP COLUMN fdrop3, DROP COLUMN fdrop4;
CREATE TABLE fk_partitioned_fk_3_0 PARTITION OF fk_partitioned_fk_3 FOR VALUES WITH (MODULUS 5, REMAINDER 0);
CREATE TABLE fk_partitioned_fk_3_1 PARTITION OF fk_partitioned_fk_3 FOR VALUES WITH (MODULUS 5, REMAINDER 1);
+-- Merge the not-enforced parent constraint with the enforced and not-enforced child constraints
+ALTER TABLE fk_partitioned_fk_3_0 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ENFORCED;
+ALTER TABLE fk_partitioned_fk_3_1 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk_3 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+
+-- Merge the non-enforced parent constraint with both the enforced and
+-- non-enforced child constraints, where the referenced table is partitioned.
+CREATE TABLE fk_partitioned_pk (a int, b int, PRIMARY KEY (a, b)) PARTITION BY RANGE (a, b);
+CREATE TABLE fk_partitioned_pk_1 PARTITION OF fk_partitioned_pk FOR VALUES FROM (0,0) TO (1000,1000);
+-- Merge the enforced parent constraint with the enforced and not-enforced child constraints.
+ALTER TABLE fk_partitioned_fk_3_0 ADD FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk ENFORCED;
+ALTER TABLE fk_partitioned_fk_3_1 ADD FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk_3 ADD FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk ENFORCED;
+
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+
+ALTER TABLE fk_partitioned_fk_3 DETACH PARTITION fk_partitioned_fk_3_0;
+ALTER TABLE fk_partitioned_fk_3 ATTACH PARTITION fk_partitioned_fk_3_0 FOR VALUES WITH (MODULUS 5, REMAINDER 0);
+
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+
+ALTER TABLE fk_partitioned_fk_3 ALTER CONSTRAINT fk_partitioned_fk_3_a_b_fkey1 NOT ENFORCED;
+
+-- Merging an enforced parent constraint (validated) with a not-enforced child
+-- constraint will implicitly change the child constraint to enforced and apply
+-- the validation as well.
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_3
FOR VALUES FROM (2000,2000) TO (3000,3000);
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+
-- Creating a foreign key with ONLY on a partitioned table referencing
-- a non-partitioned table fails.
ALTER TABLE ONLY fk_partitioned_fk ADD FOREIGN KEY (a, b)
@@ -1381,8 +1412,6 @@ WHERE conrelid::regclass::text like 'fk_partitioned_fk%' ORDER BY oid::regclass:
DROP TABLE fk_partitioned_fk, fk_notpartitioned_pk;
-- NOT VALID foreign key on a non-partitioned table referencing a partitioned table
-CREATE TABLE fk_partitioned_pk (a int, b int, PRIMARY KEY (a, b)) PARTITION BY RANGE (a, b);
-CREATE TABLE fk_partitioned_pk_1 PARTITION OF fk_partitioned_pk FOR VALUES FROM (0,0) TO (1000,1000);
CREATE TABLE fk_notpartitioned_fk (b int, a int);
ALTER TABLE fk_notpartitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk NOT VALID;
@@ -1523,7 +1552,7 @@ CREATE TABLE fk_partitioned_fk_2 (b int, a int,
FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
BEGIN;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
--- should be having two constraints
+-- should only have one constraint
\d fk_partitioned_fk_2
DROP TABLE fk_partitioned_fk_2;
ROLLBACK;
--
2.43.5
v15-0001-refactor-re-add-ATExecAlterChildConstr.patchapplication/octet-stream; name=v15-0001-refactor-re-add-ATExecAlterChildConstr.patchDownload
From 2e87af9604e882a4d8e33131dfc9295c3eeda670 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Wed, 26 Feb 2025 15:49:11 +0530
Subject: [PATCH v15 1/9] refactor: re-add ATExecAlterChildConstr
ATExecAlterChildConstr was removed in commit
80d7f990496b1c7be61d9a00a2635b7d96b96197, but it is needed in the
next patches to recurse over child constraints.
---
src/backend/commands/tablecmds.c | 82 ++++++++++++++++++++------------
1 file changed, 52 insertions(+), 30 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index ce7d115667e..12bb71c50e1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -397,6 +397,9 @@ static bool ATExecAlterConstraintInternal(ATAlterConstraint *cmdcon, Relation co
static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
bool deferrable, bool initdeferred,
List **otherrelids);
+static void ATExecAlterChildConstr(ATAlterConstraint *cmdcon, Relation conrel,
+ Relation tgrel, Relation rel, HeapTuple contuple,
+ bool recurse, List **otherrelids, LOCKMODE lockmode);
static ObjectAddress ATExecValidateConstraint(List **wqueue,
Relation rel, char *constrName,
bool recurse, bool recursing, LOCKMODE lockmode);
@@ -11995,41 +11998,13 @@ ATExecAlterConstraintInternal(ATAlterConstraint *cmdcon, Relation conrel,
/*
* If the table at either end of the constraint is partitioned, we need to
* handle every constraint that is a child of this one.
- *
- * Note that this doesn't handle recursion the normal way, viz. by
- * scanning the list of child relations and recursing; instead it uses the
- * conparentid relationships. This may need to be reconsidered.
*/
if (recurse && changed &&
(rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
(OidIsValid(refrelid) &&
get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)))
- {
- ScanKeyData pkey;
- SysScanDesc pscan;
- HeapTuple childtup;
-
- ScanKeyInit(&pkey,
- Anum_pg_constraint_conparentid,
- BTEqualStrategyNumber, F_OIDEQ,
- ObjectIdGetDatum(currcon->oid));
-
- pscan = systable_beginscan(conrel, ConstraintParentIndexId,
- true, NULL, 1, &pkey);
-
- while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
- {
- Form_pg_constraint childcon = (Form_pg_constraint) GETSTRUCT(childtup);
- Relation childrel;
-
- childrel = table_open(childcon->conrelid, lockmode);
- ATExecAlterConstraintInternal(cmdcon, conrel, tgrel, childrel, childtup,
- recurse, otherrelids, lockmode);
- table_close(childrel, NoLock);
- }
-
- systable_endscan(pscan);
- }
+ ATExecAlterChildConstr(cmdcon, conrel, tgrel, rel, contuple,
+ recurse, otherrelids, lockmode);
return changed;
}
@@ -12099,6 +12074,53 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
systable_endscan(tgscan);
}
+/*
+ * Invokes ATExecAlterConstraintInternal for each constraint that is a child of
+ * the specified constraint.
+ *
+ * Note that this doesn't handle recursion the normal way, viz. by scanning the
+ * list of child relations and recursing; instead it uses the conparentid
+ * relationships. This may need to be reconsidered.
+ *
+ * The arguments to this function have the same meaning as the arguments to
+ * ATExecAlterConstraintInternal.
+ */
+static void
+ATExecAlterChildConstr(ATAlterConstraint *cmdcon, Relation conrel,
+ Relation tgrel, Relation rel, HeapTuple contuple,
+ bool recurse, List **otherrelids, LOCKMODE lockmode)
+{
+ Form_pg_constraint currcon;
+ Oid conoid;
+ ScanKeyData pkey;
+ SysScanDesc pscan;
+ HeapTuple childtup;
+
+ currcon = (Form_pg_constraint) GETSTRUCT(contuple);
+ conoid = currcon->oid;
+
+ ScanKeyInit(&pkey,
+ Anum_pg_constraint_conparentid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+
+ pscan = systable_beginscan(conrel, ConstraintParentIndexId,
+ true, NULL, 1, &pkey);
+
+ while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
+ {
+ Form_pg_constraint childcon = (Form_pg_constraint) GETSTRUCT(childtup);
+ Relation childrel;
+
+ childrel = table_open(childcon->conrelid, lockmode);
+ ATExecAlterConstraintInternal(cmdcon, conrel, tgrel, childrel, childtup,
+ recurse, otherrelids, lockmode);
+ table_close(childrel, NoLock);
+ }
+
+ systable_endscan(pscan);
+}
+
/*
* ALTER TABLE VALIDATE CONSTRAINT
*
--
2.43.5
v15-0002-refactor-Split-tryAttachPartitionForeignKey.patchapplication/octet-stream; name=v15-0002-refactor-Split-tryAttachPartitionForeignKey.patchDownload
From f18e6fee19667b127875788be9042e051a70f01c Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Wed, 29 Jan 2025 11:35:27 +0530
Subject: [PATCH v15 2/9] refactor: Split tryAttachPartitionForeignKey()
Split tryAttachPartitionForeignKey() into three functions:
AttachPartitionForeignKey(), RemoveInheritedConstraint() and
DropForeignKeyConstraintTriggers(), so they can be reused in the next
patch.
---
src/backend/commands/tablecmds.c | 319 ++++++++++++++++++++-----------
1 file changed, 204 insertions(+), 115 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 12bb71c50e1..cb62e138a94 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -583,6 +583,14 @@ static bool tryAttachPartitionForeignKey(List **wqueue,
Oid parentInsTrigger,
Oid parentUpdTrigger,
Relation trigrel);
+static void AttachPartitionForeignKey(List **wqueue, Relation partition,
+ Oid partConstrOid, Oid parentConstrOid,
+ Oid parentInsTrigger, Oid parentUpdTrigger,
+ Relation trigrel);
+static void RemoveInheritedConstraint(Relation conrel, Relation trigrel,
+ Oid conoid, Oid conrelid);
+static void DropForeignKeyConstraintTriggers(Relation trigrel, Oid conoid,
+ Oid confrelid, Oid conrelid);
static void GetForeignKeyActionTriggers(Relation trigrel,
Oid conoid, Oid confrelid, Oid conrelid,
Oid *deleteTriggerOid,
@@ -11446,12 +11454,6 @@ tryAttachPartitionForeignKey(List **wqueue,
Form_pg_constraint parentConstr;
HeapTuple partcontup;
Form_pg_constraint partConstr;
- bool queueValidation;
- ScanKeyData key;
- SysScanDesc scan;
- HeapTuple trigtup;
- Oid insertTriggerOid,
- updateTriggerOid;
parentConstrTup = SearchSysCache1(CONSTROID,
ObjectIdGetDatum(parentConstrOid));
@@ -11496,6 +11498,59 @@ tryAttachPartitionForeignKey(List **wqueue,
return false;
}
+ ReleaseSysCache(parentConstrTup);
+ ReleaseSysCache(partcontup);
+
+ /* Looks good! Attach this constraint. */
+ AttachPartitionForeignKey(wqueue, partition, fk->conoid,
+ parentConstrOid, parentInsTrigger,
+ parentUpdTrigger, trigrel);
+
+ return true;
+}
+
+/*
+ * AttachPartitionForeignKey
+ *
+ * The subroutine for tryAttachPartitionForeignKey performs the final tasks of
+ * attaching the constraint, removing redundant triggers and entries from
+ * pg_constraint, and setting the constraint's parent.
+ */
+static void
+AttachPartitionForeignKey(List **wqueue,
+ Relation partition,
+ Oid partConstrOid,
+ Oid parentConstrOid,
+ Oid parentInsTrigger,
+ Oid parentUpdTrigger,
+ Relation trigrel)
+{
+ HeapTuple parentConstrTup;
+ Form_pg_constraint parentConstr;
+ HeapTuple partcontup;
+ Form_pg_constraint partConstr;
+ bool queueValidation;
+ Oid partConstrFrelid;
+ Oid partConstrRelid;
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
+ /* Fetch the parent constraint tuple */
+ parentConstrTup = SearchSysCache1(CONSTROID,
+ ObjectIdGetDatum(parentConstrOid));
+ if (!HeapTupleIsValid(parentConstrTup))
+ elog(ERROR, "cache lookup failed for constraint %u", parentConstrOid);
+ parentConstr = (Form_pg_constraint) GETSTRUCT(parentConstrTup);
+
+ /* Fetch the child constraint tuple */
+ partcontup = SearchSysCache1(CONSTROID,
+ ObjectIdGetDatum(partConstrOid));
+ if (!HeapTupleIsValid(partcontup))
+ elog(ERROR, "cache lookup failed for constraint %u", partConstrOid);
+ partConstr = (Form_pg_constraint) GETSTRUCT(partcontup);
+ partConstrFrelid = partConstr->confrelid;
+ partConstrRelid = partConstr->conrelid;
+
/*
* Will we need to validate this constraint? A valid parent constraint
* implies that all child constraints have been validated, so if this one
@@ -11507,50 +11562,15 @@ tryAttachPartitionForeignKey(List **wqueue,
ReleaseSysCache(parentConstrTup);
/*
- * Looks good! Attach this constraint. The action triggers in the new
- * partition become redundant -- the parent table already has equivalent
- * ones, and those will be able to reach the partition. Remove the ones
- * in the partition. We identify them because they have our constraint
- * OID, as well as being on the referenced rel.
+ * The action triggers in the new partition become redundant -- the parent
+ * table already has equivalent ones, and those will be able to reach the
+ * partition. Remove the ones in the partition. We identify them because
+ * they have our constraint OID, as well as being on the referenced rel.
*/
- ScanKeyInit(&key,
- Anum_pg_trigger_tgconstraint,
- BTEqualStrategyNumber, F_OIDEQ,
- ObjectIdGetDatum(fk->conoid));
- scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
- NULL, 1, &key);
- while ((trigtup = systable_getnext(scan)) != NULL)
- {
- Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
- ObjectAddress trigger;
+ DropForeignKeyConstraintTriggers(trigrel, partConstrOid, partConstrFrelid,
+ partConstrRelid);
- if (trgform->tgconstrrelid != fk->conrelid)
- continue;
- if (trgform->tgrelid != fk->confrelid)
- continue;
-
- /*
- * The constraint is originally set up to contain this trigger as an
- * implementation object, so there's a dependency record that links
- * the two; however, since the trigger is no longer needed, we remove
- * the dependency link in order to be able to drop the trigger while
- * keeping the constraint intact.
- */
- deleteDependencyRecordsFor(TriggerRelationId,
- trgform->oid,
- false);
- /* make dependency deletion visible to performDeletion */
- CommandCounterIncrement();
- ObjectAddressSet(trigger, TriggerRelationId,
- trgform->oid);
- performDeletion(&trigger, DROP_RESTRICT, 0);
- /* make trigger drop visible, in case the loop iterates */
- CommandCounterIncrement();
- }
-
- systable_endscan(scan);
-
- ConstraintSetParentConstraint(fk->conoid, parentConstrOid,
+ ConstraintSetParentConstraint(partConstrOid, parentConstrOid,
RelationGetRelid(partition));
/*
@@ -11558,7 +11578,7 @@ tryAttachPartitionForeignKey(List **wqueue,
* corresponding parent triggers.
*/
GetForeignKeyCheckTriggers(trigrel,
- fk->conoid, fk->confrelid, fk->conrelid,
+ partConstrOid, partConstrFrelid, partConstrRelid,
&insertTriggerOid, &updateTriggerOid);
Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
@@ -11572,72 +11592,12 @@ tryAttachPartitionForeignKey(List **wqueue,
* attaching now has extra pg_constraint rows and action triggers that are
* no longer needed. Remove those.
*/
- if (get_rel_relkind(fk->confrelid) == RELKIND_PARTITIONED_TABLE)
+ if (get_rel_relkind(partConstrFrelid) == RELKIND_PARTITIONED_TABLE)
{
Relation pg_constraint = table_open(ConstraintRelationId, RowShareLock);
- ObjectAddresses *objs;
- HeapTuple consttup;
- ScanKeyInit(&key,
- Anum_pg_constraint_conrelid,
- BTEqualStrategyNumber, F_OIDEQ,
- ObjectIdGetDatum(fk->conrelid));
-
- scan = systable_beginscan(pg_constraint,
- ConstraintRelidTypidNameIndexId,
- true, NULL, 1, &key);
- objs = new_object_addresses();
- while ((consttup = systable_getnext(scan)) != NULL)
- {
- Form_pg_constraint conform = (Form_pg_constraint) GETSTRUCT(consttup);
-
- if (conform->conparentid != fk->conoid)
- continue;
- else
- {
- ObjectAddress addr;
- SysScanDesc scan2;
- ScanKeyData key2;
- int n PG_USED_FOR_ASSERTS_ONLY;
-
- ObjectAddressSet(addr, ConstraintRelationId, conform->oid);
- add_exact_object_address(&addr, objs);
-
- /*
- * First we must delete the dependency record that binds the
- * constraint records together.
- */
- n = deleteDependencyRecordsForSpecific(ConstraintRelationId,
- conform->oid,
- DEPENDENCY_INTERNAL,
- ConstraintRelationId,
- fk->conoid);
- Assert(n == 1); /* actually only one is expected */
-
- /*
- * Now search for the triggers for this constraint and set
- * them up for deletion too
- */
- ScanKeyInit(&key2,
- Anum_pg_trigger_tgconstraint,
- BTEqualStrategyNumber, F_OIDEQ,
- ObjectIdGetDatum(conform->oid));
- scan2 = systable_beginscan(trigrel, TriggerConstraintIndexId,
- true, NULL, 1, &key2);
- while ((trigtup = systable_getnext(scan2)) != NULL)
- {
- ObjectAddressSet(addr, TriggerRelationId,
- ((Form_pg_trigger) GETSTRUCT(trigtup))->oid);
- add_exact_object_address(&addr, objs);
- }
- systable_endscan(scan2);
- }
- }
- /* make the dependency deletions visible */
- CommandCounterIncrement();
- performMultipleDeletions(objs, DROP_RESTRICT,
- PERFORM_DELETION_INTERNAL);
- systable_endscan(scan);
+ RemoveInheritedConstraint(pg_constraint, trigrel, partConstrOid,
+ partConstrRelid);
table_close(pg_constraint, RowShareLock);
}
@@ -11658,9 +11618,10 @@ tryAttachPartitionForeignKey(List **wqueue,
Relation conrel;
conrel = table_open(ConstraintRelationId, RowExclusiveLock);
- partcontup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
+
+ partcontup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(partConstrOid));
if (!HeapTupleIsValid(partcontup))
- elog(ERROR, "cache lookup failed for constraint %u", fk->conoid);
+ elog(ERROR, "cache lookup failed for constraint %u", partConstrOid);
/* Use the same lock as for AT_ValidateConstraint */
QueueFKConstraintValidation(wqueue, conrel, partition, partcontup,
@@ -11668,8 +11629,136 @@ tryAttachPartitionForeignKey(List **wqueue,
ReleaseSysCache(partcontup);
table_close(conrel, RowExclusiveLock);
}
+}
- return true;
+/*
+ * RemoveInheritedConstraint
+ *
+ * Removes the constraint and its associated trigger from the specified
+ * relation, which inherited the given constraint.
+ */
+static void
+RemoveInheritedConstraint(Relation conrel, Relation trigrel, Oid conoid,
+ Oid conrelid)
+{
+ ObjectAddresses *objs;
+ HeapTuple consttup;
+ ScanKeyData key;
+ SysScanDesc scan;
+ HeapTuple trigtup;
+
+ ScanKeyInit(&key,
+ Anum_pg_constraint_conrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conrelid));
+
+ scan = systable_beginscan(conrel,
+ ConstraintRelidTypidNameIndexId,
+ true, NULL, 1, &key);
+ objs = new_object_addresses();
+ while ((consttup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_constraint conform = (Form_pg_constraint) GETSTRUCT(consttup);
+
+ if (conform->conparentid != conoid)
+ continue;
+ else
+ {
+ ObjectAddress addr;
+ SysScanDesc scan2;
+ ScanKeyData key2;
+ int n PG_USED_FOR_ASSERTS_ONLY;
+
+ ObjectAddressSet(addr, ConstraintRelationId, conform->oid);
+ add_exact_object_address(&addr, objs);
+
+ /*
+ * First we must delete the dependency record that binds the
+ * constraint records together.
+ */
+ n = deleteDependencyRecordsForSpecific(ConstraintRelationId,
+ conform->oid,
+ DEPENDENCY_INTERNAL,
+ ConstraintRelationId,
+ conoid);
+ Assert(n == 1); /* actually only one is expected */
+
+ /*
+ * Now search for the triggers for this constraint and set them up
+ * for deletion too
+ */
+ ScanKeyInit(&key2,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conform->oid));
+ scan2 = systable_beginscan(trigrel, TriggerConstraintIndexId,
+ true, NULL, 1, &key2);
+ while ((trigtup = systable_getnext(scan2)) != NULL)
+ {
+ ObjectAddressSet(addr, TriggerRelationId,
+ ((Form_pg_trigger) GETSTRUCT(trigtup))->oid);
+ add_exact_object_address(&addr, objs);
+ }
+ systable_endscan(scan2);
+ }
+ }
+ /* make the dependency deletions visible */
+ CommandCounterIncrement();
+ performMultipleDeletions(objs, DROP_RESTRICT,
+ PERFORM_DELETION_INTERNAL);
+ systable_endscan(scan);
+}
+
+/*
+ * DropForeignKeyConstraintTriggers
+ *
+ * The subroutine for tryAttachPartitionForeignKey handles the deletion of
+ * action triggers for the foreign key constraint.
+ */
+static void
+DropForeignKeyConstraintTriggers(Relation trigrel, Oid conoid, Oid confrelid,
+ Oid conrelid)
+{
+ ScanKeyData key;
+ SysScanDesc scan;
+ HeapTuple trigtup;
+
+ ScanKeyInit(&key,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+ scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
+ NULL, 1, &key);
+ while ((trigtup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
+ ObjectAddress trigger;
+
+ if (trgform->tgconstrrelid != conrelid)
+ continue;
+ if (trgform->tgrelid != confrelid)
+ continue;
+
+ /*
+ * The constraint is originally set up to contain this trigger as an
+ * implementation object, so there's a dependency record that links
+ * the two; however, since the trigger is no longer needed, we remove
+ * the dependency link in order to be able to drop the trigger while
+ * keeping the constraint intact.
+ */
+ deleteDependencyRecordsFor(TriggerRelationId,
+ trgform->oid,
+ false);
+ /* make dependency deletion visible to performDeletion */
+ CommandCounterIncrement();
+ ObjectAddressSet(trigger, TriggerRelationId,
+ trgform->oid);
+ performDeletion(&trigger, DROP_RESTRICT, 0);
+ /* make trigger drop visible, in case the loop iterates */
+ CommandCounterIncrement();
+ }
+
+ systable_endscan(scan);
}
/*
--
2.43.5
v15-0003-Move-the-RemoveInheritedConstraint-function-call.patchapplication/octet-stream; name=v15-0003-Move-the-RemoveInheritedConstraint-function-call.patchDownload
From 443b0ec319cbbd884fd1cfe16fa872a070a1474c Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 10 Feb 2025 09:12:06 +0530
Subject: [PATCH v15 3/9] Move the RemoveInheritedConstraint() function call
slightly earlier.
This change is harmless and does not affect the existing intended
operation. It is necessary for the feature patch operation, where we
may need to change the child constraint to enforced. In this case, we
would create the necessary triggers and queue the constraint for
validation, so it is important to remove any unnecessary constraints
before proceeding.
-- NOTE --
This is a small change that could have been included in the previous
"split tryAttachPartitionForeignKey" refactoring patch, but was kept
separate to highlight the changes.
---------
---
src/backend/commands/tablecmds.c | 30 +++++++++++++++---------------
1 file changed, 15 insertions(+), 15 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index cb62e138a94..c501201cad7 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -11551,6 +11551,21 @@ AttachPartitionForeignKey(List **wqueue,
partConstrFrelid = partConstr->confrelid;
partConstrRelid = partConstr->conrelid;
+ /*
+ * If the referenced table is partitioned, then the partition we're
+ * attaching now has extra pg_constraint rows and action triggers that are
+ * no longer needed. Remove those.
+ */
+ if (get_rel_relkind(partConstrFrelid) == RELKIND_PARTITIONED_TABLE)
+ {
+ Relation pg_constraint = table_open(ConstraintRelationId, RowShareLock);
+
+ RemoveInheritedConstraint(pg_constraint, trigrel, partConstrOid,
+ partConstrRelid);
+
+ table_close(pg_constraint, RowShareLock);
+ }
+
/*
* Will we need to validate this constraint? A valid parent constraint
* implies that all child constraints have been validated, so if this one
@@ -11587,21 +11602,6 @@ AttachPartitionForeignKey(List **wqueue,
TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
RelationGetRelid(partition));
- /*
- * If the referenced table is partitioned, then the partition we're
- * attaching now has extra pg_constraint rows and action triggers that are
- * no longer needed. Remove those.
- */
- if (get_rel_relkind(partConstrFrelid) == RELKIND_PARTITIONED_TABLE)
- {
- Relation pg_constraint = table_open(ConstraintRelationId, RowShareLock);
-
- RemoveInheritedConstraint(pg_constraint, trigrel, partConstrOid,
- partConstrRelid);
-
- table_close(pg_constraint, RowShareLock);
- }
-
/*
* We updated this pg_constraint row above to set its parent; validating
* it will cause its convalidated flag to change, so we need CCI here. In
--
2.43.5
v15-0004-refactor-Pass-Relid-instead-of-Relation-to-creat.patchapplication/octet-stream; name=v15-0004-refactor-Pass-Relid-instead-of-Relation-to-creat.patchDownload
From 2d66089ab99812e333aa460d7be20057c614a822 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 20 Jan 2025 18:49:41 +0530
Subject: [PATCH v15 4/9] refactor: Pass Relid instead of Relation to
createForeignKeyCheckTriggers().
Currently, createForeignKeyCheckTriggers() takes a Relation type as
its first argument, but it doesn't use that argument directly.
Instead, it fetches the Relid by calling RelationGetRelid().
Therefore, it would be more consistent with other functions (e.g.,
createForeignKeyCheckTriggers()) to pass the Relid directly instead of
the whole Relation.
---
src/backend/commands/tablecmds.c | 20 ++++++++++----------
1 file changed, 10 insertions(+), 10 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c501201cad7..288d033434d 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -569,7 +569,7 @@ static void createForeignKeyCheckTriggers(Oid myRelOid, Oid refRelOid,
Oid indexOid,
Oid parentInsTrigger, Oid parentUpdTrigger,
Oid *insertTrigOid, Oid *updateTrigOid);
-static void createForeignKeyActionTriggers(Relation rel, Oid refRelOid,
+static void createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid,
Constraint *fkconstraint, Oid constraintOid,
Oid indexOid,
Oid parentDelTrigger, Oid parentUpdTrigger,
@@ -10660,7 +10660,8 @@ addFkRecurseReferenced(Constraint *fkconstraint, Relation rel,
/*
* Create the action triggers that enforce the constraint.
*/
- createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
+ createForeignKeyActionTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
fkconstraint,
parentConstr, indexOid,
parentDelTrigger, parentUpdTrigger,
@@ -13041,10 +13042,11 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
* *updateTrigOid.
*/
static void
-createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstraint,
- Oid constraintOid, Oid indexOid,
- Oid parentDelTrigger, Oid parentUpdTrigger,
- Oid *deleteTrigOid, Oid *updateTrigOid)
+createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid,
+ Constraint *fkconstraint, Oid constraintOid,
+ Oid indexOid, Oid parentDelTrigger,
+ Oid parentUpdTrigger, Oid *deleteTrigOid,
+ Oid *updateTrigOid)
{
CreateTrigStmt *fk_trigger;
ObjectAddress trigAddress;
@@ -13100,8 +13102,7 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
break;
}
- trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid,
- RelationGetRelid(rel),
+ trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid, myRelOid,
constraintOid, indexOid, InvalidOid,
parentDelTrigger, NULL, true, false);
if (deleteTrigOid)
@@ -13161,8 +13162,7 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
break;
}
- trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid,
- RelationGetRelid(rel),
+ trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid, myRelOid,
constraintOid, indexOid, InvalidOid,
parentUpdTrigger, NULL, true, false);
if (updateTrigOid)
--
2.43.5
v15-0005-refactor-Change-ATExecAlterConstrRecurse-argumen.patchapplication/octet-stream; name=v15-0005-refactor-Change-ATExecAlterConstrRecurse-argumen.patchDownload
From 8eb630bfa9b8147c742a27a4974d49bacec9c5f2 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Tue, 14 Jan 2025 22:35:32 +0530
Subject: [PATCH v15 5/9] refactor: Change ATExecAlterConstrRecurse() argument.
Instead of passing the Relation of the referencing relation, we will
pass a relid. Opening the relation using the relid again should not
cause significant issues. However, this approach makes recursion more
efficient, especially in the later patch where we will be recreating
the referencing and referred triggers. In addition to that passing
relid of the referred relation too.
----
NOTE: This patch is not meant to be committed separately. It should
be squashed with the main patch that adds ENFORCED/NOT ENFORCED.
----
---
src/backend/commands/tablecmds.c | 45 ++++++++++++++++++--------------
1 file changed, 25 insertions(+), 20 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 288d033434d..1e93591e3e0 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -392,14 +392,18 @@ static void AlterSeqNamespaces(Relation classRel, Relation rel,
static ObjectAddress ATExecAlterConstraint(Relation rel, ATAlterConstraint *cmdcon,
bool recurse, LOCKMODE lockmode);
static bool ATExecAlterConstraintInternal(ATAlterConstraint *cmdcon, Relation conrel,
- Relation tgrel, Relation rel, HeapTuple contuple,
- bool recurse, List **otherrelids, LOCKMODE lockmode);
+ Relation tgrel, const Oid fkrelid,
+ const Oid pkrelid, HeapTuple contuple,
+ bool recurse, List **otherrelids,
+ LOCKMODE lockmode);
static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
bool deferrable, bool initdeferred,
List **otherrelids);
static void ATExecAlterChildConstr(ATAlterConstraint *cmdcon, Relation conrel,
- Relation tgrel, Relation rel, HeapTuple contuple,
- bool recurse, List **otherrelids, LOCKMODE lockmode);
+ Relation tgrel, const Oid fkrelid,
+ const Oid pkrelid, HeapTuple contuple,
+ bool recurse, List **otherrelids,
+ LOCKMODE lockmode);
static ObjectAddress ATExecValidateConstraint(List **wqueue,
Relation rel, char *constrName,
bool recurse, bool recursing, LOCKMODE lockmode);
@@ -12002,8 +12006,9 @@ ATExecAlterConstraint(Relation rel, ATAlterConstraint *cmdcon, bool recurse,
/*
* Do the actual catalog work, and recurse if necessary.
*/
- if (ATExecAlterConstraintInternal(cmdcon, conrel, tgrel, rel, contuple,
- recurse, &otherrelids, lockmode))
+ if (ATExecAlterConstraintInternal(cmdcon, conrel, tgrel, currcon->conrelid,
+ currcon->confrelid, contuple, recurse,
+ &otherrelids, lockmode))
ObjectAddressSet(address, ConstraintRelationId, currcon->oid);
/*
@@ -12035,12 +12040,14 @@ ATExecAlterConstraint(Relation rel, ATAlterConstraint *cmdcon, bool recurse,
*/
static bool
ATExecAlterConstraintInternal(ATAlterConstraint *cmdcon, Relation conrel,
- Relation tgrel, Relation rel, HeapTuple contuple,
+ Relation tgrel, const Oid fkrelid,
+ const Oid pkrelid, HeapTuple contuple,
bool recurse, List **otherrelids, LOCKMODE lockmode)
{
Form_pg_constraint currcon;
Oid refrelid = InvalidOid;
bool changed = false;
+ Relation rel;
/* since this function recurses, it could be driven to stack overflow */
check_stack_depth();
@@ -12049,6 +12056,8 @@ ATExecAlterConstraintInternal(ATAlterConstraint *cmdcon, Relation conrel,
if (currcon->contype == CONSTRAINT_FOREIGN)
refrelid = currcon->confrelid;
+ rel = table_open(currcon->conrelid, lockmode);
+
/*
* Update pg_constraint with the flags from cmdcon.
*
@@ -12093,8 +12102,10 @@ ATExecAlterConstraintInternal(ATAlterConstraint *cmdcon, Relation conrel,
(rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
(OidIsValid(refrelid) &&
get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)))
- ATExecAlterChildConstr(cmdcon, conrel, tgrel, rel, contuple,
- recurse, otherrelids, lockmode);
+ ATExecAlterChildConstr(cmdcon, conrel, tgrel, fkrelid, pkrelid,
+ contuple, recurse, otherrelids, lockmode);
+
+ table_close(rel, NoLock);
return changed;
}
@@ -12177,8 +12188,9 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
*/
static void
ATExecAlterChildConstr(ATAlterConstraint *cmdcon, Relation conrel,
- Relation tgrel, Relation rel, HeapTuple contuple,
- bool recurse, List **otherrelids, LOCKMODE lockmode)
+ Relation tgrel, const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, bool recurse, List **otherrelids,
+ LOCKMODE lockmode)
{
Form_pg_constraint currcon;
Oid conoid;
@@ -12198,15 +12210,8 @@ ATExecAlterChildConstr(ATAlterConstraint *cmdcon, Relation conrel,
true, NULL, 1, &pkey);
while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
- {
- Form_pg_constraint childcon = (Form_pg_constraint) GETSTRUCT(childtup);
- Relation childrel;
-
- childrel = table_open(childcon->conrelid, lockmode);
- ATExecAlterConstraintInternal(cmdcon, conrel, tgrel, childrel, childtup,
- recurse, otherrelids, lockmode);
- table_close(childrel, NoLock);
- }
+ ATExecAlterConstraintInternal(cmdcon, conrel, tgrel, fkrelid, pkrelid,
+ childtup, recurse, otherrelids, lockmode);
systable_endscan(pscan);
}
--
2.43.5
v15-0006-Remove-hastriggers-flag-check-before-fetching-FK.patchapplication/octet-stream; name=v15-0006-Remove-hastriggers-flag-check-before-fetching-FK.patchDownload
From e99a492aa8d7f8477b058ef63f37f03d92276177 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 7 Oct 2024 12:40:07 +0530
Subject: [PATCH v15 6/9] Remove hastriggers flag check before fetching FK
constraints.
With NOT ENFORCED, foreign key constraints will be created without
triggers. Therefore, the criteria for fetching foreign keys based on
the presence of triggers no longer apply.
----
NOTE: This patch is intended to reduce the diff noise from the main
patch and is not meant to be committed separately. It should be
squashed with the main patch that adds ENFORCED/NOT ENFORCED.
----
---
src/backend/utils/cache/relcache.c | 5 -
src/bin/pg_dump/pg_dump.c | 8 +-
src/bin/psql/describe.c | 226 ++++++++++++++---------------
3 files changed, 108 insertions(+), 131 deletions(-)
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 398114373e9..c010de4aeca 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4661,11 +4661,6 @@ RelationGetFKeyList(Relation relation)
if (relation->rd_fkeyvalid)
return relation->rd_fkeylist;
- /* Fast path: non-partitioned tables without triggers can't have FKs */
- if (!relation->rd_rel->relhastriggers &&
- relation->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
- return NIL;
-
/*
* We build the list we intend to return (in the caller's context) while
* doing the scan. After successfully completing the scan, we copy that
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 0de6c959bb0..14b19d21dbb 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -7954,13 +7954,7 @@ getConstraints(Archive *fout, TableInfo tblinfo[], int numTables)
{
TableInfo *tinfo = &tblinfo[i];
- /*
- * For partitioned tables, foreign keys have no triggers so they must
- * be included anyway in case some foreign keys are defined.
- */
- if ((!tinfo->hastriggers &&
- tinfo->relkind != RELKIND_PARTITIONED_TABLE) ||
- !(tinfo->dobj.dump & DUMP_COMPONENT_DEFINITION))
+ if (!(tinfo->dobj.dump & DUMP_COMPONENT_DEFINITION))
continue;
/* OK, we need info for this table */
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..a23abb2417c 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2550,136 +2550,124 @@ describeOneTableDetails(const char *schemaname,
PQclear(result);
}
- /*
- * Print foreign-key constraints (there are none if no triggers,
- * except if the table is partitioned, in which case the triggers
- * appear in the partitions)
- */
- if (tableinfo.hastriggers ||
- tableinfo.relkind == RELKIND_PARTITIONED_TABLE)
+ /* Print foreign-key constraints */
+ if (pset.sversion >= 120000 &&
+ (tableinfo.ispartition || tableinfo.relkind == RELKIND_PARTITIONED_TABLE))
{
- if (pset.sversion >= 120000 &&
- (tableinfo.ispartition || tableinfo.relkind == RELKIND_PARTITIONED_TABLE))
+ /*
+ * Put the constraints defined in this table first, followed
+ * by the constraints defined in ancestor partitioned tables.
+ */
+ printfPQExpBuffer(&buf,
+ "SELECT conrelid = '%s'::pg_catalog.regclass AS sametable,\n"
+ " conname,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef,\n"
+ " conrelid::pg_catalog.regclass AS ontable\n"
+ " FROM pg_catalog.pg_constraint,\n"
+ " pg_catalog.pg_partition_ancestors('%s')\n"
+ " WHERE conrelid = relid AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
+ "ORDER BY sametable DESC, conname;",
+ oid, oid);
+ }
+ else
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT true as sametable, conname,\n"
+ " pg_catalog.pg_get_constraintdef(r.oid, true) as condef,\n"
+ " conrelid::pg_catalog.regclass AS ontable\n"
+ "FROM pg_catalog.pg_constraint r\n"
+ "WHERE r.conrelid = '%s' AND r.contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n",
+ oid);
+
+ if (pset.sversion >= 120000)
+ appendPQExpBufferStr(&buf, " AND conparentid = 0\n");
+ appendPQExpBufferStr(&buf, "ORDER BY conname");
+ }
+
+ result = PSQLexec(buf.data);
+ if (!result)
+ goto error_return;
+ else
+ tuples = PQntuples(result);
+
+ if (tuples > 0)
+ {
+ int i_sametable = PQfnumber(result, "sametable"),
+ i_conname = PQfnumber(result, "conname"),
+ i_condef = PQfnumber(result, "condef"),
+ i_ontable = PQfnumber(result, "ontable");
+
+ printTableAddFooter(&cont, _("Foreign-key constraints:"));
+ for (i = 0; i < tuples; i++)
{
/*
- * Put the constraints defined in this table first, followed
- * by the constraints defined in ancestor partitioned tables.
+ * Print untranslated constraint name and definition. Use
+ * a "TABLE tab" prefix when the constraint is defined in
+ * a parent partitioned table.
*/
- printfPQExpBuffer(&buf,
- "SELECT conrelid = '%s'::pg_catalog.regclass AS sametable,\n"
- " conname,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef,\n"
- " conrelid::pg_catalog.regclass AS ontable\n"
- " FROM pg_catalog.pg_constraint,\n"
- " pg_catalog.pg_partition_ancestors('%s')\n"
- " WHERE conrelid = relid AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
- "ORDER BY sametable DESC, conname;",
- oid, oid);
- }
- else
- {
- printfPQExpBuffer(&buf,
- "SELECT true as sametable, conname,\n"
- " pg_catalog.pg_get_constraintdef(r.oid, true) as condef,\n"
- " conrelid::pg_catalog.regclass AS ontable\n"
- "FROM pg_catalog.pg_constraint r\n"
- "WHERE r.conrelid = '%s' AND r.contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n",
- oid);
-
- if (pset.sversion >= 120000)
- appendPQExpBufferStr(&buf, " AND conparentid = 0\n");
- appendPQExpBufferStr(&buf, "ORDER BY conname");
- }
-
- result = PSQLexec(buf.data);
- if (!result)
- goto error_return;
- else
- tuples = PQntuples(result);
-
- if (tuples > 0)
- {
- int i_sametable = PQfnumber(result, "sametable"),
- i_conname = PQfnumber(result, "conname"),
- i_condef = PQfnumber(result, "condef"),
- i_ontable = PQfnumber(result, "ontable");
-
- printTableAddFooter(&cont, _("Foreign-key constraints:"));
- for (i = 0; i < tuples; i++)
- {
- /*
- * Print untranslated constraint name and definition. Use
- * a "TABLE tab" prefix when the constraint is defined in
- * a parent partitioned table.
- */
- if (strcmp(PQgetvalue(result, i, i_sametable), "f") == 0)
- printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
- PQgetvalue(result, i, i_ontable),
- PQgetvalue(result, i, i_conname),
- PQgetvalue(result, i, i_condef));
- else
- printfPQExpBuffer(&buf, " \"%s\" %s",
- PQgetvalue(result, i, i_conname),
- PQgetvalue(result, i, i_condef));
-
- printTableAddFooter(&cont, buf.data);
- }
- }
- PQclear(result);
- }
-
- /* print incoming foreign-key references */
- if (tableinfo.hastriggers ||
- tableinfo.relkind == RELKIND_PARTITIONED_TABLE)
- {
- if (pset.sversion >= 120000)
- {
- printfPQExpBuffer(&buf,
- "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
- " FROM pg_catalog.pg_constraint c\n"
- " WHERE confrelid IN (SELECT pg_catalog.pg_partition_ancestors('%s')\n"
- " UNION ALL VALUES ('%s'::pg_catalog.regclass))\n"
- " AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
- "ORDER BY conname;",
- oid, oid);
- }
- else
- {
- printfPQExpBuffer(&buf,
- "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
- " FROM pg_catalog.pg_constraint\n"
- " WHERE confrelid = %s AND contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n"
- "ORDER BY conname;",
- oid);
- }
-
- result = PSQLexec(buf.data);
- if (!result)
- goto error_return;
- else
- tuples = PQntuples(result);
-
- if (tuples > 0)
- {
- int i_conname = PQfnumber(result, "conname"),
- i_ontable = PQfnumber(result, "ontable"),
- i_condef = PQfnumber(result, "condef");
-
- printTableAddFooter(&cont, _("Referenced by:"));
- for (i = 0; i < tuples; i++)
- {
+ if (strcmp(PQgetvalue(result, i, i_sametable), "f") == 0)
printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
PQgetvalue(result, i, i_ontable),
PQgetvalue(result, i, i_conname),
PQgetvalue(result, i, i_condef));
+ else
+ printfPQExpBuffer(&buf, " \"%s\" %s",
+ PQgetvalue(result, i, i_conname),
+ PQgetvalue(result, i, i_condef));
- printTableAddFooter(&cont, buf.data);
- }
+ printTableAddFooter(&cont, buf.data);
}
- PQclear(result);
}
+ PQclear(result);
+
+ /* print incoming foreign-key references */
+ if (pset.sversion >= 120000)
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
+ " FROM pg_catalog.pg_constraint c\n"
+ " WHERE confrelid IN (SELECT pg_catalog.pg_partition_ancestors('%s')\n"
+ " UNION ALL VALUES ('%s'::pg_catalog.regclass))\n"
+ " AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
+ "ORDER BY conname;",
+ oid, oid);
+ }
+ else
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
+ " FROM pg_catalog.pg_constraint\n"
+ " WHERE confrelid = %s AND contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n"
+ "ORDER BY conname;",
+ oid);
+ }
+
+ result = PSQLexec(buf.data);
+ if (!result)
+ goto error_return;
+ else
+ tuples = PQntuples(result);
+
+ if (tuples > 0)
+ {
+ int i_conname = PQfnumber(result, "conname"),
+ i_ontable = PQfnumber(result, "ontable"),
+ i_condef = PQfnumber(result, "condef");
+
+ printTableAddFooter(&cont, _("Referenced by:"));
+ for (i = 0; i < tuples; i++)
+ {
+ printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
+ PQgetvalue(result, i, i_ontable),
+ PQgetvalue(result, i, i_conname),
+ PQgetvalue(result, i, i_condef));
+
+ printTableAddFooter(&cont, buf.data);
+ }
+ }
+ PQclear(result);
/* print any row-level policies */
if (pset.sversion >= 90500)
--
2.43.5
v15-0007-Ease-the-restriction-that-a-NOT-ENFORCED-constra.patchapplication/octet-stream; name=v15-0007-Ease-the-restriction-that-a-NOT-ENFORCED-constra.patchDownload
From 60346ea7bbf93b431b5b72bbe07c802bcacf30ae Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Fri, 7 Feb 2025 09:41:20 +0530
Subject: [PATCH v15 7/9] Ease the restriction that a NOT ENFORCED constraint
must be INVALID.
In the initial support for NOT ENFORCED check constraints in commit
ca87c415e2fccf81cec6fd45698dde9fae0ab570, we introduced a restriction
that a NOT ENFORCED constraint must be NOT VALID. However, we still
mark a NOT ENFORCED constraint as NOT VALID when adding it to an
already existing table, as validation cannot be performed on a NOT
ENFORCED constraint. Conversely, when the constraint is created along
with the table, it is acceptable to mark the constraint as valid.
---
src/backend/catalog/heap.c | 3 +-
src/backend/catalog/pg_constraint.c | 2 -
src/backend/commands/tablecmds.c | 19 ++---
src/backend/optimizer/util/plancat.c | 9 +--
src/backend/parser/gram.y | 9 ---
src/backend/parser/parse_utilcmd.c | 13 ++--
src/backend/utils/adt/ruleutils.c | 6 +-
src/test/regress/expected/alter_table.out | 4 +-
src/test/regress/expected/inherit.out | 88 ++++++++++++++---------
src/test/regress/sql/alter_table.sql | 3 +-
src/test/regress/sql/inherit.sql | 13 +++-
11 files changed, 84 insertions(+), 85 deletions(-)
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 956f196fc95..913e63fe93f 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -2740,7 +2740,7 @@ MergeWithExistingConstraint(Relation rel, const char *ccname, Node *expr,
* If the child constraint is "not valid" then cannot merge with a
* valid parent constraint.
*/
- if (is_initially_valid && con->conenforced && !con->convalidated)
+ if (is_initially_valid && !con->convalidated)
ereport(ERROR,
(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
errmsg("constraint \"%s\" conflicts with NOT VALID constraint on relation \"%s\"",
@@ -2803,7 +2803,6 @@ MergeWithExistingConstraint(Relation rel, const char *ccname, Node *expr,
{
Assert(is_local);
con->conenforced = true;
- con->convalidated = true;
}
CatalogTupleUpdate(conDesc, &tup->t_self, tup);
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index ac80652baf2..60bc54a96b6 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -102,8 +102,6 @@ CreateConstraintEntry(const char *constraintName,
/* Only CHECK constraint can be not enforced */
Assert(isEnforced || constraintType == CONSTRAINT_CHECK);
- /* NOT ENFORCED constraint must be NOT VALID */
- Assert(isEnforced || !isValidated);
conDesc = table_open(ConstraintRelationId, RowExclusiveLock);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 1e93591e3e0..93f9e3e152e 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -3168,10 +3168,7 @@ MergeCheckConstraint(List *constraints, const char *name, Node *expr, bool is_en
* marked as ENFORCED because one of the parents is ENFORCED.
*/
if (!ccon->is_enforced && is_enforced)
- {
ccon->is_enforced = true;
- ccon->skip_validation = false;
- }
return constraints;
}
@@ -3192,7 +3189,6 @@ MergeCheckConstraint(List *constraints, const char *name, Node *expr, bool is_en
newcon->expr = expr;
newcon->inhcount = 1;
newcon->is_enforced = is_enforced;
- newcon->skip_validation = !is_enforced;
return lappend(constraints, newcon);
}
@@ -16907,8 +16903,7 @@ MergeConstraintsIntoExisting(Relation child_rel, Relation parent_rel)
* If the child constraint is "not valid" then cannot merge with a
* valid parent constraint
*/
- if (parent_con->convalidated && child_con->conenforced &&
- !child_con->convalidated)
+ if (parent_con->convalidated && !child_con->convalidated)
ereport(ERROR,
(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
errmsg("constraint \"%s\" conflicts with NOT VALID constraint on child table \"%s\"",
@@ -19276,18 +19271,12 @@ ConstraintImpliedByRelConstraint(Relation scanrel, List *testConstraint, List *p
Node *cexpr;
/*
- * If this constraint hasn't been fully validated yet, we must ignore
- * it here.
+ * If this constraint hasn't been fully validated yet or is not
+ * enforced, we must ignore it here.
*/
- if (!constr->check[i].ccvalid)
+ if (!constr->check[i].ccvalid || !constr->check[i].ccenforced)
continue;
- /*
- * NOT ENFORCED constraints are always marked as invalid, which should
- * have been ignored.
- */
- Assert(constr->check[i].ccenforced);
-
cexpr = stringToNode(constr->check[i].ccbin);
/*
diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c
index 71abb01f655..233a5d8da98 100644
--- a/src/backend/optimizer/util/plancat.c
+++ b/src/backend/optimizer/util/plancat.c
@@ -1302,22 +1302,15 @@ get_relation_constraints(PlannerInfo *root,
* ignore it here. Also ignore if NO INHERIT and we weren't told
* that that's safe.
*/
- if (!constr->check[i].ccvalid)
+ if (!constr->check[i].ccvalid || !constr->check[i].ccenforced)
continue;
- /*
- * NOT ENFORCED constraints are always marked as invalid, which
- * should have been ignored.
- */
- Assert(constr->check[i].ccenforced);
-
/*
* Also ignore if NO INHERIT and we weren't told that that's safe.
*/
if (constr->check[i].ccnoinherit && !include_noinherit)
continue;
-
cexpr = stringToNode(constr->check[i].ccbin);
/*
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 7d99c9355c6..b790a0f8e22 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -19559,15 +19559,6 @@ processCASbits(int cas_bits, int location, const char *constrType,
errmsg("%s constraints cannot be marked NOT ENFORCED",
constrType),
parser_errposition(location)));
-
- /*
- * NB: The validated status is irrelevant when the constraint is set to
- * NOT ENFORCED, but for consistency, it should be set accordingly.
- * This ensures that if the constraint is later changed to ENFORCED, it
- * will automatically be in the correct NOT VALIDATED state.
- */
- if (not_valid)
- *not_valid = true;
}
if (cas_bits & CAS_ENFORCED)
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index abbe1bb45a3..37becb62356 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -1453,7 +1453,6 @@ expandTableLikeClause(RangeVar *heapRel, TableLikeClause *table_like_clause)
char *ccname = constr->check[ccnum].ccname;
char *ccbin = constr->check[ccnum].ccbin;
bool ccenforced = constr->check[ccnum].ccenforced;
- bool ccvalid = constr->check[ccnum].ccvalid;
bool ccnoinherit = constr->check[ccnum].ccnoinherit;
Node *ccbin_node;
bool found_whole_row;
@@ -1484,13 +1483,13 @@ expandTableLikeClause(RangeVar *heapRel, TableLikeClause *table_like_clause)
n->conname = pstrdup(ccname);
n->location = -1;
n->is_enforced = ccenforced;
- n->initially_valid = ccvalid;
n->is_no_inherit = ccnoinherit;
n->raw_expr = NULL;
n->cooked_expr = nodeToString(ccbin_node);
/* We can skip validation, since the new table should be empty. */
n->skip_validation = true;
+ n->initially_valid = true;
atsubcmd = makeNode(AlterTableCmd);
atsubcmd->subtype = AT_AddConstraint;
@@ -2944,11 +2943,9 @@ transformCheckConstraints(CreateStmtContext *cxt, bool skipValidation)
return;
/*
- * When creating a new table (but not a foreign table), we can safely skip
- * the validation of check constraints and mark them as valid based on the
- * constraint enforcement flag, since NOT ENFORCED constraints must always
- * be marked as NOT VALID. (This will override any user-supplied NOT VALID
- * flag.)
+ * If creating a new table (but not a foreign table), we can safely skip
+ * validation of check constraints, and nonetheless mark them valid. (This
+ * will override any user-supplied NOT VALID flag.)
*/
if (skipValidation)
{
@@ -2957,7 +2954,7 @@ transformCheckConstraints(CreateStmtContext *cxt, bool skipValidation)
Constraint *constraint = (Constraint *) lfirst(ckclist);
constraint->skip_validation = true;
- constraint->initially_valid = constraint->is_enforced;
+ constraint->initially_valid = true;
}
}
}
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index d11a8a20eea..f709ded1136 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -2594,12 +2594,10 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
appendStringInfoString(&buf, " DEFERRABLE");
if (conForm->condeferred)
appendStringInfoString(&buf, " INITIALLY DEFERRED");
-
- /* Validated status is irrelevant when the constraint is NOT ENFORCED. */
+ if (!conForm->convalidated)
+ appendStringInfoString(&buf, " NOT VALID");
if (!conForm->conenforced)
appendStringInfoString(&buf, " NOT ENFORCED");
- else if (!conForm->convalidated)
- appendStringInfoString(&buf, " NOT VALID");
/* Cleanup */
systable_endscan(scandesc);
diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out
index 362f38856d2..54bdb3f2250 100644
--- a/src/test/regress/expected/alter_table.out
+++ b/src/test/regress/expected/alter_table.out
@@ -507,7 +507,9 @@ ALTER TABLE attmp3 validate constraint attmpconstr;
ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten CHECK (b > 10); -- fail
ERROR: check constraint "b_greater_than_ten" of relation "attmp3" is violated by some row
ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten CHECK (b > 10) NOT VALID; -- succeeds
-ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten_not_enforced CHECK (b > 10) NOT ENFORCED; -- succeeds
+ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten_not_enforced CHECK (b > 10) NOT ENFORCED; -- fail
+ERROR: check constraint "b_greater_than_ten_not_enforced" of relation "attmp3" is violated by some row
+ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten_not_enforced CHECK (b > 10) NOT VALID NOT ENFORCED; -- succeeds
ALTER TABLE attmp3 VALIDATE CONSTRAINT b_greater_than_ten; -- fails
ERROR: check constraint "b_greater_than_ten" of relation "attmp3" is violated by some row
DELETE FROM attmp3 WHERE NOT b > 10;
diff --git a/src/test/regress/expected/inherit.out b/src/test/regress/expected/inherit.out
index 420b6ae5996..f34555e7daa 100644
--- a/src/test/regress/expected/inherit.out
+++ b/src/test/regress/expected/inherit.out
@@ -1332,6 +1332,18 @@ NOTICE: merging constraint "inh_check_constraint5" with inherited definition
alter table p1 add constraint inh_check_constraint6 check (f1 < 10) not enforced;
alter table p1_c1 add constraint inh_check_constraint6 check (f1 < 10) enforced;
NOTICE: merging constraint "inh_check_constraint6" with inherited definition
+-- however, cannot merge a not valid enforced child constraint with a valid
+-- not-enforced parent constraint.
+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;
+ERROR: constraint "inh_check_constraint9" conflicts with NOT VALID constraint on relation "p1_c1"
+-- but, allowed if the parent constraint is also invalid.
+alter table p1 add constraint inh_check_constraint9 check (f1 < 10) not valid not enforced;
+NOTICE: merging constraint "inh_check_constraint9" with inherited definition
+-- the invalid state of the child constraint will be ignored here.
+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
create table p1_c2(f1 int constraint inh_check_constraint4 check (f1 < 10)) inherits(p1);
NOTICE: merging column "f1" with inherited definition
NOTICE: merging constraint "inh_check_constraint4" with inherited definition
@@ -1353,42 +1365,50 @@ create table p1_fail(f1 int constraint inh_check_constraint6 check (f1 < 10) not
NOTICE: merging multiple inherited definitions of column "f1"
NOTICE: merging column "f1" with inherited definition
ERROR: constraint "inh_check_constraint6" conflicts with NOT ENFORCED constraint on relation "p1_fail"
-select conrelid::regclass::text as relname, conname, conislocal, coninhcount, conenforced
+select conrelid::regclass::text as relname, conname, conislocal, coninhcount, conenforced, convalidated
from pg_constraint where conname like 'inh\_check\_constraint%'
order by 1, 2;
- relname | conname | conislocal | coninhcount | conenforced
----------+-----------------------+------------+-------------+-------------
- p1 | inh_check_constraint1 | t | 0 | t
- p1 | inh_check_constraint2 | t | 0 | t
- p1 | inh_check_constraint3 | t | 0 | f
- p1 | inh_check_constraint4 | t | 0 | f
- p1 | inh_check_constraint5 | t | 0 | f
- p1 | inh_check_constraint6 | t | 0 | f
- p1 | inh_check_constraint8 | t | 0 | t
- p1_c1 | inh_check_constraint1 | t | 1 | t
- p1_c1 | inh_check_constraint2 | t | 1 | t
- p1_c1 | inh_check_constraint3 | t | 1 | f
- p1_c1 | inh_check_constraint4 | t | 1 | f
- p1_c1 | inh_check_constraint5 | t | 1 | t
- p1_c1 | inh_check_constraint6 | t | 1 | t
- p1_c1 | inh_check_constraint7 | t | 0 | f
- p1_c1 | inh_check_constraint8 | f | 1 | t
- p1_c2 | inh_check_constraint1 | f | 1 | t
- p1_c2 | inh_check_constraint2 | f | 1 | t
- p1_c2 | inh_check_constraint3 | f | 1 | f
- p1_c2 | inh_check_constraint4 | t | 1 | t
- p1_c2 | inh_check_constraint5 | f | 1 | f
- p1_c2 | inh_check_constraint6 | f | 1 | f
- p1_c2 | inh_check_constraint8 | f | 1 | t
- p1_c3 | inh_check_constraint1 | f | 2 | t
- p1_c3 | inh_check_constraint2 | f | 2 | t
- p1_c3 | inh_check_constraint3 | f | 2 | f
- p1_c3 | inh_check_constraint4 | f | 2 | f
- p1_c3 | inh_check_constraint5 | f | 2 | t
- p1_c3 | inh_check_constraint6 | f | 2 | t
- p1_c3 | inh_check_constraint7 | f | 1 | f
- p1_c3 | inh_check_constraint8 | f | 2 | t
-(30 rows)
+ relname | conname | conislocal | coninhcount | conenforced | convalidated
+---------+------------------------+------------+-------------+-------------+--------------
+ p1 | inh_check_constraint1 | t | 0 | t | t
+ p1 | inh_check_constraint10 | t | 0 | f | t
+ p1 | inh_check_constraint2 | t | 0 | t | t
+ p1 | inh_check_constraint3 | t | 0 | f | t
+ p1 | inh_check_constraint4 | t | 0 | f | t
+ p1 | inh_check_constraint5 | t | 0 | f | t
+ p1 | inh_check_constraint6 | t | 0 | f | t
+ 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_constraint2 | t | 1 | t | t
+ p1_c1 | inh_check_constraint3 | t | 1 | f | t
+ p1_c1 | inh_check_constraint4 | t | 1 | f | t
+ p1_c1 | inh_check_constraint5 | t | 1 | t | t
+ p1_c1 | inh_check_constraint6 | t | 1 | t | t
+ p1_c1 | inh_check_constraint7 | t | 0 | f | t
+ p1_c1 | inh_check_constraint8 | f | 1 | t | t
+ p1_c1 | inh_check_constraint9 | t | 1 | t | f
+ p1_c2 | inh_check_constraint1 | f | 1 | t | t
+ p1_c2 | inh_check_constraint10 | f | 1 | f | t
+ p1_c2 | inh_check_constraint2 | f | 1 | t | t
+ p1_c2 | inh_check_constraint3 | f | 1 | f | t
+ p1_c2 | inh_check_constraint4 | t | 1 | t | t
+ p1_c2 | inh_check_constraint5 | f | 1 | f | t
+ p1_c2 | inh_check_constraint6 | f | 1 | f | t
+ p1_c2 | inh_check_constraint8 | f | 1 | t | t
+ p1_c2 | inh_check_constraint9 | f | 1 | f | t
+ p1_c3 | inh_check_constraint1 | f | 2 | t | t
+ p1_c3 | inh_check_constraint10 | f | 2 | t | t
+ p1_c3 | inh_check_constraint2 | f | 2 | t | t
+ p1_c3 | inh_check_constraint3 | f | 2 | f | t
+ p1_c3 | inh_check_constraint4 | f | 2 | f | t
+ p1_c3 | inh_check_constraint5 | f | 2 | t | t
+ p1_c3 | inh_check_constraint6 | f | 2 | t | t
+ p1_c3 | inh_check_constraint7 | f | 1 | f | t
+ p1_c3 | inh_check_constraint8 | f | 2 | t | t
+ p1_c3 | inh_check_constraint9 | f | 2 | t | t
+(38 rows)
drop table p1 cascade;
NOTICE: drop cascades to 3 other objects
diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql
index 84e93ef575e..a9268de14b5 100644
--- a/src/test/regress/sql/alter_table.sql
+++ b/src/test/regress/sql/alter_table.sql
@@ -387,7 +387,8 @@ ALTER TABLE attmp3 validate constraint attmpconstr;
-- Try a non-verified CHECK constraint
ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten CHECK (b > 10); -- fail
ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten CHECK (b > 10) NOT VALID; -- succeeds
-ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten_not_enforced CHECK (b > 10) NOT ENFORCED; -- succeeds
+ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten_not_enforced CHECK (b > 10) NOT ENFORCED; -- fail
+ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten_not_enforced CHECK (b > 10) NOT VALID NOT ENFORCED; -- succeeds
ALTER TABLE attmp3 VALIDATE CONSTRAINT b_greater_than_ten; -- fails
DELETE FROM attmp3 WHERE NOT b > 10;
ALTER TABLE attmp3 VALIDATE CONSTRAINT b_greater_than_ten; -- succeeds
diff --git a/src/test/regress/sql/inherit.sql b/src/test/regress/sql/inherit.sql
index 30fba16231c..34104d9002f 100644
--- a/src/test/regress/sql/inherit.sql
+++ b/src/test/regress/sql/inherit.sql
@@ -481,6 +481,17 @@ alter table p1 add constraint inh_check_constraint5 check (f1 < 10) not enforced
alter table p1 add constraint inh_check_constraint6 check (f1 < 10) not enforced;
alter table p1_c1 add constraint inh_check_constraint6 check (f1 < 10) enforced;
+-- however, cannot merge a not valid enforced child constraint with a valid
+-- not-enforced parent constraint.
+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;
+-- but, allowed if the parent constraint is also invalid.
+alter table p1 add constraint inh_check_constraint9 check (f1 < 10) not valid not enforced;
+
+-- the invalid state of the child constraint will be ignored here.
+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;
+
create table p1_c2(f1 int constraint inh_check_constraint4 check (f1 < 10)) inherits(p1);
-- but reverse is not allowed
@@ -498,7 +509,7 @@ create table p1_c3() inherits(p1, p1_c1);
-- but not allowed if the child constraint is explicitly asked to be NOT ENFORCED
create table p1_fail(f1 int constraint inh_check_constraint6 check (f1 < 10) not enforced) inherits(p1, p1_c1);
-select conrelid::regclass::text as relname, conname, conislocal, coninhcount, conenforced
+select conrelid::regclass::text as relname, conname, conislocal, coninhcount, conenforced, convalidated
from pg_constraint where conname like 'inh\_check\_constraint%'
order by 1, 2;
--
2.43.5
v15-0008-Add-support-for-NOT-ENFORCED-in-foreign-key-cons.patchapplication/octet-stream; name=v15-0008-Add-support-for-NOT-ENFORCED-in-foreign-key-cons.patchDownload
From f71eb9a0cffbb8da42a29308f8799c1ac83c8978 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 20 Jan 2025 21:45:07 +0530
Subject: [PATCH v15 8/9] Add support for NOT ENFORCED in foreign key
constraints.
Typically, when a foreign key (FK) constraint is created on a table,
action and check triggers are added to maintain data integrity. With
this patch, if a constraint is marked as NOT ENFORCED, integrity
checks are no longer required, making these triggers unnecessary.
Consequently, when creating a NOT ENFORCED FK constraint, triggers
will not be created, and the constraint will be marked as NOT VALID.
Similarly, if an existing FK constraint is changed to NOT ENFORCED,
the associated triggers will be dropped, and the constraint will also
be marked as NOT VALID. Conversely, if a NOT ENFORCED FK constraint is
changed to ENFORCED, the necessary triggers will be created, and the
constraint will remain in the NOT VALID state.
----
NOTE: In this patch, the tryAttachPartitionForeignKey() function will
not merge the constraint if the enforcibility differs. This will be
addressed in the next patch.
----
---
doc/src/sgml/advanced.sgml | 13 +
doc/src/sgml/catalogs.sgml | 2 +-
doc/src/sgml/ddl.sgml | 12 +-
doc/src/sgml/ref/alter_table.sgml | 5 +-
doc/src/sgml/ref/create_table.sgml | 2 +-
src/backend/catalog/pg_constraint.c | 5 +-
src/backend/catalog/sql_features.txt | 2 +-
src/backend/commands/tablecmds.c | 482 +++++++++++++++++-----
src/backend/parser/gram.y | 11 +-
src/backend/parser/parse_utilcmd.c | 6 +-
src/backend/utils/cache/relcache.c | 1 +
src/include/nodes/parsenodes.h | 4 +
src/include/utils/rel.h | 3 +
src/test/regress/expected/constraints.out | 11 +-
src/test/regress/expected/foreign_key.out | 158 ++++++-
src/test/regress/sql/constraints.sql | 1 -
src/test/regress/sql/foreign_key.sql | 104 ++++-
17 files changed, 676 insertions(+), 146 deletions(-)
diff --git a/doc/src/sgml/advanced.sgml b/doc/src/sgml/advanced.sgml
index e15a3323dfb..14ed11e1f4d 100644
--- a/doc/src/sgml/advanced.sgml
+++ b/doc/src/sgml/advanced.sgml
@@ -125,6 +125,19 @@ ERROR: insert or update on table "weather" violates foreign key constraint "wea
DETAIL: Key (city)=(Berkeley) is not present in table "cities".
</screen>
</para>
+ <para>
+ If, for some reason, you do not want to enforce this constraint and wish to
+ avoid the error, you can modify the foreign key constraint to <literal>NOT ENFORCED</literal>,
+ or specify it in the <link linkend="sql-createtable"><command>CREATE TABLE</command></link>.
+ </para>
+ <para>
+ Let's change the constraint to <literal>NOT ENFORCED</literal>, and when you
+ attempt to reinsert the same record, it will not result in any error.
+<programlisting>
+ALTER TABLE weather ALTER CONSTRAINT weather_city_fkey NOT ENFORCED;
+INSERT INTO weather VALUES ('Berkeley', 45, 53, 0.0, '1994-11-28');
+</programlisting>
+ </para>
<para>
The behavior of foreign keys can be finely tuned to your
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index ee59a7e15d0..3831c68eeb1 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2599,7 +2599,7 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l
</para>
<para>
Is the constraint enforced?
- Currently, can be false only for CHECK constraints
+ Currently, can be false only for foreign keys and CHECK constraints
</para></entry>
</row>
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index ae156b6b1cd..90f4264f6d7 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -1081,11 +1081,13 @@ CREATE TABLE example (
</indexterm>
<para>
- A foreign key constraint specifies that the values in a column (or
- a group of columns) must match the values appearing in some row
- of another table.
- We say this maintains the <firstterm>referential
- integrity</firstterm> between two related tables.
+ A foreign key constraint with <literal>ENFORCED</literal>, the default
+ setting, specifies that the values in a column (or a group of columns)
+ must match the values appearing in some row of another table. We say this
+ maintains the <firstterm>referential integrity</firstterm> between two
+ related tables. If a foreign key constraint is created with <literal>NOT ENFORCED</literal>,
+ the check for aforementioned <firstterm>referential integrity</firstterm>
+ will not be performed.
</para>
<para>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 8e56b8e59b0..3d7cd0842de 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -58,7 +58,7 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable>
ALTER [ COLUMN ] <replaceable class="parameter">column_name</replaceable> SET COMPRESSION <replaceable class="parameter">compression_method</replaceable>
ADD <replaceable class="parameter">table_constraint</replaceable> [ NOT VALID ]
ADD <replaceable class="parameter">table_constraint_using_index</replaceable>
- ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+ ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]
VALIDATE CONSTRAINT <replaceable class="parameter">constraint_name</replaceable>
DROP CONSTRAINT [ IF EXISTS ] <replaceable class="parameter">constraint_name</replaceable> [ RESTRICT | CASCADE ]
DISABLE TRIGGER [ <replaceable class="parameter">trigger_name</replaceable> | ALL | USER ]
@@ -568,7 +568,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
This form validates a foreign key or check constraint that was
previously created as <literal>NOT VALID</literal>, by scanning the
table to ensure there are no rows for which the constraint is not
- satisfied. Nothing happens if the constraint is already marked valid.
+ satisfied. If the constraint is not enforced, an error is thrown.
+ Nothing happens if the constraint is already marked valid.
(See <xref linkend="sql-altertable-notes"/> below for an explanation
of the usefulness of this command.)
</para>
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index 0a3e520f215..99bffe6b749 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -1405,7 +1405,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</para>
<para>
- This is currently only supported for <literal>CHECK</literal>
+ This is currently only supported for foreign key and <literal>CHECK</literal>
constraints.
</para>
</listitem>
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index 60bc54a96b6..aed9c0c8d9f 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -100,8 +100,9 @@ CreateConstraintEntry(const char *constraintName,
ObjectAddresses *addrs_auto;
ObjectAddresses *addrs_normal;
- /* Only CHECK constraint can be not enforced */
- Assert(isEnforced || constraintType == CONSTRAINT_CHECK);
+ /* Only CHECK or FOREIGN KEY constraint can be not enforced */
+ Assert(isEnforced || constraintType == CONSTRAINT_CHECK ||
+ constraintType == CONSTRAINT_FOREIGN);
conDesc = table_open(ConstraintRelationId, RowExclusiveLock);
diff --git a/src/backend/catalog/sql_features.txt b/src/backend/catalog/sql_features.txt
index 2f250d2c57b..d49471bbdf2 100644
--- a/src/backend/catalog/sql_features.txt
+++ b/src/backend/catalog/sql_features.txt
@@ -281,7 +281,7 @@ F461 Named character sets NO
F471 Scalar subquery values YES
F481 Expanded NULL predicate YES
F491 Constraint management YES
-F492 Optional table constraint enforcement NO check constraints only
+F492 Optional table constraint enforcement NO foreign key and check constraints only
F501 Features and conformance views YES
F501 Features and conformance views 01 SQL_FEATURES view YES
F501 Features and conformance views 02 SQL_SIZING view YES
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 93f9e3e152e..9947f29b2f6 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -389,21 +389,35 @@ static void AlterIndexNamespaces(Relation classRel, Relation rel,
static void AlterSeqNamespaces(Relation classRel, Relation rel,
Oid oldNspOid, Oid newNspOid, ObjectAddresses *objsMoved,
LOCKMODE lockmode);
-static ObjectAddress ATExecAlterConstraint(Relation rel, ATAlterConstraint *cmdcon,
+static ObjectAddress ATExecAlterConstraint(List **wqueue, Relation rel,
+ ATAlterConstraint *cmdcon,
bool recurse, LOCKMODE lockmode);
-static bool ATExecAlterConstraintInternal(ATAlterConstraint *cmdcon, Relation conrel,
- Relation tgrel, const Oid fkrelid,
- const Oid pkrelid, HeapTuple contuple,
- bool recurse, List **otherrelids,
- LOCKMODE lockmode);
-static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
+static bool ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon,
+ Relation conrel, Relation tgrel,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, bool recurse,
+ List **otherrelids, LOCKMODE lockmode,
+ Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger);
+static void ATExecAlterConstrEnforceability(List **wqueue, ATAlterConstraint *cmdcon,
+ Relation conrel, Relation tgrel,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode,
+ Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger);
+static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Oid fkrel,
bool deferrable, bool initdeferred,
List **otherrelids);
-static void ATExecAlterChildConstr(ATAlterConstraint *cmdcon, Relation conrel,
- Relation tgrel, const Oid fkrelid,
- const Oid pkrelid, HeapTuple contuple,
- bool recurse, List **otherrelids,
- LOCKMODE lockmode);
+static void ATExecAlterChildConstr(List **wqueue, ATAlterConstraint *cmdcon,
+ Relation conrel, Relation tgrel,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, bool recurse,
+ List **otherrelids, LOCKMODE lockmode);
static ObjectAddress ATExecValidateConstraint(List **wqueue,
Relation rel, char *constrName,
bool recurse, bool recursing, LOCKMODE lockmode);
@@ -5460,8 +5474,9 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
lockmode);
break;
case AT_AlterConstraint: /* ALTER CONSTRAINT */
- address = ATExecAlterConstraint(rel, castNode(ATAlterConstraint,
- cmd->def),
+ address = ATExecAlterConstraint(wqueue, rel,
+ castNode(ATAlterConstraint,
+ cmd->def),
cmd->recurse, lockmode);
break;
case AT_ValidateConstraint: /* VALIDATE CONSTRAINT */
@@ -10533,7 +10548,7 @@ addFkConstraint(addFkConstraintSides fkside,
CONSTRAINT_FOREIGN,
fkconstraint->deferrable,
fkconstraint->initdeferred,
- true, /* Is Enforced */
+ fkconstraint->is_enforced,
fkconstraint->initially_valid,
parentConstr,
RelationGetRelid(rel),
@@ -10651,21 +10666,23 @@ addFkRecurseReferenced(Constraint *fkconstraint, Relation rel,
Oid parentDelTrigger, Oid parentUpdTrigger,
bool with_period)
{
- Oid deleteTriggerOid,
- updateTriggerOid;
+ Oid deleteTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
Assert(CheckRelationLockedByMe(pkrel, ShareRowExclusiveLock, true));
Assert(CheckRelationLockedByMe(rel, ShareRowExclusiveLock, true));
/*
- * Create the action triggers that enforce the constraint.
+ * Create action triggers to enforce the constraint, or skip them if the
+ * constraint is not enforced.
*/
- createForeignKeyActionTriggers(RelationGetRelid(rel),
- RelationGetRelid(pkrel),
- fkconstraint,
- parentConstr, indexOid,
- parentDelTrigger, parentUpdTrigger,
- &deleteTriggerOid, &updateTriggerOid);
+ if (fkconstraint->is_enforced)
+ createForeignKeyActionTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
+ fkconstraint,
+ parentConstr, indexOid,
+ parentDelTrigger, parentUpdTrigger,
+ &deleteTriggerOid, &updateTriggerOid);
/*
* If the referenced table is partitioned, recurse on ourselves to handle
@@ -10786,8 +10803,8 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
Oid parentInsTrigger, Oid parentUpdTrigger,
bool with_period)
{
- Oid insertTriggerOid,
- updateTriggerOid;
+ Oid insertTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
Assert(OidIsValid(parentConstr));
Assert(CheckRelationLockedByMe(rel, ShareRowExclusiveLock, true));
@@ -10799,29 +10816,32 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
errmsg("foreign key constraints are not supported on foreign tables")));
/*
- * Add the check triggers to it and, if necessary, schedule it to be
- * checked in Phase 3.
+ * Add check triggers if the constraint is enforced, and if needed,
+ * schedule them to be checked in Phase 3.
*
* If the relation is partitioned, drill down to do it to its partitions.
*/
- createForeignKeyCheckTriggers(RelationGetRelid(rel),
- RelationGetRelid(pkrel),
- fkconstraint,
- parentConstr,
- indexOid,
- parentInsTrigger, parentUpdTrigger,
- &insertTriggerOid, &updateTriggerOid);
+ if (fkconstraint->is_enforced)
+ createForeignKeyCheckTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
+ fkconstraint,
+ parentConstr,
+ indexOid,
+ parentInsTrigger, parentUpdTrigger,
+ &insertTriggerOid, &updateTriggerOid);
if (rel->rd_rel->relkind == RELKIND_RELATION)
{
/*
* Tell Phase 3 to check that the constraint is satisfied by existing
- * rows. We can skip this during table creation, when requested
- * explicitly by specifying NOT VALID in an ADD FOREIGN KEY command,
- * and when we're recreating a constraint following a SET DATA TYPE
- * operation that did not impugn its validity.
+ * rows. We can skip this during table creation, when constraint is
+ * specified as NOT ENFORCED, or when requested explicitly by
+ * specifying NOT VALID in an ADD FOREIGN KEY command, and when we're
+ * recreating a constraint following a SET DATA TYPE operation that
+ * did not impugn its validity.
*/
- if (wqueue && !old_check_ok && !fkconstraint->skip_validation)
+ if (wqueue && !old_check_ok && !fkconstraint->skip_validation &&
+ fkconstraint->is_enforced)
{
NewConstraint *newcon;
AlteredTableInfo *tab;
@@ -11052,8 +11072,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
AttrNumber confdelsetcols[INDEX_MAX_KEYS];
Constraint *fkconstraint;
ObjectAddress address;
- Oid deleteTriggerOid,
- updateTriggerOid;
+ Oid deleteTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(constrOid));
if (!HeapTupleIsValid(tuple))
@@ -11113,6 +11133,7 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
fkconstraint->fk_del_set_cols = NIL;
fkconstraint->old_conpfeqop = NIL;
fkconstraint->old_pktable_oid = InvalidOid;
+ fkconstraint->is_enforced = constrForm->conenforced;
fkconstraint->skip_validation = false;
fkconstraint->initially_valid = true;
@@ -11142,9 +11163,10 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
* parent OIDs for similar triggers that will be created on the
* partition in addFkRecurseReferenced().
*/
- GetForeignKeyActionTriggers(trigrel, constrOid,
- constrForm->confrelid, constrForm->conrelid,
- &deleteTriggerOid, &updateTriggerOid);
+ if (constrForm->conenforced)
+ GetForeignKeyActionTriggers(trigrel, constrOid,
+ constrForm->confrelid, constrForm->conrelid,
+ &deleteTriggerOid, &updateTriggerOid);
/* Add this constraint ... */
address = addFkConstraint(addFkReferencedSide,
@@ -11277,8 +11299,8 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
Oid indexOid;
ObjectAddress address;
ListCell *lc;
- Oid insertTriggerOid,
- updateTriggerOid;
+ Oid insertTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
bool with_period;
tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(parentConstrOid));
@@ -11310,17 +11332,18 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
mapped_conkey[i] = attmap->attnums[conkey[i] - 1];
/*
- * Get the "check" triggers belonging to the constraint to pass as
- * parent OIDs for similar triggers that will be created on the
- * partition in addFkRecurseReferencing(). They are also passed to
- * tryAttachPartitionForeignKey() below to simply assign as parents to
- * the partition's existing "check" triggers, that is, if the
- * corresponding constraints is deemed attachable to the parent
- * constraint.
+ * Get the "check" triggers belonging to the constraint, if it is
+ * enforced, to pass as parent OIDs for similar triggers that will be
+ * created on the partition in addFkRecurseReferencing(). They are
+ * also passed to tryAttachPartitionForeignKey() below to simply
+ * assign as parents to the partition's existing "check" triggers,
+ * that is, if the corresponding constraints is deemed attachable to
+ * the parent constraint.
*/
- GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
- constrForm->confrelid, constrForm->conrelid,
- &insertTriggerOid, &updateTriggerOid);
+ if (constrForm->conenforced)
+ GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
+ constrForm->confrelid, constrForm->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
/*
* Before creating a new constraint, see whether any existing FKs are
@@ -11373,6 +11396,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
fkconstraint->fk_del_set_cols = NIL;
fkconstraint->old_conpfeqop = NIL;
fkconstraint->old_pktable_oid = InvalidOid;
+ fkconstraint->is_enforced = constrForm->conenforced;
fkconstraint->skip_validation = false;
fkconstraint->initially_valid = constrForm->convalidated;
for (int i = 0; i < numfks; i++)
@@ -11490,6 +11514,7 @@ tryAttachPartitionForeignKey(List **wqueue,
if (OidIsValid(partConstr->conparentid) ||
partConstr->condeferrable != parentConstr->condeferrable ||
partConstr->condeferred != parentConstr->condeferred ||
+ partConstr->conenforced != parentConstr->conenforced ||
partConstr->confupdtype != parentConstr->confupdtype ||
partConstr->confdeltype != parentConstr->confdeltype ||
partConstr->confmatchtype != parentConstr->confmatchtype)
@@ -11533,8 +11558,7 @@ AttachPartitionForeignKey(List **wqueue,
bool queueValidation;
Oid partConstrFrelid;
Oid partConstrRelid;
- Oid insertTriggerOid,
- updateTriggerOid;
+ bool parentConstrIsEnforced;
/* Fetch the parent constraint tuple */
parentConstrTup = SearchSysCache1(CONSTROID,
@@ -11542,6 +11566,7 @@ AttachPartitionForeignKey(List **wqueue,
if (!HeapTupleIsValid(parentConstrTup))
elog(ERROR, "cache lookup failed for constraint %u", parentConstrOid);
parentConstr = (Form_pg_constraint) GETSTRUCT(parentConstrTup);
+ parentConstrIsEnforced = parentConstr->conenforced;
/* Fetch the child constraint tuple */
partcontup = SearchSysCache1(CONSTROID,
@@ -11591,17 +11616,24 @@ AttachPartitionForeignKey(List **wqueue,
/*
* Like the constraint, attach partition's "check" triggers to the
- * corresponding parent triggers.
+ * corresponding parent triggers if the constraint is enforced.
+ * Non-enforced constraints do not have these triggers.
*/
- GetForeignKeyCheckTriggers(trigrel,
- partConstrOid, partConstrFrelid, partConstrRelid,
- &insertTriggerOid, &updateTriggerOid);
- Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
- TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
- RelationGetRelid(partition));
- Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
- TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
- RelationGetRelid(partition));
+ if (parentConstrIsEnforced)
+ {
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
+ GetForeignKeyCheckTriggers(trigrel,
+ partConstrOid, partConstrFrelid, partConstrRelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
+ RelationGetRelid(partition));
+ Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
+ RelationGetRelid(partition));
+ }
/*
* We updated this pg_constraint row above to set its parent; validating
@@ -11715,6 +11747,10 @@ RemoveInheritedConstraint(Relation conrel, Relation trigrel, Oid conoid,
*
* The subroutine for tryAttachPartitionForeignKey handles the deletion of
* action triggers for the foreign key constraint.
+ *
+ * If valid confrelid and conrelid values are not provided, the respective
+ * trigger check will be skipped, and the trigger will be considered for
+ * removal.
*/
static void
DropForeignKeyConstraintTriggers(Relation trigrel, Oid conoid, Oid confrelid,
@@ -11735,10 +11771,27 @@ DropForeignKeyConstraintTriggers(Relation trigrel, Oid conoid, Oid confrelid,
Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
ObjectAddress trigger;
- if (trgform->tgconstrrelid != conrelid)
+ /* Invalid if trigger is not for a referential integrity constraint */
+ if (!OidIsValid(trgform->tgconstrrelid))
continue;
- if (trgform->tgrelid != confrelid)
+ if (OidIsValid(conrelid) && trgform->tgconstrrelid != conrelid)
continue;
+ if (OidIsValid(confrelid) && trgform->tgrelid != confrelid)
+ continue;
+
+ /* We should be droping trigger related to foreign ke constraint */
+ Assert(trgform->tgfoid == F_RI_FKEY_CHECK_INS ||
+ trgform->tgfoid == F_RI_FKEY_CHECK_UPD ||
+ trgform->tgfoid == F_RI_FKEY_CASCADE_DEL ||
+ trgform->tgfoid == F_RI_FKEY_CASCADE_UPD ||
+ trgform->tgfoid == F_RI_FKEY_RESTRICT_DEL ||
+ trgform->tgfoid == F_RI_FKEY_RESTRICT_UPD ||
+ trgform->tgfoid == F_RI_FKEY_SETNULL_DEL ||
+ trgform->tgfoid == F_RI_FKEY_SETNULL_UPD ||
+ trgform->tgfoid == F_RI_FKEY_SETDEFAULT_DEL ||
+ trgform->tgfoid == F_RI_FKEY_SETDEFAULT_UPD ||
+ trgform->tgfoid == F_RI_FKEY_NOACTION_DEL ||
+ trgform->tgfoid == F_RI_FKEY_NOACTION_UPD);
/*
* The constraint is originally set up to contain this trigger as an
@@ -11895,8 +11948,8 @@ GetForeignKeyCheckTriggers(Relation trigrel,
* InvalidObjectAddress.
*/
static ObjectAddress
-ATExecAlterConstraint(Relation rel, ATAlterConstraint *cmdcon, bool recurse,
- LOCKMODE lockmode)
+ATExecAlterConstraint(List **wqueue, Relation rel, ATAlterConstraint *cmdcon,
+ bool recurse, LOCKMODE lockmode)
{
Relation conrel;
Relation tgrel;
@@ -11948,10 +12001,19 @@ ATExecAlterConstraint(Relation rel, ATAlterConstraint *cmdcon, bool recurse,
currcon = (Form_pg_constraint) GETSTRUCT(contuple);
if (currcon->contype != CONSTRAINT_FOREIGN)
- ereport(ERROR,
- (errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("constraint \"%s\" of relation \"%s\" is not a foreign key constraint",
- cmdcon->conname, RelationGetRelationName(rel))));
+ {
+ if (cmdcon->alterEnforceability)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot alter enforceability of constraint \"%s\" of relation \"%s\"",
+ cmdcon->conname, RelationGetRelationName(rel)),
+ errdetail("Enforceability can only be altered for foreign key constraints.")));
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("constraint \"%s\" of relation \"%s\" is not a foreign key constraint",
+ cmdcon->conname, RelationGetRelationName(rel))));
+ }
/*
* If it's not the topmost constraint, raise an error.
@@ -12002,9 +12064,11 @@ ATExecAlterConstraint(Relation rel, ATAlterConstraint *cmdcon, bool recurse,
/*
* Do the actual catalog work, and recurse if necessary.
*/
- if (ATExecAlterConstraintInternal(cmdcon, conrel, tgrel, currcon->conrelid,
- currcon->confrelid, contuple, recurse,
- &otherrelids, lockmode))
+ if (ATExecAlterConstraintInternal(wqueue, cmdcon, conrel, tgrel,
+ currcon->conrelid, currcon->confrelid,
+ contuple, recurse, &otherrelids, lockmode,
+ InvalidOid, InvalidOid, InvalidOid,
+ InvalidOid))
ObjectAddressSet(address, ConstraintRelationId, currcon->oid);
/*
@@ -12035,10 +12099,15 @@ ATExecAlterConstraint(Relation rel, ATAlterConstraint *cmdcon, bool recurse,
* but existing releases don't do that.)
*/
static bool
-ATExecAlterConstraintInternal(ATAlterConstraint *cmdcon, Relation conrel,
- Relation tgrel, const Oid fkrelid,
- const Oid pkrelid, HeapTuple contuple,
- bool recurse, List **otherrelids, LOCKMODE lockmode)
+ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon,
+ Relation conrel, Relation tgrel,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, bool recurse,
+ List **otherrelids, LOCKMODE lockmode,
+ Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger)
{
Form_pg_constraint currcon;
Oid refrelid = InvalidOid;
@@ -12060,17 +12129,27 @@ ATExecAlterConstraintInternal(ATAlterConstraint *cmdcon, Relation conrel,
* If called to modify a constraint that's already in the desired state,
* silently do nothing.
*/
- if (cmdcon->alterDeferrability &&
- (currcon->condeferrable != cmdcon->deferrable ||
- currcon->condeferred != cmdcon->initdeferred))
+ if (currcon->conenforced != cmdcon->is_enforced ||
+ currcon->condeferrable != cmdcon->deferrable ||
+ currcon->condeferred != cmdcon->initdeferred)
{
HeapTuple copyTuple;
Form_pg_constraint copy_con;
+ Assert(cmdcon->alterEnforceability || cmdcon->alterDeferrability);
+
copyTuple = heap_copytuple(contuple);
copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
- copy_con->condeferrable = cmdcon->deferrable;
- copy_con->condeferred = cmdcon->initdeferred;
+
+ if (cmdcon->alterEnforceability)
+ copy_con->conenforced = cmdcon->is_enforced;
+
+ if (cmdcon->alterDeferrability)
+ {
+ copy_con->condeferrable = cmdcon->deferrable;
+ copy_con->condeferred = cmdcon->initdeferred;
+ }
+
CatalogTupleUpdate(conrel, ©Tuple->t_self, copyTuple);
InvokeObjectPostAlterHook(ConstraintRelationId, currcon->oid, 0);
@@ -12082,12 +12161,68 @@ ATExecAlterConstraintInternal(ATAlterConstraint *cmdcon, Relation conrel,
CacheInvalidateRelcache(rel);
/*
- * Now we need to update the multiple entries in pg_trigger that
- * implement the constraint.
+ * Note that even if deferrability is requested to be altered along with
+ * enforceability, we don't need to explicitly update multiple entries in
+ * pg_trigger related to deferrability.
+ *
+ * Modifying enforceability involves either creating or dropping the trigger,
+ * during which the deferrability setting will be adjusted automatically.
*/
- AlterConstrTriggerDeferrability(currcon->oid, tgrel, rel,
- cmdcon->deferrable,
- cmdcon->initdeferred, otherrelids);
+ if (cmdcon->alterEnforceability)
+ {
+ ATExecAlterConstrEnforceability(wqueue, cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, contuple, otherrelids, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
+ /*
+ * We don't need further recursion, as altering enforceability is
+ * already handled with the necessary adjustments.
+ */
+ recurse = false;
+
+ /*
+ * Tell Phase 3 to check that the constraint is satisfied by
+ * existing rows, but skip this step if the constraint is NOT
+ * VALID.
+ */
+ if (cmdcon->is_enforced && currcon->convalidated &&
+ rel->rd_rel->relkind == RELKIND_RELATION)
+ {
+ AlteredTableInfo *tab;
+ NewConstraint *newcon;
+ Constraint *fkconstraint;
+
+ /* Queue validation for phase 3 */
+ fkconstraint = makeNode(Constraint);
+ /* for now this is all we need */
+ fkconstraint->conname = pstrdup(NameStr(currcon->conname));
+
+ newcon = (NewConstraint *) palloc0(sizeof(NewConstraint));
+ newcon->name = fkconstraint->conname;
+ newcon->contype = CONSTR_FOREIGN;
+ newcon->refrelid = currcon->confrelid;
+ newcon->refindid = currcon->conindid;
+ newcon->conid = currcon->oid;
+ newcon->qual = (Node *) fkconstraint;
+
+ /* Find or create work queue entry for this table */
+ tab = ATGetQueueEntry(wqueue, rel);
+ tab->constraints = lappend(tab->constraints, newcon);
+ }
+ }
+ else
+ {
+ /*
+ * Update the multiple entries in pg_trigger that implement the
+ * constraint.
+ */
+ AlterConstrTriggerDeferrability(currcon->oid, tgrel,
+ RelationGetRelid(rel),
+ cmdcon->deferrable,
+ cmdcon->initdeferred, otherrelids);
+ }
}
/*
@@ -12098,7 +12233,7 @@ ATExecAlterConstraintInternal(ATAlterConstraint *cmdcon, Relation conrel,
(rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
(OidIsValid(refrelid) &&
get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)))
- ATExecAlterChildConstr(cmdcon, conrel, tgrel, fkrelid, pkrelid,
+ ATExecAlterChildConstr(wqueue, cmdcon, conrel, tgrel, fkrelid, pkrelid,
contuple, recurse, otherrelids, lockmode);
table_close(rel, NoLock);
@@ -12106,6 +12241,120 @@ ATExecAlterConstraintInternal(ATAlterConstraint *cmdcon, Relation conrel,
return changed;
}
+/*
+ * A subroutine of ATExecAlterConstrRecurse that updates the enforceability of
+ * a foreign key constraint. Depending on whether the constraint is being set
+ * to enforced or not enforced, it creates or drops the trigger accordingly.
+ *
+ * The arguments to this function have the same meaning as the arguments to
+ * ATExecAlterConstrRecurse.
+ */
+static void
+ATExecAlterConstrEnforceability(List **wqueue, ATAlterConstraint *cmdcon,
+ Relation conrel, Relation tgrel,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode,
+ Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger)
+{
+ Form_pg_constraint currcon;
+ Oid conoid;
+
+ currcon = (Form_pg_constraint) GETSTRUCT(contuple);
+ conoid = currcon->oid;
+
+ /* Drop triggers */
+ if (!cmdcon->is_enforced)
+ {
+ /*
+ * When setting a constraint to NOT ENFORCED, the constraint triggers
+ * need to be dropped. Therefore, we must process the child relations
+ * first, followed by the parent, to account for dependencies.
+ */
+ if (get_rel_relkind(currcon->conrelid) == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE)
+ ATExecAlterChildConstr(wqueue, cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, contuple, false, otherrelids,
+ lockmode);
+
+ /* Drop all the triggers */
+ DropForeignKeyConstraintTriggers(tgrel, conoid, InvalidOid, InvalidOid);
+ }
+ else /* Create triggers */
+ {
+ Oid ReferencedDelTriggerOid = InvalidOid,
+ ReferencedUpdTriggerOid = InvalidOid,
+ ReferencingInsTriggerOid = InvalidOid,
+ ReferencingUpdTriggerOid = InvalidOid;
+
+ /* Prepare the minimal information required for trigger creation. */
+ Constraint *fkconstraint = makeNode(Constraint);
+
+ fkconstraint->fk_matchtype = currcon->confmatchtype;
+ fkconstraint->fk_upd_action = currcon->confupdtype;
+ fkconstraint->fk_del_action = currcon->confdeltype;
+
+ /* Create referenced triggers */
+ if (currcon->conrelid == fkrelid)
+ createForeignKeyActionTriggers(currcon->conrelid,
+ currcon->confrelid,
+ fkconstraint,
+ conoid,
+ currcon->conindid,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ &ReferencedDelTriggerOid,
+ &ReferencedUpdTriggerOid);
+
+ /* Create referencing triggers */
+ if (currcon->confrelid == pkrelid)
+ createForeignKeyCheckTriggers(currcon->conrelid,
+ pkrelid,
+ fkconstraint,
+ conoid,
+ currcon->conindid,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger,
+ &ReferencingInsTriggerOid,
+ &ReferencingUpdTriggerOid);
+
+ /*
+ * If the table at either end of the constraint is partitioned, we
+ * need to recurse and create triggers for each constraint that is a
+ * child of this one.
+ */
+ if (get_rel_relkind(currcon->conrelid) == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE)
+ {
+ ScanKeyData pkey;
+ SysScanDesc pscan;
+ HeapTuple childtup;
+
+ ScanKeyInit(&pkey,
+ Anum_pg_constraint_conparentid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+
+ pscan = systable_beginscan(conrel, ConstraintParentIndexId,
+ true, NULL, 1, &pkey);
+
+ while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
+ ATExecAlterConstraintInternal(wqueue, cmdcon, conrel, tgrel,
+ fkrelid, pkrelid, childtup, false,
+ otherrelids, lockmode,
+ ReferencedDelTriggerOid,
+ ReferencedUpdTriggerOid,
+ ReferencingInsTriggerOid,
+ ReferencingUpdTriggerOid);
+
+ systable_endscan(pscan);
+ }
+ }
+}
+
/*
* A subroutine of ATExecAlterConstrDeferrability that updated constraint
* trigger's deferrability.
@@ -12114,7 +12363,7 @@ ATExecAlterConstraintInternal(ATAlterConstraint *cmdcon, Relation conrel,
* ATExecAlterConstrDeferrability.
*/
static void
-AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
+AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Oid fkrel,
bool deferrable, bool initdeferred,
List **otherrelids)
{
@@ -12140,7 +12389,7 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
* other rels that don't have a trigger whose properties change, but
* let's be conservative.)
*/
- if (tgform->tgrelid != RelationGetRelid(rel))
+ if (tgform->tgrelid != fkrel)
*otherrelids = list_append_unique_oid(*otherrelids,
tgform->tgrelid);
@@ -12183,7 +12432,7 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
* ATExecAlterConstraintInternal.
*/
static void
-ATExecAlterChildConstr(ATAlterConstraint *cmdcon, Relation conrel,
+ATExecAlterChildConstr(List **wqueue, ATAlterConstraint *cmdcon, Relation conrel,
Relation tgrel, const Oid fkrelid, const Oid pkrelid,
HeapTuple contuple, bool recurse, List **otherrelids,
LOCKMODE lockmode)
@@ -12206,8 +12455,10 @@ ATExecAlterChildConstr(ATAlterConstraint *cmdcon, Relation conrel,
true, NULL, 1, &pkey);
while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
- ATExecAlterConstraintInternal(cmdcon, conrel, tgrel, fkrelid, pkrelid,
- childtup, recurse, otherrelids, lockmode);
+ ATExecAlterConstraintInternal(wqueue, cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, childtup, recurse, otherrelids,
+ lockmode, InvalidOid, InvalidOid,
+ InvalidOid, InvalidOid);
systable_endscan(pscan);
}
@@ -20277,8 +20528,6 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
Form_pg_constraint conform;
- Oid insertTriggerOid,
- updateTriggerOid;
contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
if (!HeapTupleIsValid(contup))
@@ -20305,17 +20554,25 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
/*
* Also, look up the partition's "check" triggers corresponding to the
- * constraint being detached and detach them from the parent triggers.
+ * enforced constraint being detached and detach them from the parent
+ * triggers. Non-enforced constraints do not have these triggers;
+ * therefore, this step is not needed.
*/
- GetForeignKeyCheckTriggers(trigrel,
- fk->conoid, fk->confrelid, fk->conrelid,
- &insertTriggerOid, &updateTriggerOid);
- Assert(OidIsValid(insertTriggerOid));
- TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
- RelationGetRelid(partRel));
- Assert(OidIsValid(updateTriggerOid));
- TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
- RelationGetRelid(partRel));
+ if (fk->conenforced)
+ {
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ Assert(OidIsValid(updateTriggerOid));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ }
/*
* Lastly, create the action triggers on the referenced table, using
@@ -20355,6 +20612,7 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
fkconstraint->conname = pstrdup(NameStr(conform->conname));
fkconstraint->deferrable = conform->condeferrable;
fkconstraint->initdeferred = conform->condeferred;
+ fkconstraint->is_enforced = conform->conenforced;
fkconstraint->skip_validation = true;
fkconstraint->initially_valid = true;
/* a few irrelevant fields omitted here */
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index b790a0f8e22..ea7c32f6dcd 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -2666,7 +2666,12 @@ alter_table_cmd:
processCASbits($4, @4, "ALTER CONSTRAINT statement",
&c->deferrable,
&c->initdeferred,
- NULL, NULL, NULL, yyscanner);
+ &c->is_enforced, NULL, NULL, yyscanner);
+ c->alterDeferrability =
+ ($4 & (CAS_NOT_DEFERRABLE | CAS_DEFERRABLE |
+ CAS_INITIALLY_IMMEDIATE | CAS_INITIALLY_IMMEDIATE)) != 0;
+ c->alterEnforceability =
+ ($4 & (CAS_NOT_ENFORCED | CAS_ENFORCED)) != 0;
$$ = (Node *) n;
}
/* ALTER TABLE <name> VALIDATE CONSTRAINT ... */
@@ -4316,8 +4321,8 @@ ConstraintElem:
n->fk_del_set_cols = ($11)->deleteAction->cols;
processCASbits($12, @12, "FOREIGN KEY",
&n->deferrable, &n->initdeferred,
- NULL, &n->skip_validation, NULL,
- yyscanner);
+ &n->is_enforced, &n->skip_validation,
+ NULL, yyscanner);
n->initially_valid = !n->skip_validation;
$$ = (Node *) n;
}
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 37becb62356..c579e56d967 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -3979,7 +3979,8 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
case CONSTR_ATTR_ENFORCED:
if (lastprimarycon == NULL ||
- lastprimarycon->contype != CONSTR_CHECK)
+ (lastprimarycon->contype != CONSTR_CHECK &&
+ lastprimarycon->contype != CONSTR_FOREIGN))
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("misplaced ENFORCED clause"),
@@ -3995,7 +3996,8 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
case CONSTR_ATTR_NOT_ENFORCED:
if (lastprimarycon == NULL ||
- lastprimarycon->contype != CONSTR_CHECK)
+ (lastprimarycon->contype != CONSTR_CHECK &&
+ lastprimarycon->contype != CONSTR_FOREIGN))
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("misplaced NOT ENFORCED clause"),
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index c010de4aeca..dd956b0bf4a 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4692,6 +4692,7 @@ RelationGetFKeyList(Relation relation)
info->conoid = constraint->oid;
info->conrelid = constraint->conrelid;
info->confrelid = constraint->confrelid;
+ info->conenforced = constraint->conenforced;
DeconstructFkConstraintRow(htup, &info->nkeys,
info->conkey,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 0b208f51bdd..b151286f238 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2490,9 +2490,13 @@ typedef struct ATAlterConstraint
{
NodeTag type;
char *conname; /* Constraint name */
+
bool alterDeferrability; /* changing deferrability properties? */
bool deferrable; /* DEFERRABLE? */
bool initdeferred; /* INITIALLY DEFERRED? */
+
+ bool alterEnforceability; /* changing enforceability properties? */
+ bool is_enforced; /* ENFORCED? */
} ATAlterConstraint;
/* Ad-hoc node for AT_ReplicaIdentity */
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index db3e504c3d2..dba0fb70146 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -284,6 +284,9 @@ typedef struct ForeignKeyCacheInfo
/* number of columns in the foreign key */
int nkeys;
+ /* Is enforced ? */
+ bool conenforced;
+
/*
* these arrays each have nkeys valid entries:
*/
diff --git a/src/test/regress/expected/constraints.out b/src/test/regress/expected/constraints.out
index 692a69fe457..75d40d06b6e 100644
--- a/src/test/regress/expected/constraints.out
+++ b/src/test/regress/expected/constraints.out
@@ -744,15 +744,12 @@ CREATE TABLE UNIQUE_NOTEN_TBL(i int UNIQUE NOT ENFORCED);
ERROR: misplaced NOT ENFORCED clause
LINE 1: CREATE TABLE UNIQUE_NOTEN_TBL(i int UNIQUE NOT ENFORCED);
^
--- XXX: error message is misleading here
ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
-ERROR: ALTER CONSTRAINT statement constraints cannot be marked ENFORCED
-LINE 1: ...TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
- ^
+ERROR: cannot alter enforceability of constraint "unique_tbl_i_key" of relation "unique_tbl"
+DETAIL: Enforceability can only be altered for foreign key constraints.
ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORCED;
-ERROR: ALTER CONSTRAINT statement constraints cannot be marked NOT ENFORCED
-LINE 1: ...ABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORC...
- ^
+ERROR: cannot alter enforceability of constraint "unique_tbl_i_key" of relation "unique_tbl"
+DETAIL: Enforceability can only be altered for foreign key constraints.
DROP TABLE unique_tbl;
--
-- EXCLUDE constraints
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 374dcb266e7..3183b1cbe8f 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -1,12 +1,43 @@
--
-- FOREIGN KEY
--
--- MATCH FULL
+-- NOT ENFORCED
--
-- First test, check and cascade
--
CREATE TABLE PKTABLE ( ptest1 int PRIMARY KEY, ptest2 text );
-CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE, ftest2 int );
+CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE NOT ENFORCED,
+ ftest2 int );
+-- Inserting into the foreign key table will not result in an error, even if
+-- there is no matching key in the referenced table.
+INSERT INTO FKTABLE VALUES (1, 2);
+INSERT INTO FKTABLE VALUES (2, 3);
+-- Check FKTABLE
+SELECT * FROM FKTABLE;
+ ftest1 | ftest2
+--------+--------
+ 1 | 2
+ 2 | 3
+(2 rows)
+
+-- Reverting it back to ENFORCED will result in failure because constraint validation will be triggered,
+-- as it was previously in a valid state.
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_fkey"
+DETAIL: Key (ftest1)=(1) is not present in table "pktable".
+-- Remove any existing rows that violate the constraint, then attempt to change
+-- it.
+TRUNCATE FKTABLE;
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+-- Any further inserts will fail due to the enforcement.
+INSERT INTO FKTABLE VALUES (3, 4);
+ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_fkey"
+DETAIL: Key (ftest1)=(3) is not present in table "pktable".
+--
+-- MATCH FULL
+--
+-- First test, check and cascade
+--
-- Insert test data into PKTABLE
INSERT INTO PKTABLE VALUES (1, 'Test1');
INSERT INTO PKTABLE VALUES (2, 'Test2');
@@ -351,6 +382,41 @@ INSERT INTO FKTABLE VALUES (1, NULL);
ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL;
ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_ftest2_fkey"
DETAIL: MATCH FULL does not allow mixing of null and nonnull key values.
+-- Modifying other attributes of a constraint should not affect its enforceability, and vice versa
+ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE NOT VALID NOT ENFORCED;
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_ftest2_fkey DEFERRABLE INITIALLY DEFERRED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fktable_ftest1_ftest2_fkey';
+ condeferrable | condeferred | conenforced | convalidated
+---------------+-------------+-------------+--------------
+ t | t | f | f
+(1 row)
+
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_ftest2_fkey NOT ENFORCED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fktable_ftest1_ftest2_fkey';
+ condeferrable | condeferred | conenforced | convalidated
+---------------+-------------+-------------+--------------
+ t | t | f | f
+(1 row)
+
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_ftest2_fkey ENFORCED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fktable_ftest1_ftest2_fkey';
+ condeferrable | condeferred | conenforced | convalidated
+---------------+-------------+-------------+--------------
+ t | t | t | f
+(1 row)
+
+-- Can change enforceability and deferrability together
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_ftest2_fkey NOT ENFORCED NOT DEFERRABLE;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fktable_ftest1_ftest2_fkey';
+ condeferrable | condeferred | conenforced | convalidated
+---------------+-------------+-------------+--------------
+ f | f | f | f
+(1 row)
+
DROP TABLE FKTABLE;
DROP TABLE PKTABLE;
-- MATCH SIMPLE
@@ -1276,6 +1342,13 @@ INSERT INTO fktable VALUES (0, 20);
ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_fk_fkey"
DETAIL: Key (fk)=(20) is not present in table "pktable".
COMMIT;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT ENFORCED;
+BEGIN;
+-- doesn't match FK, no error.
+UPDATE pktable SET id = 10 WHERE id = 5;
+-- doesn't match PK, no error.
+INSERT INTO fktable VALUES (0, 20);
+ROLLBACK;
-- try additional syntax
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE;
-- illegal option
@@ -1283,6 +1356,14 @@ ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE INITIALLY DE
ERROR: constraint declared INITIALLY DEFERRED must be DEFERRABLE
LINE 1: ...e ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE INITIALLY ...
^
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORCED;
+ERROR: conflicting constraint properties
+LINE 1: ...fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORC...
+ ^
+CREATE TEMP TABLE fktable2 (fk int references pktable ENFORCED NOT ENFORCED);
+ERROR: multiple ENFORCED/NOT ENFORCED clauses not allowed
+LINE 1: ...ABLE fktable2 (fk int references pktable ENFORCED NOT ENFORC...
+ ^
-- test order of firing of FK triggers when several RI-induced changes need to
-- be made to the same row. This was broken by subtransaction-related
-- changes in 8.0.
@@ -1580,10 +1661,12 @@ ALTER TABLE fk_partitioned_fk DROP COLUMN fdrop1;
CREATE TABLE fk_partitioned_fk_1 (fdrop1 int, fdrop2 int, a int, fdrop3 int, b int);
ALTER TABLE fk_partitioned_fk_1 DROP COLUMN fdrop1, DROP COLUMN fdrop2, DROP COLUMN fdrop3;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FROM (0,0) TO (1000,1000);
-ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk;
+ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
+ALTER TABLE fk_partitioned_fk_2 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
CREATE TABLE fk_partitioned_fk_3 (fdrop1 int, fdrop2 int, fdrop3 int, fdrop4 int, b int, a int)
PARTITION BY HASH (a);
ALTER TABLE fk_partitioned_fk_3 DROP COLUMN fdrop1, DROP COLUMN fdrop2,
@@ -1606,10 +1689,10 @@ INSERT INTO fk_partitioned_fk_1 (a,b) VALUES (500, 501);
ERROR: insert or update on table "fk_partitioned_fk_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
DETAIL: Key (a, b)=(500, 501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (1500, 1501);
-ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_2_a_b_fkey"
DETAIL: Key (a, b)=(1500, 1501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk_2 (a,b) VALUES (1500, 1501);
-ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_2_a_b_fkey"
DETAIL: Key (a, b)=(1500, 1501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (2500, 2502);
ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
@@ -1659,6 +1742,37 @@ Indexes:
Referenced by:
TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b)
+-- Check the exsting FK trigger
+CREATE TEMP TABLE tmp_trg_info AS SELECT tgtype, tgrelid::regclass, tgconstraint
+FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+SELECT count(1) FROM tmp_trg_info;
+ count
+-------
+ 14
+(1 row)
+
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+-- No triggers
+SELECT count(1) FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+ count
+-------
+ 0
+(1 row)
+
+-- Changing it back to ENFORCED will recreate the necessary triggers.
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+-- Should be exactly the same number of triggers found as before
+SELECT COUNT(1) FROM pg_trigger pgt JOIN tmp_trg_info tt
+ON (pgt.tgtype = tt.tgtype AND pgt.tgrelid = tt.tgrelid AND pgt.tgconstraint = tt.tgconstraint);
+ count
+-------
+ 14
+(1 row)
+
ALTER TABLE fk_partitioned_fk DROP CONSTRAINT fk_partitioned_fk_a_b_fkey;
-- done.
DROP TABLE fk_notpartitioned_pk, fk_partitioned_fk;
@@ -1956,6 +2070,40 @@ Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
Foreign-key constraints:
TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
+DROP TABLE fk_partitioned_fk_2;
+CREATE TABLE fk_partitioned_fk_2 (b int, a int,
+ FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
+BEGIN;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should be having two constraints
+\d fk_partitioned_fk_2
+ Table "public.fk_partitioned_fk_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ b | integer | | |
+ a | integer | | |
+Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
+Foreign-key constraints:
+ "fk_partitioned_fk_2_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED
+ TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
+
+DROP TABLE fk_partitioned_fk_2;
+ROLLBACK;
+BEGIN;
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have only one constraint
+\d fk_partitioned_fk_2
+ Table "public.fk_partitioned_fk_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ b | integer | | |
+ a | integer | | |
+Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
+Foreign-key constraints:
+ TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED
+
+ROLLBACK;
DROP TABLE fk_partitioned_fk_2;
CREATE TABLE fk_partitioned_fk_4 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE) PARTITION BY RANGE (b, a);
CREATE TABLE fk_partitioned_fk_4_1 PARTITION OF fk_partitioned_fk_4 FOR VALUES FROM (1,1) TO (100,100);
diff --git a/src/test/regress/sql/constraints.sql b/src/test/regress/sql/constraints.sql
index d6742f83fb9..e61e7324768 100644
--- a/src/test/regress/sql/constraints.sql
+++ b/src/test/regress/sql/constraints.sql
@@ -534,7 +534,6 @@ SELECT * FROM unique_tbl;
-- enforcibility cannot be specified or set for unique constrain
CREATE TABLE UNIQUE_EN_TBL(i int UNIQUE ENFORCED);
CREATE TABLE UNIQUE_NOTEN_TBL(i int UNIQUE NOT ENFORCED);
--- XXX: error message is misleading here
ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORCED;
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index bc0adb8cfe9..408a01e377b 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -2,13 +2,39 @@
-- FOREIGN KEY
--
--- MATCH FULL
+-- NOT ENFORCED
--
-- First test, check and cascade
--
CREATE TABLE PKTABLE ( ptest1 int PRIMARY KEY, ptest2 text );
-CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE, ftest2 int );
+CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE NOT ENFORCED,
+ ftest2 int );
+-- Inserting into the foreign key table will not result in an error, even if
+-- there is no matching key in the referenced table.
+INSERT INTO FKTABLE VALUES (1, 2);
+INSERT INTO FKTABLE VALUES (2, 3);
+
+-- Check FKTABLE
+SELECT * FROM FKTABLE;
+
+-- Reverting it back to ENFORCED will result in failure because constraint validation will be triggered,
+-- as it was previously in a valid state.
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+
+-- Remove any existing rows that violate the constraint, then attempt to change
+-- it.
+TRUNCATE FKTABLE;
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+
+-- Any further inserts will fail due to the enforcement.
+INSERT INTO FKTABLE VALUES (3, 4);
+
+--
+-- MATCH FULL
+--
+-- First test, check and cascade
+--
-- Insert test data into PKTABLE
INSERT INTO PKTABLE VALUES (1, 'Test1');
INSERT INTO PKTABLE VALUES (2, 'Test2');
@@ -230,6 +256,25 @@ INSERT INTO FKTABLE VALUES (1, NULL);
ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL;
+-- Modifying other attributes of a constraint should not affect its enforceability, and vice versa
+ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE NOT VALID NOT ENFORCED;
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_ftest2_fkey DEFERRABLE INITIALLY DEFERRED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fktable_ftest1_ftest2_fkey';
+
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_ftest2_fkey NOT ENFORCED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fktable_ftest1_ftest2_fkey';
+
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_ftest2_fkey ENFORCED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fktable_ftest1_ftest2_fkey';
+
+-- Can change enforceability and deferrability together
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_ftest2_fkey NOT ENFORCED NOT DEFERRABLE;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fktable_ftest1_ftest2_fkey';
+
DROP TABLE FKTABLE;
DROP TABLE PKTABLE;
@@ -968,10 +1013,23 @@ INSERT INTO fktable VALUES (0, 20);
COMMIT;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT ENFORCED;
+
+BEGIN;
+
+-- doesn't match FK, no error.
+UPDATE pktable SET id = 10 WHERE id = 5;
+-- doesn't match PK, no error.
+INSERT INTO fktable VALUES (0, 20);
+
+ROLLBACK;
+
-- try additional syntax
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE;
-- illegal option
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORCED;
+CREATE TEMP TABLE fktable2 (fk int references pktable ENFORCED NOT ENFORCED);
-- test order of firing of FK triggers when several RI-induced changes need to
-- be made to the same row. This was broken by subtransaction-related
@@ -1182,11 +1240,12 @@ ALTER TABLE fk_partitioned_fk DROP COLUMN fdrop1;
CREATE TABLE fk_partitioned_fk_1 (fdrop1 int, fdrop2 int, a int, fdrop3 int, b int);
ALTER TABLE fk_partitioned_fk_1 DROP COLUMN fdrop1, DROP COLUMN fdrop2, DROP COLUMN fdrop3;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FROM (0,0) TO (1000,1000);
-ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk;
+ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
+ALTER TABLE fk_partitioned_fk_2 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
-
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
CREATE TABLE fk_partitioned_fk_3 (fdrop1 int, fdrop2 int, fdrop3 int, fdrop4 int, b int, a int)
PARTITION BY HASH (a);
ALTER TABLE fk_partitioned_fk_3 DROP COLUMN fdrop1, DROP COLUMN fdrop2,
@@ -1232,6 +1291,27 @@ UPDATE fk_notpartitioned_pk SET b = 1502 WHERE a = 1500;
UPDATE fk_notpartitioned_pk SET b = 2504 WHERE a = 2500;
-- check psql behavior
\d fk_notpartitioned_pk
+
+-- Check the exsting FK trigger
+CREATE TEMP TABLE tmp_trg_info AS SELECT tgtype, tgrelid::regclass, tgconstraint
+FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+SELECT count(1) FROM tmp_trg_info;
+
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+-- No triggers
+SELECT count(1) FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+
+-- Changing it back to ENFORCED will recreate the necessary triggers.
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+
+-- Should be exactly the same number of triggers found as before
+SELECT COUNT(1) FROM pg_trigger pgt JOIN tmp_trg_info tt
+ON (pgt.tgtype = tt.tgtype AND pgt.tgrelid = tt.tgrelid AND pgt.tgconstraint = tt.tgconstraint);
+
ALTER TABLE fk_partitioned_fk DROP CONSTRAINT fk_partitioned_fk_a_b_fkey;
-- done.
DROP TABLE fk_notpartitioned_pk, fk_partitioned_fk;
@@ -1439,6 +1519,22 @@ ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN
\d fk_partitioned_fk_2
DROP TABLE fk_partitioned_fk_2;
+CREATE TABLE fk_partitioned_fk_2 (b int, a int,
+ FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
+BEGIN;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should be having two constraints
+\d fk_partitioned_fk_2
+DROP TABLE fk_partitioned_fk_2;
+ROLLBACK;
+BEGIN;
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have only one constraint
+\d fk_partitioned_fk_2
+ROLLBACK;
+DROP TABLE fk_partitioned_fk_2;
+
CREATE TABLE fk_partitioned_fk_4 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE) PARTITION BY RANGE (b, a);
CREATE TABLE fk_partitioned_fk_4_1 PARTITION OF fk_partitioned_fk_4 FOR VALUES FROM (1,1) TO (100,100);
CREATE TABLE fk_partitioned_fk_4_2 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE SET NULL);
--
2.43.5
On 2025-Feb-27, Amul Sul wrote:
Attached is the rebased patch set against the latest master head,
which also includes a *new* refactoring patch (0001). In this patch,
I’ve re-added ATExecAlterChildConstr(), which is required for the main
feature patch (0008) to handle recursion from different places while
altering enforceability.
I think you refer to ATExecAlterConstrEnforceability, which claims
(falsely) that it is a subroutine to ATExecAlterConstrRecurse; in
reality it is called from ATExecAlterConstraintInternal or at least
that's what I see in your 0008. So I wonder if you haven't confused
yourself here. If nothing else, that comments needs fixed. I didn't
review these patches.
--
Álvaro Herrera 48°01'N 7°57'E — https://www.EnterpriseDB.com/
"Porque francamente, si para saber manejarse a uno mismo hubiera que
rendir examen... ¿Quién es el machito que tendría carnet?" (Mafalda)
On Thu, Feb 27, 2025 at 4:48 PM Álvaro Herrera <alvherre@alvh.no-ip.org> wrote:
On 2025-Feb-27, Amul Sul wrote:
Attached is the rebased patch set against the latest master head,
which also includes a *new* refactoring patch (0001). In this patch,
I’ve re-added ATExecAlterChildConstr(), which is required for the main
feature patch (0008) to handle recursion from different places while
altering enforceability.I think you refer to ATExecAlterConstrEnforceability, which claims
(falsely) that it is a subroutine to ATExecAlterConstrRecurse; in
reality it is called from ATExecAlterConstraintInternal or at least
that's what I see in your 0008. So I wonder if you haven't confused
yourself here. If nothing else, that comments needs fixed. I didn't
review these patches.
Yeah, that was intentional. I wanted to avoid recursion again by
hitting ATExecAlterChildConstr() at the end of
ATExecAlterConstraintInternal(). Also, I realized the value doesn’t
matter since recurse = false is explicitly set inside the
cmdcon->alterEnforceability condition. I wasn’t fully satisfied with
how we handled the recursion decision (code design), so I’ll give it
more thought. If I don’t find a better approach, I’ll add clearer
comments to explain the reasoning.
Regards,
Amul
Hi Amul,
On Thu, Feb 27, 2025 at 12:57 AM Amul Sul <sulamul@gmail.com> wrote:
Attached is the rebased patch set against the latest master head,
which also includes a *new* refactoring patch (0001). In this patch,
I’ve re-added ATExecAlterChildConstr(), which is required for the main
feature patch (0008) to handle recursion from different places while
altering enforceability.
Thanks for the patches!
I reviewed and ran “make check” on each patch. I appreciate how the
patches are organized; separating the refactors from the
implementations made the review process very straightforward.
Overall, LGTM, and I have minor comments below:
0008
Since we are added "convalidated" in some of the constraints tests,
should we also add a "convalidated" field in the "table_constraints"
system view defined in src/backend/catalog/information_schema.sql? If
we do that, we'd also need to update the documentation for this view.
0009
Comment on top of the function ATExecAlterConstrEnforceability():
s/ATExecAlterConstrRecurse/ATExecAlterConstraintInternal/g
Typo in tablecmds.c: s/droping/dropping, s/ke/key
/* We should be droping trigger related to foreign ke constraint */
Thanks,
Alex
On Thu, Mar 6, 2025 at 9:37 PM Alexandra Wang
<alexandra.wang.oss@gmail.com> wrote:
Hi Amul,
On Thu, Feb 27, 2025 at 12:57 AM Amul Sul <sulamul@gmail.com> wrote:
Attached is the rebased patch set against the latest master head,
which also includes a *new* refactoring patch (0001). In this patch,
I’ve re-added ATExecAlterChildConstr(), which is required for the main
feature patch (0008) to handle recursion from different places while
altering enforceability.Thanks for the patches!
I reviewed and ran “make check” on each patch. I appreciate how the
patches are organized; separating the refactors from the
implementations made the review process very straightforward.
Thank you for the feedback and the review !
Overall, LGTM, and I have minor comments below:
0008
Since we are added "convalidated" in some of the constraints tests,
should we also add a "convalidated" field in the "table_constraints"
system view defined in src/backend/catalog/information_schema.sql? If
we do that, we'd also need to update the documentation for this view.
I am not sure why we don't already have "convalidated" in the
table_constraints, but if we need it, we can add it separately.
0009
Comment on top of the function ATExecAlterConstrEnforceability():
s/ATExecAlterConstrRecurse/ATExecAlterConstraintInternal/gTypo in tablecmds.c: s/droping/dropping, s/ke/key
/* We should be droping trigger related to foreign ke constraint */
Thanks, fixed in the attached version.
Regards,
Amul
Attachments:
v16-0001-refactor-re-add-ATExecAlterChildConstr.patchapplication/octet-stream; name=v16-0001-refactor-re-add-ATExecAlterChildConstr.patchDownload
From f000888d8161faee57582a033726109d3d7219bc Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Wed, 26 Feb 2025 15:49:11 +0530
Subject: [PATCH v16 1/9] refactor: re-add ATExecAlterChildConstr
ATExecAlterChildConstr was removed in commit
80d7f990496b1c7be61d9a00a2635b7d96b96197, but it is needed in the
next patches to recurse over child constraints.
---
src/backend/commands/tablecmds.c | 84 ++++++++++++++++++++------------
1 file changed, 54 insertions(+), 30 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 59156a1c1f6..4e49116d2c6 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -398,6 +398,10 @@ static bool ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdc
static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
bool deferrable, bool initdeferred,
List **otherrelids);
+static void ATExecAlterChildConstr(List **wqueue, ATAlterConstraint *cmdcon,
+ Relation conrel, Relation tgrel, Relation rel,
+ HeapTuple contuple, bool recurse, List **otherrelids,
+ LOCKMODE lockmode);
static ObjectAddress ATExecValidateConstraint(List **wqueue,
Relation rel, char *constrName,
bool recurse, bool recursing, LOCKMODE lockmode);
@@ -12029,41 +12033,13 @@ ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon,
/*
* If the table at either end of the constraint is partitioned, we need to
* handle every constraint that is a child of this one.
- *
- * Note that this doesn't handle recursion the normal way, viz. by
- * scanning the list of child relations and recursing; instead it uses the
- * conparentid relationships. This may need to be reconsidered.
*/
if (recurse && changed &&
(rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
(OidIsValid(refrelid) &&
get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)))
- {
- ScanKeyData pkey;
- SysScanDesc pscan;
- HeapTuple childtup;
-
- ScanKeyInit(&pkey,
- Anum_pg_constraint_conparentid,
- BTEqualStrategyNumber, F_OIDEQ,
- ObjectIdGetDatum(currcon->oid));
-
- pscan = systable_beginscan(conrel, ConstraintParentIndexId,
- true, NULL, 1, &pkey);
-
- while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
- {
- Form_pg_constraint childcon = (Form_pg_constraint) GETSTRUCT(childtup);
- Relation childrel;
-
- childrel = table_open(childcon->conrelid, lockmode);
- ATExecAlterConstraintInternal(wqueue, cmdcon, conrel, tgrel, childrel,
- childtup, recurse, otherrelids, lockmode);
- table_close(childrel, NoLock);
- }
-
- systable_endscan(pscan);
- }
+ ATExecAlterChildConstr(wqueue, cmdcon, conrel, tgrel, rel, contuple,
+ recurse, otherrelids, lockmode);
/*
* Update the catalog for inheritability. No work if the constraint is
@@ -12201,6 +12177,54 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
systable_endscan(tgscan);
}
+/*
+ * Invokes ATExecAlterConstraintInternal for each constraint that is a child of
+ * the specified constraint.
+ *
+ * Note that this doesn't handle recursion the normal way, viz. by scanning the
+ * list of child relations and recursing; instead it uses the conparentid
+ * relationships. This may need to be reconsidered.
+ *
+ * The arguments to this function have the same meaning as the arguments to
+ * ATExecAlterConstraintInternal.
+ */
+static void
+ATExecAlterChildConstr(List **wqueue, ATAlterConstraint *cmdcon,
+ Relation conrel, Relation tgrel, Relation rel,
+ HeapTuple contuple, bool recurse, List **otherrelids,
+ LOCKMODE lockmode)
+{
+ Form_pg_constraint currcon;
+ Oid conoid;
+ ScanKeyData pkey;
+ SysScanDesc pscan;
+ HeapTuple childtup;
+
+ currcon = (Form_pg_constraint) GETSTRUCT(contuple);
+ conoid = currcon->oid;
+
+ ScanKeyInit(&pkey,
+ Anum_pg_constraint_conparentid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+
+ pscan = systable_beginscan(conrel, ConstraintParentIndexId,
+ true, NULL, 1, &pkey);
+
+ while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
+ {
+ Form_pg_constraint childcon = (Form_pg_constraint) GETSTRUCT(childtup);
+ Relation childrel;
+
+ childrel = table_open(childcon->conrelid, lockmode);
+ ATExecAlterConstraintInternal(wqueue, cmdcon, conrel, tgrel, childrel,
+ childtup, recurse, otherrelids, lockmode);
+ table_close(childrel, NoLock);
+ }
+
+ systable_endscan(pscan);
+}
+
/*
* ALTER TABLE VALIDATE CONSTRAINT
*
--
2.43.5
v16-0002-refactor-Split-tryAttachPartitionForeignKey.patchapplication/octet-stream; name=v16-0002-refactor-Split-tryAttachPartitionForeignKey.patchDownload
From 5498d6f3e6c7d143aaf3aa76db5658c95573c415 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Wed, 29 Jan 2025 11:35:27 +0530
Subject: [PATCH v16 2/9] refactor: Split tryAttachPartitionForeignKey()
Split tryAttachPartitionForeignKey() into three functions:
AttachPartitionForeignKey(), RemoveInheritedConstraint() and
DropForeignKeyConstraintTriggers(), so they can be reused in the next
patch.
---
src/backend/commands/tablecmds.c | 319 ++++++++++++++++++++-----------
1 file changed, 204 insertions(+), 115 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 4e49116d2c6..7e50e39c4a8 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -585,6 +585,14 @@ static bool tryAttachPartitionForeignKey(List **wqueue,
Oid parentInsTrigger,
Oid parentUpdTrigger,
Relation trigrel);
+static void AttachPartitionForeignKey(List **wqueue, Relation partition,
+ Oid partConstrOid, Oid parentConstrOid,
+ Oid parentInsTrigger, Oid parentUpdTrigger,
+ Relation trigrel);
+static void RemoveInheritedConstraint(Relation conrel, Relation trigrel,
+ Oid conoid, Oid conrelid);
+static void DropForeignKeyConstraintTriggers(Relation trigrel, Oid conoid,
+ Oid confrelid, Oid conrelid);
static void GetForeignKeyActionTriggers(Relation trigrel,
Oid conoid, Oid confrelid, Oid conrelid,
Oid *deleteTriggerOid,
@@ -11465,12 +11473,6 @@ tryAttachPartitionForeignKey(List **wqueue,
Form_pg_constraint parentConstr;
HeapTuple partcontup;
Form_pg_constraint partConstr;
- bool queueValidation;
- ScanKeyData key;
- SysScanDesc scan;
- HeapTuple trigtup;
- Oid insertTriggerOid,
- updateTriggerOid;
parentConstrTup = SearchSysCache1(CONSTROID,
ObjectIdGetDatum(parentConstrOid));
@@ -11515,6 +11517,59 @@ tryAttachPartitionForeignKey(List **wqueue,
return false;
}
+ ReleaseSysCache(parentConstrTup);
+ ReleaseSysCache(partcontup);
+
+ /* Looks good! Attach this constraint. */
+ AttachPartitionForeignKey(wqueue, partition, fk->conoid,
+ parentConstrOid, parentInsTrigger,
+ parentUpdTrigger, trigrel);
+
+ return true;
+}
+
+/*
+ * AttachPartitionForeignKey
+ *
+ * The subroutine for tryAttachPartitionForeignKey performs the final tasks of
+ * attaching the constraint, removing redundant triggers and entries from
+ * pg_constraint, and setting the constraint's parent.
+ */
+static void
+AttachPartitionForeignKey(List **wqueue,
+ Relation partition,
+ Oid partConstrOid,
+ Oid parentConstrOid,
+ Oid parentInsTrigger,
+ Oid parentUpdTrigger,
+ Relation trigrel)
+{
+ HeapTuple parentConstrTup;
+ Form_pg_constraint parentConstr;
+ HeapTuple partcontup;
+ Form_pg_constraint partConstr;
+ bool queueValidation;
+ Oid partConstrFrelid;
+ Oid partConstrRelid;
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
+ /* Fetch the parent constraint tuple */
+ parentConstrTup = SearchSysCache1(CONSTROID,
+ ObjectIdGetDatum(parentConstrOid));
+ if (!HeapTupleIsValid(parentConstrTup))
+ elog(ERROR, "cache lookup failed for constraint %u", parentConstrOid);
+ parentConstr = (Form_pg_constraint) GETSTRUCT(parentConstrTup);
+
+ /* Fetch the child constraint tuple */
+ partcontup = SearchSysCache1(CONSTROID,
+ ObjectIdGetDatum(partConstrOid));
+ if (!HeapTupleIsValid(partcontup))
+ elog(ERROR, "cache lookup failed for constraint %u", partConstrOid);
+ partConstr = (Form_pg_constraint) GETSTRUCT(partcontup);
+ partConstrFrelid = partConstr->confrelid;
+ partConstrRelid = partConstr->conrelid;
+
/*
* Will we need to validate this constraint? A valid parent constraint
* implies that all child constraints have been validated, so if this one
@@ -11526,50 +11581,15 @@ tryAttachPartitionForeignKey(List **wqueue,
ReleaseSysCache(parentConstrTup);
/*
- * Looks good! Attach this constraint. The action triggers in the new
- * partition become redundant -- the parent table already has equivalent
- * ones, and those will be able to reach the partition. Remove the ones
- * in the partition. We identify them because they have our constraint
- * OID, as well as being on the referenced rel.
+ * The action triggers in the new partition become redundant -- the parent
+ * table already has equivalent ones, and those will be able to reach the
+ * partition. Remove the ones in the partition. We identify them because
+ * they have our constraint OID, as well as being on the referenced rel.
*/
- ScanKeyInit(&key,
- Anum_pg_trigger_tgconstraint,
- BTEqualStrategyNumber, F_OIDEQ,
- ObjectIdGetDatum(fk->conoid));
- scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
- NULL, 1, &key);
- while ((trigtup = systable_getnext(scan)) != NULL)
- {
- Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
- ObjectAddress trigger;
+ DropForeignKeyConstraintTriggers(trigrel, partConstrOid, partConstrFrelid,
+ partConstrRelid);
- if (trgform->tgconstrrelid != fk->conrelid)
- continue;
- if (trgform->tgrelid != fk->confrelid)
- continue;
-
- /*
- * The constraint is originally set up to contain this trigger as an
- * implementation object, so there's a dependency record that links
- * the two; however, since the trigger is no longer needed, we remove
- * the dependency link in order to be able to drop the trigger while
- * keeping the constraint intact.
- */
- deleteDependencyRecordsFor(TriggerRelationId,
- trgform->oid,
- false);
- /* make dependency deletion visible to performDeletion */
- CommandCounterIncrement();
- ObjectAddressSet(trigger, TriggerRelationId,
- trgform->oid);
- performDeletion(&trigger, DROP_RESTRICT, 0);
- /* make trigger drop visible, in case the loop iterates */
- CommandCounterIncrement();
- }
-
- systable_endscan(scan);
-
- ConstraintSetParentConstraint(fk->conoid, parentConstrOid,
+ ConstraintSetParentConstraint(partConstrOid, parentConstrOid,
RelationGetRelid(partition));
/*
@@ -11577,7 +11597,7 @@ tryAttachPartitionForeignKey(List **wqueue,
* corresponding parent triggers.
*/
GetForeignKeyCheckTriggers(trigrel,
- fk->conoid, fk->confrelid, fk->conrelid,
+ partConstrOid, partConstrFrelid, partConstrRelid,
&insertTriggerOid, &updateTriggerOid);
Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
@@ -11591,72 +11611,12 @@ tryAttachPartitionForeignKey(List **wqueue,
* attaching now has extra pg_constraint rows and action triggers that are
* no longer needed. Remove those.
*/
- if (get_rel_relkind(fk->confrelid) == RELKIND_PARTITIONED_TABLE)
+ if (get_rel_relkind(partConstrFrelid) == RELKIND_PARTITIONED_TABLE)
{
Relation pg_constraint = table_open(ConstraintRelationId, RowShareLock);
- ObjectAddresses *objs;
- HeapTuple consttup;
- ScanKeyInit(&key,
- Anum_pg_constraint_conrelid,
- BTEqualStrategyNumber, F_OIDEQ,
- ObjectIdGetDatum(fk->conrelid));
-
- scan = systable_beginscan(pg_constraint,
- ConstraintRelidTypidNameIndexId,
- true, NULL, 1, &key);
- objs = new_object_addresses();
- while ((consttup = systable_getnext(scan)) != NULL)
- {
- Form_pg_constraint conform = (Form_pg_constraint) GETSTRUCT(consttup);
-
- if (conform->conparentid != fk->conoid)
- continue;
- else
- {
- ObjectAddress addr;
- SysScanDesc scan2;
- ScanKeyData key2;
- int n PG_USED_FOR_ASSERTS_ONLY;
-
- ObjectAddressSet(addr, ConstraintRelationId, conform->oid);
- add_exact_object_address(&addr, objs);
-
- /*
- * First we must delete the dependency record that binds the
- * constraint records together.
- */
- n = deleteDependencyRecordsForSpecific(ConstraintRelationId,
- conform->oid,
- DEPENDENCY_INTERNAL,
- ConstraintRelationId,
- fk->conoid);
- Assert(n == 1); /* actually only one is expected */
-
- /*
- * Now search for the triggers for this constraint and set
- * them up for deletion too
- */
- ScanKeyInit(&key2,
- Anum_pg_trigger_tgconstraint,
- BTEqualStrategyNumber, F_OIDEQ,
- ObjectIdGetDatum(conform->oid));
- scan2 = systable_beginscan(trigrel, TriggerConstraintIndexId,
- true, NULL, 1, &key2);
- while ((trigtup = systable_getnext(scan2)) != NULL)
- {
- ObjectAddressSet(addr, TriggerRelationId,
- ((Form_pg_trigger) GETSTRUCT(trigtup))->oid);
- add_exact_object_address(&addr, objs);
- }
- systable_endscan(scan2);
- }
- }
- /* make the dependency deletions visible */
- CommandCounterIncrement();
- performMultipleDeletions(objs, DROP_RESTRICT,
- PERFORM_DELETION_INTERNAL);
- systable_endscan(scan);
+ RemoveInheritedConstraint(pg_constraint, trigrel, partConstrOid,
+ partConstrRelid);
table_close(pg_constraint, RowShareLock);
}
@@ -11677,9 +11637,10 @@ tryAttachPartitionForeignKey(List **wqueue,
Relation conrel;
conrel = table_open(ConstraintRelationId, RowExclusiveLock);
- partcontup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
+
+ partcontup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(partConstrOid));
if (!HeapTupleIsValid(partcontup))
- elog(ERROR, "cache lookup failed for constraint %u", fk->conoid);
+ elog(ERROR, "cache lookup failed for constraint %u", partConstrOid);
/* Use the same lock as for AT_ValidateConstraint */
QueueFKConstraintValidation(wqueue, conrel, partition, partcontup,
@@ -11687,8 +11648,136 @@ tryAttachPartitionForeignKey(List **wqueue,
ReleaseSysCache(partcontup);
table_close(conrel, RowExclusiveLock);
}
+}
- return true;
+/*
+ * RemoveInheritedConstraint
+ *
+ * Removes the constraint and its associated trigger from the specified
+ * relation, which inherited the given constraint.
+ */
+static void
+RemoveInheritedConstraint(Relation conrel, Relation trigrel, Oid conoid,
+ Oid conrelid)
+{
+ ObjectAddresses *objs;
+ HeapTuple consttup;
+ ScanKeyData key;
+ SysScanDesc scan;
+ HeapTuple trigtup;
+
+ ScanKeyInit(&key,
+ Anum_pg_constraint_conrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conrelid));
+
+ scan = systable_beginscan(conrel,
+ ConstraintRelidTypidNameIndexId,
+ true, NULL, 1, &key);
+ objs = new_object_addresses();
+ while ((consttup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_constraint conform = (Form_pg_constraint) GETSTRUCT(consttup);
+
+ if (conform->conparentid != conoid)
+ continue;
+ else
+ {
+ ObjectAddress addr;
+ SysScanDesc scan2;
+ ScanKeyData key2;
+ int n PG_USED_FOR_ASSERTS_ONLY;
+
+ ObjectAddressSet(addr, ConstraintRelationId, conform->oid);
+ add_exact_object_address(&addr, objs);
+
+ /*
+ * First we must delete the dependency record that binds the
+ * constraint records together.
+ */
+ n = deleteDependencyRecordsForSpecific(ConstraintRelationId,
+ conform->oid,
+ DEPENDENCY_INTERNAL,
+ ConstraintRelationId,
+ conoid);
+ Assert(n == 1); /* actually only one is expected */
+
+ /*
+ * Now search for the triggers for this constraint and set them up
+ * for deletion too
+ */
+ ScanKeyInit(&key2,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conform->oid));
+ scan2 = systable_beginscan(trigrel, TriggerConstraintIndexId,
+ true, NULL, 1, &key2);
+ while ((trigtup = systable_getnext(scan2)) != NULL)
+ {
+ ObjectAddressSet(addr, TriggerRelationId,
+ ((Form_pg_trigger) GETSTRUCT(trigtup))->oid);
+ add_exact_object_address(&addr, objs);
+ }
+ systable_endscan(scan2);
+ }
+ }
+ /* make the dependency deletions visible */
+ CommandCounterIncrement();
+ performMultipleDeletions(objs, DROP_RESTRICT,
+ PERFORM_DELETION_INTERNAL);
+ systable_endscan(scan);
+}
+
+/*
+ * DropForeignKeyConstraintTriggers
+ *
+ * The subroutine for tryAttachPartitionForeignKey handles the deletion of
+ * action triggers for the foreign key constraint.
+ */
+static void
+DropForeignKeyConstraintTriggers(Relation trigrel, Oid conoid, Oid confrelid,
+ Oid conrelid)
+{
+ ScanKeyData key;
+ SysScanDesc scan;
+ HeapTuple trigtup;
+
+ ScanKeyInit(&key,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+ scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
+ NULL, 1, &key);
+ while ((trigtup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
+ ObjectAddress trigger;
+
+ if (trgform->tgconstrrelid != conrelid)
+ continue;
+ if (trgform->tgrelid != confrelid)
+ continue;
+
+ /*
+ * The constraint is originally set up to contain this trigger as an
+ * implementation object, so there's a dependency record that links
+ * the two; however, since the trigger is no longer needed, we remove
+ * the dependency link in order to be able to drop the trigger while
+ * keeping the constraint intact.
+ */
+ deleteDependencyRecordsFor(TriggerRelationId,
+ trgform->oid,
+ false);
+ /* make dependency deletion visible to performDeletion */
+ CommandCounterIncrement();
+ ObjectAddressSet(trigger, TriggerRelationId,
+ trgform->oid);
+ performDeletion(&trigger, DROP_RESTRICT, 0);
+ /* make trigger drop visible, in case the loop iterates */
+ CommandCounterIncrement();
+ }
+
+ systable_endscan(scan);
}
/*
--
2.43.5
v16-0003-Move-the-RemoveInheritedConstraint-function-call.patchapplication/octet-stream; name=v16-0003-Move-the-RemoveInheritedConstraint-function-call.patchDownload
From 987addd4cd0b6fa3282b329798201eaad2c010f8 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 10 Feb 2025 09:12:06 +0530
Subject: [PATCH v16 3/9] Move the RemoveInheritedConstraint() function call
slightly earlier.
This change is harmless and does not affect the existing intended
operation. It is necessary for the feature patch operation, where we
may need to change the child constraint to enforced. In this case, we
would create the necessary triggers and queue the constraint for
validation, so it is important to remove any unnecessary constraints
before proceeding.
-- NOTE --
This is a small change that could have been included in the previous
"split tryAttachPartitionForeignKey" refactoring patch, but was kept
separate to highlight the changes.
---------
---
src/backend/commands/tablecmds.c | 30 +++++++++++++++---------------
1 file changed, 15 insertions(+), 15 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 7e50e39c4a8..91c9262ba7d 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -11570,6 +11570,21 @@ AttachPartitionForeignKey(List **wqueue,
partConstrFrelid = partConstr->confrelid;
partConstrRelid = partConstr->conrelid;
+ /*
+ * If the referenced table is partitioned, then the partition we're
+ * attaching now has extra pg_constraint rows and action triggers that are
+ * no longer needed. Remove those.
+ */
+ if (get_rel_relkind(partConstrFrelid) == RELKIND_PARTITIONED_TABLE)
+ {
+ Relation pg_constraint = table_open(ConstraintRelationId, RowShareLock);
+
+ RemoveInheritedConstraint(pg_constraint, trigrel, partConstrOid,
+ partConstrRelid);
+
+ table_close(pg_constraint, RowShareLock);
+ }
+
/*
* Will we need to validate this constraint? A valid parent constraint
* implies that all child constraints have been validated, so if this one
@@ -11606,21 +11621,6 @@ AttachPartitionForeignKey(List **wqueue,
TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
RelationGetRelid(partition));
- /*
- * If the referenced table is partitioned, then the partition we're
- * attaching now has extra pg_constraint rows and action triggers that are
- * no longer needed. Remove those.
- */
- if (get_rel_relkind(partConstrFrelid) == RELKIND_PARTITIONED_TABLE)
- {
- Relation pg_constraint = table_open(ConstraintRelationId, RowShareLock);
-
- RemoveInheritedConstraint(pg_constraint, trigrel, partConstrOid,
- partConstrRelid);
-
- table_close(pg_constraint, RowShareLock);
- }
-
/*
* We updated this pg_constraint row above to set its parent; validating
* it will cause its convalidated flag to change, so we need CCI here. In
--
2.43.5
v16-0004-refactor-Pass-Relid-instead-of-Relation-to-creat.patchapplication/octet-stream; name=v16-0004-refactor-Pass-Relid-instead-of-Relation-to-creat.patchDownload
From 3ea4e9f778b22a5e26196abe7f01876d624fe667 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 20 Jan 2025 18:49:41 +0530
Subject: [PATCH v16 4/9] refactor: Pass Relid instead of Relation to
createForeignKeyCheckTriggers().
Currently, createForeignKeyCheckTriggers() takes a Relation type as
its first argument, but it doesn't use that argument directly.
Instead, it fetches the Relid by calling RelationGetRelid().
Therefore, it would be more consistent with other functions (e.g.,
createForeignKeyCheckTriggers()) to pass the Relid directly instead of
the whole Relation.
---
src/backend/commands/tablecmds.c | 20 ++++++++++----------
1 file changed, 10 insertions(+), 10 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 91c9262ba7d..9bace24b731 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -571,7 +571,7 @@ static void createForeignKeyCheckTriggers(Oid myRelOid, Oid refRelOid,
Oid indexOid,
Oid parentInsTrigger, Oid parentUpdTrigger,
Oid *insertTrigOid, Oid *updateTrigOid);
-static void createForeignKeyActionTriggers(Relation rel, Oid refRelOid,
+static void createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid,
Constraint *fkconstraint, Oid constraintOid,
Oid indexOid,
Oid parentDelTrigger, Oid parentUpdTrigger,
@@ -10679,7 +10679,8 @@ addFkRecurseReferenced(Constraint *fkconstraint, Relation rel,
/*
* Create the action triggers that enforce the constraint.
*/
- createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
+ createForeignKeyActionTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
fkconstraint,
parentConstr, indexOid,
parentDelTrigger, parentUpdTrigger,
@@ -13145,10 +13146,11 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
* *updateTrigOid.
*/
static void
-createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstraint,
- Oid constraintOid, Oid indexOid,
- Oid parentDelTrigger, Oid parentUpdTrigger,
- Oid *deleteTrigOid, Oid *updateTrigOid)
+createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid,
+ Constraint *fkconstraint, Oid constraintOid,
+ Oid indexOid, Oid parentDelTrigger,
+ Oid parentUpdTrigger, Oid *deleteTrigOid,
+ Oid *updateTrigOid)
{
CreateTrigStmt *fk_trigger;
ObjectAddress trigAddress;
@@ -13204,8 +13206,7 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
break;
}
- trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid,
- RelationGetRelid(rel),
+ trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid, myRelOid,
constraintOid, indexOid, InvalidOid,
parentDelTrigger, NULL, true, false);
if (deleteTrigOid)
@@ -13265,8 +13266,7 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
break;
}
- trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid,
- RelationGetRelid(rel),
+ trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid, myRelOid,
constraintOid, indexOid, InvalidOid,
parentUpdTrigger, NULL, true, false);
if (updateTrigOid)
--
2.43.5
v16-0005-refactor-Change-ATExecAlterConstrRecurse-argumen.patchapplication/octet-stream; name=v16-0005-refactor-Change-ATExecAlterConstrRecurse-argumen.patchDownload
From b9623e40ab8692abf4195422e5ecc0ca1097c6f9 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Tue, 14 Jan 2025 22:35:32 +0530
Subject: [PATCH v16 5/9] refactor: Change ATExecAlterConstrRecurse() argument.
Instead of passing the Relation of the referencing relation, we will
pass a relid. Opening the relation using the relid again should not
cause significant issues. However, this approach makes recursion more
efficient, especially in the later patch where we will be recreating
the referencing and referred triggers. In addition to that passing
relid of the referred relation too.
----
NOTE: This patch is not meant to be committed separately. It should
be squashed with the main patch that adds ENFORCED/NOT ENFORCED.
----
---
src/backend/commands/tablecmds.c | 46 +++++++++++++++++---------------
1 file changed, 25 insertions(+), 21 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 9bace24b731..d8561fad53e 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -393,15 +393,18 @@ static ObjectAddress ATExecAlterConstraint(List **wqueue, Relation rel,
ATAlterConstraint *cmdcon,
bool recurse, LOCKMODE lockmode);
static bool ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon, Relation conrel,
- Relation tgrel, Relation rel, HeapTuple contuple,
- bool recurse, List **otherrelids, LOCKMODE lockmode);
+ Relation tgrel, const Oid fkrelid,
+ const Oid pkrelid, HeapTuple contuple,
+ bool recurse, List **otherrelids,
+ LOCKMODE lockmode);
static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
bool deferrable, bool initdeferred,
List **otherrelids);
static void ATExecAlterChildConstr(List **wqueue, ATAlterConstraint *cmdcon,
- Relation conrel, Relation tgrel, Relation rel,
- HeapTuple contuple, bool recurse, List **otherrelids,
- LOCKMODE lockmode);
+ Relation conrel, Relation tgrel,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, bool recurse,
+ List **otherrelids, LOCKMODE lockmode);
static ObjectAddress ATExecValidateConstraint(List **wqueue,
Relation rel, char *constrName,
bool recurse, bool recursing, LOCKMODE lockmode);
@@ -12036,7 +12039,8 @@ ATExecAlterConstraint(List **wqueue, Relation rel, ATAlterConstraint *cmdcon,
/*
* Do the actual catalog work, and recurse if necessary.
*/
- if (ATExecAlterConstraintInternal(wqueue, cmdcon, conrel, tgrel, rel,
+ if (ATExecAlterConstraintInternal(wqueue, cmdcon, conrel, tgrel,
+ currcon->conrelid, currcon->confrelid,
contuple, recurse, &otherrelids, lockmode))
ObjectAddressSet(address, ConstraintRelationId, currcon->oid);
@@ -12069,13 +12073,15 @@ ATExecAlterConstraint(List **wqueue, Relation rel, ATAlterConstraint *cmdcon,
*/
static bool
ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon,
- Relation conrel, Relation tgrel, Relation rel,
+ Relation conrel, Relation tgrel,
+ const Oid fkrelid, const Oid pkrelid,
HeapTuple contuple, bool recurse,
List **otherrelids, LOCKMODE lockmode)
{
Form_pg_constraint currcon;
Oid refrelid = InvalidOid;
bool changed = false;
+ Relation rel;
/* since this function recurses, it could be driven to stack overflow */
check_stack_depth();
@@ -12084,6 +12090,8 @@ ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon,
if (currcon->contype == CONSTRAINT_FOREIGN)
refrelid = currcon->confrelid;
+ rel = table_open(currcon->conrelid, lockmode);
+
/*
* Update pg_constraint with the flags from cmdcon.
*
@@ -12128,8 +12136,10 @@ ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon,
(rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
(OidIsValid(refrelid) &&
get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)))
- ATExecAlterChildConstr(wqueue, cmdcon, conrel, tgrel, rel, contuple,
- recurse, otherrelids, lockmode);
+ ATExecAlterChildConstr(wqueue, cmdcon, conrel, tgrel, fkrelid, pkrelid,
+ contuple, recurse, otherrelids, lockmode);
+
+ table_close(rel, NoLock);
/*
* Update the catalog for inheritability. No work if the constraint is
@@ -12280,9 +12290,9 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
*/
static void
ATExecAlterChildConstr(List **wqueue, ATAlterConstraint *cmdcon,
- Relation conrel, Relation tgrel, Relation rel,
- HeapTuple contuple, bool recurse, List **otherrelids,
- LOCKMODE lockmode)
+ Relation conrel, Relation tgrel, const Oid fkrelid,
+ const Oid pkrelid, HeapTuple contuple, bool recurse,
+ List **otherrelids, LOCKMODE lockmode)
{
Form_pg_constraint currcon;
Oid conoid;
@@ -12302,15 +12312,9 @@ ATExecAlterChildConstr(List **wqueue, ATAlterConstraint *cmdcon,
true, NULL, 1, &pkey);
while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
- {
- Form_pg_constraint childcon = (Form_pg_constraint) GETSTRUCT(childtup);
- Relation childrel;
-
- childrel = table_open(childcon->conrelid, lockmode);
- ATExecAlterConstraintInternal(wqueue, cmdcon, conrel, tgrel, childrel,
- childtup, recurse, otherrelids, lockmode);
- table_close(childrel, NoLock);
- }
+ ATExecAlterConstraintInternal(wqueue, cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, childtup, recurse, otherrelids,
+ lockmode);
systable_endscan(pscan);
}
--
2.43.5
v16-0006-Remove-hastriggers-flag-check-before-fetching-FK.patchapplication/octet-stream; name=v16-0006-Remove-hastriggers-flag-check-before-fetching-FK.patchDownload
From 394115c2f217696688ad4b7b99bbc5c3aff21f77 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 7 Oct 2024 12:40:07 +0530
Subject: [PATCH v16 6/9] Remove hastriggers flag check before fetching FK
constraints.
With NOT ENFORCED, foreign key constraints will be created without
triggers. Therefore, the criteria for fetching foreign keys based on
the presence of triggers no longer apply.
----
NOTE: This patch is intended to reduce the diff noise from the main
patch and is not meant to be committed separately. It should be
squashed with the main patch that adds ENFORCED/NOT ENFORCED.
----
---
src/backend/utils/cache/relcache.c | 5 -
src/bin/pg_dump/pg_dump.c | 8 +-
src/bin/psql/describe.c | 226 ++++++++++++++---------------
3 files changed, 108 insertions(+), 131 deletions(-)
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index d1ae761b3f6..1a01f18f0d3 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4663,11 +4663,6 @@ RelationGetFKeyList(Relation relation)
if (relation->rd_fkeyvalid)
return relation->rd_fkeylist;
- /* Fast path: non-partitioned tables without triggers can't have FKs */
- if (!relation->rd_rel->relhastriggers &&
- relation->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
- return NIL;
-
/*
* We build the list we intend to return (in the caller's context) while
* doing the scan. After successfully completing the scan, we copy that
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index c371570501a..59b2f513101 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -7973,13 +7973,7 @@ getConstraints(Archive *fout, TableInfo tblinfo[], int numTables)
{
TableInfo *tinfo = &tblinfo[i];
- /*
- * For partitioned tables, foreign keys have no triggers so they must
- * be included anyway in case some foreign keys are defined.
- */
- if ((!tinfo->hastriggers &&
- tinfo->relkind != RELKIND_PARTITIONED_TABLE) ||
- !(tinfo->dobj.dump & DUMP_COMPONENT_DEFINITION))
+ if (!(tinfo->dobj.dump & DUMP_COMPONENT_DEFINITION))
continue;
/* OK, we need info for this table */
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..a23abb2417c 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2550,136 +2550,124 @@ describeOneTableDetails(const char *schemaname,
PQclear(result);
}
- /*
- * Print foreign-key constraints (there are none if no triggers,
- * except if the table is partitioned, in which case the triggers
- * appear in the partitions)
- */
- if (tableinfo.hastriggers ||
- tableinfo.relkind == RELKIND_PARTITIONED_TABLE)
+ /* Print foreign-key constraints */
+ if (pset.sversion >= 120000 &&
+ (tableinfo.ispartition || tableinfo.relkind == RELKIND_PARTITIONED_TABLE))
{
- if (pset.sversion >= 120000 &&
- (tableinfo.ispartition || tableinfo.relkind == RELKIND_PARTITIONED_TABLE))
+ /*
+ * Put the constraints defined in this table first, followed
+ * by the constraints defined in ancestor partitioned tables.
+ */
+ printfPQExpBuffer(&buf,
+ "SELECT conrelid = '%s'::pg_catalog.regclass AS sametable,\n"
+ " conname,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef,\n"
+ " conrelid::pg_catalog.regclass AS ontable\n"
+ " FROM pg_catalog.pg_constraint,\n"
+ " pg_catalog.pg_partition_ancestors('%s')\n"
+ " WHERE conrelid = relid AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
+ "ORDER BY sametable DESC, conname;",
+ oid, oid);
+ }
+ else
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT true as sametable, conname,\n"
+ " pg_catalog.pg_get_constraintdef(r.oid, true) as condef,\n"
+ " conrelid::pg_catalog.regclass AS ontable\n"
+ "FROM pg_catalog.pg_constraint r\n"
+ "WHERE r.conrelid = '%s' AND r.contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n",
+ oid);
+
+ if (pset.sversion >= 120000)
+ appendPQExpBufferStr(&buf, " AND conparentid = 0\n");
+ appendPQExpBufferStr(&buf, "ORDER BY conname");
+ }
+
+ result = PSQLexec(buf.data);
+ if (!result)
+ goto error_return;
+ else
+ tuples = PQntuples(result);
+
+ if (tuples > 0)
+ {
+ int i_sametable = PQfnumber(result, "sametable"),
+ i_conname = PQfnumber(result, "conname"),
+ i_condef = PQfnumber(result, "condef"),
+ i_ontable = PQfnumber(result, "ontable");
+
+ printTableAddFooter(&cont, _("Foreign-key constraints:"));
+ for (i = 0; i < tuples; i++)
{
/*
- * Put the constraints defined in this table first, followed
- * by the constraints defined in ancestor partitioned tables.
+ * Print untranslated constraint name and definition. Use
+ * a "TABLE tab" prefix when the constraint is defined in
+ * a parent partitioned table.
*/
- printfPQExpBuffer(&buf,
- "SELECT conrelid = '%s'::pg_catalog.regclass AS sametable,\n"
- " conname,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef,\n"
- " conrelid::pg_catalog.regclass AS ontable\n"
- " FROM pg_catalog.pg_constraint,\n"
- " pg_catalog.pg_partition_ancestors('%s')\n"
- " WHERE conrelid = relid AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
- "ORDER BY sametable DESC, conname;",
- oid, oid);
- }
- else
- {
- printfPQExpBuffer(&buf,
- "SELECT true as sametable, conname,\n"
- " pg_catalog.pg_get_constraintdef(r.oid, true) as condef,\n"
- " conrelid::pg_catalog.regclass AS ontable\n"
- "FROM pg_catalog.pg_constraint r\n"
- "WHERE r.conrelid = '%s' AND r.contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n",
- oid);
-
- if (pset.sversion >= 120000)
- appendPQExpBufferStr(&buf, " AND conparentid = 0\n");
- appendPQExpBufferStr(&buf, "ORDER BY conname");
- }
-
- result = PSQLexec(buf.data);
- if (!result)
- goto error_return;
- else
- tuples = PQntuples(result);
-
- if (tuples > 0)
- {
- int i_sametable = PQfnumber(result, "sametable"),
- i_conname = PQfnumber(result, "conname"),
- i_condef = PQfnumber(result, "condef"),
- i_ontable = PQfnumber(result, "ontable");
-
- printTableAddFooter(&cont, _("Foreign-key constraints:"));
- for (i = 0; i < tuples; i++)
- {
- /*
- * Print untranslated constraint name and definition. Use
- * a "TABLE tab" prefix when the constraint is defined in
- * a parent partitioned table.
- */
- if (strcmp(PQgetvalue(result, i, i_sametable), "f") == 0)
- printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
- PQgetvalue(result, i, i_ontable),
- PQgetvalue(result, i, i_conname),
- PQgetvalue(result, i, i_condef));
- else
- printfPQExpBuffer(&buf, " \"%s\" %s",
- PQgetvalue(result, i, i_conname),
- PQgetvalue(result, i, i_condef));
-
- printTableAddFooter(&cont, buf.data);
- }
- }
- PQclear(result);
- }
-
- /* print incoming foreign-key references */
- if (tableinfo.hastriggers ||
- tableinfo.relkind == RELKIND_PARTITIONED_TABLE)
- {
- if (pset.sversion >= 120000)
- {
- printfPQExpBuffer(&buf,
- "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
- " FROM pg_catalog.pg_constraint c\n"
- " WHERE confrelid IN (SELECT pg_catalog.pg_partition_ancestors('%s')\n"
- " UNION ALL VALUES ('%s'::pg_catalog.regclass))\n"
- " AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
- "ORDER BY conname;",
- oid, oid);
- }
- else
- {
- printfPQExpBuffer(&buf,
- "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
- " FROM pg_catalog.pg_constraint\n"
- " WHERE confrelid = %s AND contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n"
- "ORDER BY conname;",
- oid);
- }
-
- result = PSQLexec(buf.data);
- if (!result)
- goto error_return;
- else
- tuples = PQntuples(result);
-
- if (tuples > 0)
- {
- int i_conname = PQfnumber(result, "conname"),
- i_ontable = PQfnumber(result, "ontable"),
- i_condef = PQfnumber(result, "condef");
-
- printTableAddFooter(&cont, _("Referenced by:"));
- for (i = 0; i < tuples; i++)
- {
+ if (strcmp(PQgetvalue(result, i, i_sametable), "f") == 0)
printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
PQgetvalue(result, i, i_ontable),
PQgetvalue(result, i, i_conname),
PQgetvalue(result, i, i_condef));
+ else
+ printfPQExpBuffer(&buf, " \"%s\" %s",
+ PQgetvalue(result, i, i_conname),
+ PQgetvalue(result, i, i_condef));
- printTableAddFooter(&cont, buf.data);
- }
+ printTableAddFooter(&cont, buf.data);
}
- PQclear(result);
}
+ PQclear(result);
+
+ /* print incoming foreign-key references */
+ if (pset.sversion >= 120000)
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
+ " FROM pg_catalog.pg_constraint c\n"
+ " WHERE confrelid IN (SELECT pg_catalog.pg_partition_ancestors('%s')\n"
+ " UNION ALL VALUES ('%s'::pg_catalog.regclass))\n"
+ " AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
+ "ORDER BY conname;",
+ oid, oid);
+ }
+ else
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
+ " FROM pg_catalog.pg_constraint\n"
+ " WHERE confrelid = %s AND contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n"
+ "ORDER BY conname;",
+ oid);
+ }
+
+ result = PSQLexec(buf.data);
+ if (!result)
+ goto error_return;
+ else
+ tuples = PQntuples(result);
+
+ if (tuples > 0)
+ {
+ int i_conname = PQfnumber(result, "conname"),
+ i_ontable = PQfnumber(result, "ontable"),
+ i_condef = PQfnumber(result, "condef");
+
+ printTableAddFooter(&cont, _("Referenced by:"));
+ for (i = 0; i < tuples; i++)
+ {
+ printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
+ PQgetvalue(result, i, i_ontable),
+ PQgetvalue(result, i, i_conname),
+ PQgetvalue(result, i, i_condef));
+
+ printTableAddFooter(&cont, buf.data);
+ }
+ }
+ PQclear(result);
/* print any row-level policies */
if (pset.sversion >= 90500)
--
2.43.5
v16-0007-Ease-the-restriction-that-a-NOT-ENFORCED-constra.patchapplication/octet-stream; name=v16-0007-Ease-the-restriction-that-a-NOT-ENFORCED-constra.patchDownload
From f99a766a2bfad9af9b90d731ca9f0ba0cdc7d1fe Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Fri, 7 Feb 2025 09:41:20 +0530
Subject: [PATCH v16 7/9] Ease the restriction that a NOT ENFORCED constraint
must be INVALID.
In the initial support for NOT ENFORCED check constraints in commit
ca87c415e2fccf81cec6fd45698dde9fae0ab570, we introduced a restriction
that a NOT ENFORCED constraint must be NOT VALID. However, we still
mark a NOT ENFORCED constraint as NOT VALID when adding it to an
already existing table, as validation cannot be performed on a NOT
ENFORCED constraint. Conversely, when the constraint is created along
with the table, it is acceptable to mark the constraint as valid.
---
src/backend/catalog/heap.c | 3 +-
src/backend/catalog/pg_constraint.c | 2 -
src/backend/commands/tablecmds.c | 19 ++---
src/backend/optimizer/util/plancat.c | 9 +--
src/backend/parser/gram.y | 9 ---
src/backend/parser/parse_utilcmd.c | 13 ++--
src/backend/utils/adt/ruleutils.c | 6 +-
src/test/regress/expected/alter_table.out | 4 +-
src/test/regress/expected/inherit.out | 88 ++++++++++++++---------
src/test/regress/sql/alter_table.sql | 3 +-
src/test/regress/sql/inherit.sql | 13 +++-
11 files changed, 84 insertions(+), 85 deletions(-)
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index bd3554c0bfd..96ed3824088 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -2791,7 +2791,7 @@ MergeWithExistingConstraint(Relation rel, const char *ccname, Node *expr,
* If the child constraint is "not valid" then cannot merge with a
* valid parent constraint.
*/
- if (is_initially_valid && con->conenforced && !con->convalidated)
+ if (is_initially_valid && !con->convalidated)
ereport(ERROR,
(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
errmsg("constraint \"%s\" conflicts with NOT VALID constraint on relation \"%s\"",
@@ -2854,7 +2854,6 @@ MergeWithExistingConstraint(Relation rel, const char *ccname, Node *expr,
{
Assert(is_local);
con->conenforced = true;
- con->convalidated = true;
}
CatalogTupleUpdate(conDesc, &tup->t_self, tup);
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index ac80652baf2..60bc54a96b6 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -102,8 +102,6 @@ CreateConstraintEntry(const char *constraintName,
/* Only CHECK constraint can be not enforced */
Assert(isEnforced || constraintType == CONSTRAINT_CHECK);
- /* NOT ENFORCED constraint must be NOT VALID */
- Assert(isEnforced || !isValidated);
conDesc = table_open(ConstraintRelationId, RowExclusiveLock);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d8561fad53e..5b2b284002e 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -3157,10 +3157,7 @@ MergeCheckConstraint(List *constraints, const char *name, Node *expr, bool is_en
* marked as ENFORCED because one of the parents is ENFORCED.
*/
if (!ccon->is_enforced && is_enforced)
- {
ccon->is_enforced = true;
- ccon->skip_validation = false;
- }
return constraints;
}
@@ -3181,7 +3178,6 @@ MergeCheckConstraint(List *constraints, const char *name, Node *expr, bool is_en
newcon->expr = expr;
newcon->inhcount = 1;
newcon->is_enforced = is_enforced;
- newcon->skip_validation = !is_enforced;
return lappend(constraints, newcon);
}
@@ -17010,8 +17006,7 @@ MergeConstraintsIntoExisting(Relation child_rel, Relation parent_rel)
* If the child constraint is "not valid" then cannot merge with a
* valid parent constraint
*/
- if (parent_con->convalidated && child_con->conenforced &&
- !child_con->convalidated)
+ if (parent_con->convalidated && !child_con->convalidated)
ereport(ERROR,
(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
errmsg("constraint \"%s\" conflicts with NOT VALID constraint on child table \"%s\"",
@@ -19379,18 +19374,12 @@ ConstraintImpliedByRelConstraint(Relation scanrel, List *testConstraint, List *p
Node *cexpr;
/*
- * If this constraint hasn't been fully validated yet, we must ignore
- * it here.
+ * If this constraint hasn't been fully validated yet or is not
+ * enforced, we must ignore it here.
*/
- if (!constr->check[i].ccvalid)
+ if (!constr->check[i].ccvalid || !constr->check[i].ccenforced)
continue;
- /*
- * NOT ENFORCED constraints are always marked as invalid, which should
- * have been ignored.
- */
- Assert(constr->check[i].ccenforced);
-
cexpr = stringToNode(constr->check[i].ccbin);
/*
diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c
index 71abb01f655..233a5d8da98 100644
--- a/src/backend/optimizer/util/plancat.c
+++ b/src/backend/optimizer/util/plancat.c
@@ -1302,22 +1302,15 @@ get_relation_constraints(PlannerInfo *root,
* ignore it here. Also ignore if NO INHERIT and we weren't told
* that that's safe.
*/
- if (!constr->check[i].ccvalid)
+ if (!constr->check[i].ccvalid || !constr->check[i].ccenforced)
continue;
- /*
- * NOT ENFORCED constraints are always marked as invalid, which
- * should have been ignored.
- */
- Assert(constr->check[i].ccenforced);
-
/*
* Also ignore if NO INHERIT and we weren't told that that's safe.
*/
if (constr->check[i].ccnoinherit && !include_noinherit)
continue;
-
cexpr = stringToNode(constr->check[i].ccbin);
/*
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 271ae26cbaf..042fbbdc078 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -19587,15 +19587,6 @@ processCASbits(int cas_bits, int location, const char *constrType,
errmsg("%s constraints cannot be marked NOT ENFORCED",
constrType),
parser_errposition(location)));
-
- /*
- * NB: The validated status is irrelevant when the constraint is set to
- * NOT ENFORCED, but for consistency, it should be set accordingly.
- * This ensures that if the constraint is later changed to ENFORCED, it
- * will automatically be in the correct NOT VALIDATED state.
- */
- if (not_valid)
- *not_valid = true;
}
if (cas_bits & CAS_ENFORCED)
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index abbe1bb45a3..37becb62356 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -1453,7 +1453,6 @@ expandTableLikeClause(RangeVar *heapRel, TableLikeClause *table_like_clause)
char *ccname = constr->check[ccnum].ccname;
char *ccbin = constr->check[ccnum].ccbin;
bool ccenforced = constr->check[ccnum].ccenforced;
- bool ccvalid = constr->check[ccnum].ccvalid;
bool ccnoinherit = constr->check[ccnum].ccnoinherit;
Node *ccbin_node;
bool found_whole_row;
@@ -1484,13 +1483,13 @@ expandTableLikeClause(RangeVar *heapRel, TableLikeClause *table_like_clause)
n->conname = pstrdup(ccname);
n->location = -1;
n->is_enforced = ccenforced;
- n->initially_valid = ccvalid;
n->is_no_inherit = ccnoinherit;
n->raw_expr = NULL;
n->cooked_expr = nodeToString(ccbin_node);
/* We can skip validation, since the new table should be empty. */
n->skip_validation = true;
+ n->initially_valid = true;
atsubcmd = makeNode(AlterTableCmd);
atsubcmd->subtype = AT_AddConstraint;
@@ -2944,11 +2943,9 @@ transformCheckConstraints(CreateStmtContext *cxt, bool skipValidation)
return;
/*
- * When creating a new table (but not a foreign table), we can safely skip
- * the validation of check constraints and mark them as valid based on the
- * constraint enforcement flag, since NOT ENFORCED constraints must always
- * be marked as NOT VALID. (This will override any user-supplied NOT VALID
- * flag.)
+ * If creating a new table (but not a foreign table), we can safely skip
+ * validation of check constraints, and nonetheless mark them valid. (This
+ * will override any user-supplied NOT VALID flag.)
*/
if (skipValidation)
{
@@ -2957,7 +2954,7 @@ transformCheckConstraints(CreateStmtContext *cxt, bool skipValidation)
Constraint *constraint = (Constraint *) lfirst(ckclist);
constraint->skip_validation = true;
- constraint->initially_valid = constraint->is_enforced;
+ constraint->initially_valid = true;
}
}
}
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index d11a8a20eea..f709ded1136 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -2594,12 +2594,10 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
appendStringInfoString(&buf, " DEFERRABLE");
if (conForm->condeferred)
appendStringInfoString(&buf, " INITIALLY DEFERRED");
-
- /* Validated status is irrelevant when the constraint is NOT ENFORCED. */
+ if (!conForm->convalidated)
+ appendStringInfoString(&buf, " NOT VALID");
if (!conForm->conenforced)
appendStringInfoString(&buf, " NOT ENFORCED");
- else if (!conForm->convalidated)
- appendStringInfoString(&buf, " NOT VALID");
/* Cleanup */
systable_endscan(scandesc);
diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out
index 362f38856d2..54bdb3f2250 100644
--- a/src/test/regress/expected/alter_table.out
+++ b/src/test/regress/expected/alter_table.out
@@ -507,7 +507,9 @@ ALTER TABLE attmp3 validate constraint attmpconstr;
ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten CHECK (b > 10); -- fail
ERROR: check constraint "b_greater_than_ten" of relation "attmp3" is violated by some row
ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten CHECK (b > 10) NOT VALID; -- succeeds
-ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten_not_enforced CHECK (b > 10) NOT ENFORCED; -- succeeds
+ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten_not_enforced CHECK (b > 10) NOT ENFORCED; -- fail
+ERROR: check constraint "b_greater_than_ten_not_enforced" of relation "attmp3" is violated by some row
+ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten_not_enforced CHECK (b > 10) NOT VALID NOT ENFORCED; -- succeeds
ALTER TABLE attmp3 VALIDATE CONSTRAINT b_greater_than_ten; -- fails
ERROR: check constraint "b_greater_than_ten" of relation "attmp3" is violated by some row
DELETE FROM attmp3 WHERE NOT b > 10;
diff --git a/src/test/regress/expected/inherit.out b/src/test/regress/expected/inherit.out
index e671975a281..70e4830e032 100644
--- a/src/test/regress/expected/inherit.out
+++ b/src/test/regress/expected/inherit.out
@@ -1332,6 +1332,18 @@ NOTICE: merging constraint "inh_check_constraint5" with inherited definition
alter table p1 add constraint inh_check_constraint6 check (f1 < 10) not enforced;
alter table p1_c1 add constraint inh_check_constraint6 check (f1 < 10) enforced;
NOTICE: merging constraint "inh_check_constraint6" with inherited definition
+-- however, cannot merge a not valid enforced child constraint with a valid
+-- not-enforced parent constraint.
+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;
+ERROR: constraint "inh_check_constraint9" conflicts with NOT VALID constraint on relation "p1_c1"
+-- but, allowed if the parent constraint is also invalid.
+alter table p1 add constraint inh_check_constraint9 check (f1 < 10) not valid not enforced;
+NOTICE: merging constraint "inh_check_constraint9" with inherited definition
+-- the invalid state of the child constraint will be ignored here.
+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
create table p1_c2(f1 int constraint inh_check_constraint4 check (f1 < 10)) inherits(p1);
NOTICE: merging column "f1" with inherited definition
NOTICE: merging constraint "inh_check_constraint4" with inherited definition
@@ -1353,42 +1365,50 @@ create table p1_fail(f1 int constraint inh_check_constraint6 check (f1 < 10) not
NOTICE: merging multiple inherited definitions of column "f1"
NOTICE: merging column "f1" with inherited definition
ERROR: constraint "inh_check_constraint6" conflicts with NOT ENFORCED constraint on relation "p1_fail"
-select conrelid::regclass::text as relname, conname, conislocal, coninhcount, conenforced
+select conrelid::regclass::text as relname, conname, conislocal, coninhcount, conenforced, convalidated
from pg_constraint where conname like 'inh\_check\_constraint%'
order by 1, 2;
- relname | conname | conislocal | coninhcount | conenforced
----------+-----------------------+------------+-------------+-------------
- p1 | inh_check_constraint1 | t | 0 | t
- p1 | inh_check_constraint2 | t | 0 | t
- p1 | inh_check_constraint3 | t | 0 | f
- p1 | inh_check_constraint4 | t | 0 | f
- p1 | inh_check_constraint5 | t | 0 | f
- p1 | inh_check_constraint6 | t | 0 | f
- p1 | inh_check_constraint8 | t | 0 | t
- p1_c1 | inh_check_constraint1 | t | 1 | t
- p1_c1 | inh_check_constraint2 | t | 1 | t
- p1_c1 | inh_check_constraint3 | t | 1 | f
- p1_c1 | inh_check_constraint4 | t | 1 | f
- p1_c1 | inh_check_constraint5 | t | 1 | t
- p1_c1 | inh_check_constraint6 | t | 1 | t
- p1_c1 | inh_check_constraint7 | t | 0 | f
- p1_c1 | inh_check_constraint8 | f | 1 | t
- p1_c2 | inh_check_constraint1 | f | 1 | t
- p1_c2 | inh_check_constraint2 | f | 1 | t
- p1_c2 | inh_check_constraint3 | f | 1 | f
- p1_c2 | inh_check_constraint4 | t | 1 | t
- p1_c2 | inh_check_constraint5 | f | 1 | f
- p1_c2 | inh_check_constraint6 | f | 1 | f
- p1_c2 | inh_check_constraint8 | f | 1 | t
- p1_c3 | inh_check_constraint1 | f | 2 | t
- p1_c3 | inh_check_constraint2 | f | 2 | t
- p1_c3 | inh_check_constraint3 | f | 2 | f
- p1_c3 | inh_check_constraint4 | f | 2 | f
- p1_c3 | inh_check_constraint5 | f | 2 | t
- p1_c3 | inh_check_constraint6 | f | 2 | t
- p1_c3 | inh_check_constraint7 | f | 1 | f
- p1_c3 | inh_check_constraint8 | f | 2 | t
-(30 rows)
+ relname | conname | conislocal | coninhcount | conenforced | convalidated
+---------+------------------------+------------+-------------+-------------+--------------
+ p1 | inh_check_constraint1 | t | 0 | t | t
+ p1 | inh_check_constraint10 | t | 0 | f | t
+ p1 | inh_check_constraint2 | t | 0 | t | t
+ p1 | inh_check_constraint3 | t | 0 | f | t
+ p1 | inh_check_constraint4 | t | 0 | f | t
+ p1 | inh_check_constraint5 | t | 0 | f | t
+ p1 | inh_check_constraint6 | t | 0 | f | t
+ 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_constraint2 | t | 1 | t | t
+ p1_c1 | inh_check_constraint3 | t | 1 | f | t
+ p1_c1 | inh_check_constraint4 | t | 1 | f | t
+ p1_c1 | inh_check_constraint5 | t | 1 | t | t
+ p1_c1 | inh_check_constraint6 | t | 1 | t | t
+ p1_c1 | inh_check_constraint7 | t | 0 | f | t
+ p1_c1 | inh_check_constraint8 | f | 1 | t | t
+ p1_c1 | inh_check_constraint9 | t | 1 | t | f
+ p1_c2 | inh_check_constraint1 | f | 1 | t | t
+ p1_c2 | inh_check_constraint10 | f | 1 | f | t
+ p1_c2 | inh_check_constraint2 | f | 1 | t | t
+ p1_c2 | inh_check_constraint3 | f | 1 | f | t
+ p1_c2 | inh_check_constraint4 | t | 1 | t | t
+ p1_c2 | inh_check_constraint5 | f | 1 | f | t
+ p1_c2 | inh_check_constraint6 | f | 1 | f | t
+ p1_c2 | inh_check_constraint8 | f | 1 | t | t
+ p1_c2 | inh_check_constraint9 | f | 1 | f | t
+ p1_c3 | inh_check_constraint1 | f | 2 | t | t
+ p1_c3 | inh_check_constraint10 | f | 2 | t | t
+ p1_c3 | inh_check_constraint2 | f | 2 | t | t
+ p1_c3 | inh_check_constraint3 | f | 2 | f | t
+ p1_c3 | inh_check_constraint4 | f | 2 | f | t
+ p1_c3 | inh_check_constraint5 | f | 2 | t | t
+ p1_c3 | inh_check_constraint6 | f | 2 | t | t
+ p1_c3 | inh_check_constraint7 | f | 1 | f | t
+ p1_c3 | inh_check_constraint8 | f | 2 | t | t
+ p1_c3 | inh_check_constraint9 | f | 2 | t | t
+(38 rows)
drop table p1 cascade;
NOTICE: drop cascades to 3 other objects
diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql
index 84e93ef575e..a9268de14b5 100644
--- a/src/test/regress/sql/alter_table.sql
+++ b/src/test/regress/sql/alter_table.sql
@@ -387,7 +387,8 @@ ALTER TABLE attmp3 validate constraint attmpconstr;
-- Try a non-verified CHECK constraint
ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten CHECK (b > 10); -- fail
ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten CHECK (b > 10) NOT VALID; -- succeeds
-ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten_not_enforced CHECK (b > 10) NOT ENFORCED; -- succeeds
+ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten_not_enforced CHECK (b > 10) NOT ENFORCED; -- fail
+ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten_not_enforced CHECK (b > 10) NOT VALID NOT ENFORCED; -- succeeds
ALTER TABLE attmp3 VALIDATE CONSTRAINT b_greater_than_ten; -- fails
DELETE FROM attmp3 WHERE NOT b > 10;
ALTER TABLE attmp3 VALIDATE CONSTRAINT b_greater_than_ten; -- succeeds
diff --git a/src/test/regress/sql/inherit.sql b/src/test/regress/sql/inherit.sql
index 4e73c70495c..5169456181b 100644
--- a/src/test/regress/sql/inherit.sql
+++ b/src/test/regress/sql/inherit.sql
@@ -481,6 +481,17 @@ alter table p1 add constraint inh_check_constraint5 check (f1 < 10) not enforced
alter table p1 add constraint inh_check_constraint6 check (f1 < 10) not enforced;
alter table p1_c1 add constraint inh_check_constraint6 check (f1 < 10) enforced;
+-- however, cannot merge a not valid enforced child constraint with a valid
+-- not-enforced parent constraint.
+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;
+-- but, allowed if the parent constraint is also invalid.
+alter table p1 add constraint inh_check_constraint9 check (f1 < 10) not valid not enforced;
+
+-- the invalid state of the child constraint will be ignored here.
+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;
+
create table p1_c2(f1 int constraint inh_check_constraint4 check (f1 < 10)) inherits(p1);
-- but reverse is not allowed
@@ -498,7 +509,7 @@ create table p1_c3() inherits(p1, p1_c1);
-- but not allowed if the child constraint is explicitly asked to be NOT ENFORCED
create table p1_fail(f1 int constraint inh_check_constraint6 check (f1 < 10) not enforced) inherits(p1, p1_c1);
-select conrelid::regclass::text as relname, conname, conislocal, coninhcount, conenforced
+select conrelid::regclass::text as relname, conname, conislocal, coninhcount, conenforced, convalidated
from pg_constraint where conname like 'inh\_check\_constraint%'
order by 1, 2;
--
2.43.5
v16-0008-Add-support-for-NOT-ENFORCED-in-foreign-key-cons.patchapplication/octet-stream; name=v16-0008-Add-support-for-NOT-ENFORCED-in-foreign-key-cons.patchDownload
From 833898bf81ab1090acaefd4f7d6f622957912caa Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 20 Jan 2025 21:45:07 +0530
Subject: [PATCH v16 8/9] Add support for NOT ENFORCED in foreign key
constraints.
Typically, when a foreign key (FK) constraint is created on a table,
action and check triggers are added to maintain data integrity. With
this patch, if a constraint is marked as NOT ENFORCED, integrity
checks are no longer required, making these triggers unnecessary.
Consequently, when creating a NOT ENFORCED FK constraint, triggers
will not be created, and the constraint will be marked as NOT VALID.
Similarly, if an existing FK constraint is changed to NOT ENFORCED,
the associated triggers will be dropped, and the constraint will also
be marked as NOT VALID. Conversely, if a NOT ENFORCED FK constraint is
changed to ENFORCED, the necessary triggers will be created, and the
constraint will remain in the NOT VALID state.
----
NOTE: In this patch, the tryAttachPartitionForeignKey() function will
not merge the constraint if the enforcibility differs. This will be
addressed in the next patch.
----
---
doc/src/sgml/advanced.sgml | 13 +
doc/src/sgml/catalogs.sgml | 2 +-
doc/src/sgml/ddl.sgml | 12 +-
doc/src/sgml/ref/alter_table.sgml | 5 +-
doc/src/sgml/ref/create_table.sgml | 2 +-
src/backend/catalog/pg_constraint.c | 5 +-
src/backend/catalog/sql_features.txt | 2 +-
src/backend/commands/tablecmds.c | 424 +++++++++++++++++-----
src/backend/parser/gram.y | 12 +-
src/backend/parser/parse_utilcmd.c | 6 +-
src/backend/utils/cache/relcache.c | 1 +
src/include/nodes/parsenodes.h | 2 +
src/include/utils/rel.h | 3 +
src/test/regress/expected/constraints.out | 10 +-
src/test/regress/expected/foreign_key.out | 158 +++++++-
src/test/regress/sql/foreign_key.sql | 104 +++++-
16 files changed, 642 insertions(+), 119 deletions(-)
diff --git a/doc/src/sgml/advanced.sgml b/doc/src/sgml/advanced.sgml
index e15a3323dfb..14ed11e1f4d 100644
--- a/doc/src/sgml/advanced.sgml
+++ b/doc/src/sgml/advanced.sgml
@@ -125,6 +125,19 @@ ERROR: insert or update on table "weather" violates foreign key constraint "wea
DETAIL: Key (city)=(Berkeley) is not present in table "cities".
</screen>
</para>
+ <para>
+ If, for some reason, you do not want to enforce this constraint and wish to
+ avoid the error, you can modify the foreign key constraint to <literal>NOT ENFORCED</literal>,
+ or specify it in the <link linkend="sql-createtable"><command>CREATE TABLE</command></link>.
+ </para>
+ <para>
+ Let's change the constraint to <literal>NOT ENFORCED</literal>, and when you
+ attempt to reinsert the same record, it will not result in any error.
+<programlisting>
+ALTER TABLE weather ALTER CONSTRAINT weather_city_fkey NOT ENFORCED;
+INSERT INTO weather VALUES ('Berkeley', 45, 53, 0.0, '1994-11-28');
+</programlisting>
+ </para>
<para>
The behavior of foreign keys can be finely tuned to your
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index fb050635551..014bb815665 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2620,7 +2620,7 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l
</para>
<para>
Is the constraint enforced?
- Currently, can be false only for CHECK constraints
+ Currently, can be false only for foreign keys and CHECK constraints
</para></entry>
</row>
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index ae156b6b1cd..90f4264f6d7 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -1081,11 +1081,13 @@ CREATE TABLE example (
</indexterm>
<para>
- A foreign key constraint specifies that the values in a column (or
- a group of columns) must match the values appearing in some row
- of another table.
- We say this maintains the <firstterm>referential
- integrity</firstterm> between two related tables.
+ A foreign key constraint with <literal>ENFORCED</literal>, the default
+ setting, specifies that the values in a column (or a group of columns)
+ must match the values appearing in some row of another table. We say this
+ maintains the <firstterm>referential integrity</firstterm> between two
+ related tables. If a foreign key constraint is created with <literal>NOT ENFORCED</literal>,
+ the check for aforementioned <firstterm>referential integrity</firstterm>
+ will not be performed.
</para>
<para>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index dceb7a7593c..4174ca94667 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -58,7 +58,7 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable>
ALTER [ COLUMN ] <replaceable class="parameter">column_name</replaceable> SET COMPRESSION <replaceable class="parameter">compression_method</replaceable>
ADD <replaceable class="parameter">table_constraint</replaceable> [ NOT VALID ]
ADD <replaceable class="parameter">table_constraint_using_index</replaceable>
- ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+ ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]
ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> SET [ INHERIT | NO INHERIT ]
VALIDATE CONSTRAINT <replaceable class="parameter">constraint_name</replaceable>
DROP CONSTRAINT [ IF EXISTS ] <replaceable class="parameter">constraint_name</replaceable> [ RESTRICT | CASCADE ]
@@ -589,7 +589,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
This form validates a foreign key or check constraint that was
previously created as <literal>NOT VALID</literal>, by scanning the
table to ensure there are no rows for which the constraint is not
- satisfied. Nothing happens if the constraint is already marked valid.
+ satisfied. If the constraint is not enforced, an error is thrown.
+ Nothing happens if the constraint is already marked valid.
(See <xref linkend="sql-altertable-notes"/> below for an explanation
of the usefulness of this command.)
</para>
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index 0a3e520f215..99bffe6b749 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -1405,7 +1405,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</para>
<para>
- This is currently only supported for <literal>CHECK</literal>
+ This is currently only supported for foreign key and <literal>CHECK</literal>
constraints.
</para>
</listitem>
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index 60bc54a96b6..aed9c0c8d9f 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -100,8 +100,9 @@ CreateConstraintEntry(const char *constraintName,
ObjectAddresses *addrs_auto;
ObjectAddresses *addrs_normal;
- /* Only CHECK constraint can be not enforced */
- Assert(isEnforced || constraintType == CONSTRAINT_CHECK);
+ /* Only CHECK or FOREIGN KEY constraint can be not enforced */
+ Assert(isEnforced || constraintType == CONSTRAINT_CHECK ||
+ constraintType == CONSTRAINT_FOREIGN);
conDesc = table_open(ConstraintRelationId, RowExclusiveLock);
diff --git a/src/backend/catalog/sql_features.txt b/src/backend/catalog/sql_features.txt
index 2f250d2c57b..d49471bbdf2 100644
--- a/src/backend/catalog/sql_features.txt
+++ b/src/backend/catalog/sql_features.txt
@@ -281,7 +281,7 @@ F461 Named character sets NO
F471 Scalar subquery values YES
F481 Expanded NULL predicate YES
F491 Constraint management YES
-F492 Optional table constraint enforcement NO check constraints only
+F492 Optional table constraint enforcement NO foreign key and check constraints only
F501 Features and conformance views YES
F501 Features and conformance views 01 SQL_FEATURES view YES
F501 Features and conformance views 02 SQL_SIZING view YES
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 5b2b284002e..c8f2ba3f473 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -396,8 +396,21 @@ static bool ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdc
Relation tgrel, const Oid fkrelid,
const Oid pkrelid, HeapTuple contuple,
bool recurse, List **otherrelids,
- LOCKMODE lockmode);
-static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
+ LOCKMODE lockmode,
+ Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger);
+static void ATExecAlterConstrEnforceability(List **wqueue, ATAlterConstraint *cmdcon,
+ Relation conrel, Relation tgrel,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode,
+ Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger);
+static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Oid fkrel,
bool deferrable, bool initdeferred,
List **otherrelids);
static void ATExecAlterChildConstr(List **wqueue, ATAlterConstraint *cmdcon,
@@ -10551,7 +10564,7 @@ addFkConstraint(addFkConstraintSides fkside,
CONSTRAINT_FOREIGN,
fkconstraint->deferrable,
fkconstraint->initdeferred,
- true, /* Is Enforced */
+ fkconstraint->is_enforced,
fkconstraint->initially_valid,
parentConstr,
RelationGetRelid(rel),
@@ -10669,21 +10682,23 @@ addFkRecurseReferenced(Constraint *fkconstraint, Relation rel,
Oid parentDelTrigger, Oid parentUpdTrigger,
bool with_period)
{
- Oid deleteTriggerOid,
- updateTriggerOid;
+ Oid deleteTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
Assert(CheckRelationLockedByMe(pkrel, ShareRowExclusiveLock, true));
Assert(CheckRelationLockedByMe(rel, ShareRowExclusiveLock, true));
/*
- * Create the action triggers that enforce the constraint.
+ * Create action triggers to enforce the constraint, or skip them if the
+ * constraint is not enforced.
*/
- createForeignKeyActionTriggers(RelationGetRelid(rel),
- RelationGetRelid(pkrel),
- fkconstraint,
- parentConstr, indexOid,
- parentDelTrigger, parentUpdTrigger,
- &deleteTriggerOid, &updateTriggerOid);
+ if (fkconstraint->is_enforced)
+ createForeignKeyActionTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
+ fkconstraint,
+ parentConstr, indexOid,
+ parentDelTrigger, parentUpdTrigger,
+ &deleteTriggerOid, &updateTriggerOid);
/*
* If the referenced table is partitioned, recurse on ourselves to handle
@@ -10804,8 +10819,8 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
Oid parentInsTrigger, Oid parentUpdTrigger,
bool with_period)
{
- Oid insertTriggerOid,
- updateTriggerOid;
+ Oid insertTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
Assert(OidIsValid(parentConstr));
Assert(CheckRelationLockedByMe(rel, ShareRowExclusiveLock, true));
@@ -10817,29 +10832,32 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
errmsg("foreign key constraints are not supported on foreign tables")));
/*
- * Add the check triggers to it and, if necessary, schedule it to be
- * checked in Phase 3.
+ * Add check triggers if the constraint is enforced, and if needed,
+ * schedule them to be checked in Phase 3.
*
* If the relation is partitioned, drill down to do it to its partitions.
*/
- createForeignKeyCheckTriggers(RelationGetRelid(rel),
- RelationGetRelid(pkrel),
- fkconstraint,
- parentConstr,
- indexOid,
- parentInsTrigger, parentUpdTrigger,
- &insertTriggerOid, &updateTriggerOid);
+ if (fkconstraint->is_enforced)
+ createForeignKeyCheckTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
+ fkconstraint,
+ parentConstr,
+ indexOid,
+ parentInsTrigger, parentUpdTrigger,
+ &insertTriggerOid, &updateTriggerOid);
if (rel->rd_rel->relkind == RELKIND_RELATION)
{
/*
* Tell Phase 3 to check that the constraint is satisfied by existing
- * rows. We can skip this during table creation, when requested
- * explicitly by specifying NOT VALID in an ADD FOREIGN KEY command,
- * and when we're recreating a constraint following a SET DATA TYPE
- * operation that did not impugn its validity.
+ * rows. We can skip this during table creation, when constraint is
+ * specified as NOT ENFORCED, or when requested explicitly by
+ * specifying NOT VALID in an ADD FOREIGN KEY command, and when we're
+ * recreating a constraint following a SET DATA TYPE operation that
+ * did not impugn its validity.
*/
- if (wqueue && !old_check_ok && !fkconstraint->skip_validation)
+ if (wqueue && !old_check_ok && !fkconstraint->skip_validation &&
+ fkconstraint->is_enforced)
{
NewConstraint *newcon;
AlteredTableInfo *tab;
@@ -11070,8 +11088,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
AttrNumber confdelsetcols[INDEX_MAX_KEYS];
Constraint *fkconstraint;
ObjectAddress address;
- Oid deleteTriggerOid,
- updateTriggerOid;
+ Oid deleteTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(constrOid));
if (!HeapTupleIsValid(tuple))
@@ -11131,6 +11149,7 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
fkconstraint->fk_del_set_cols = NIL;
fkconstraint->old_conpfeqop = NIL;
fkconstraint->old_pktable_oid = InvalidOid;
+ fkconstraint->is_enforced = constrForm->conenforced;
fkconstraint->skip_validation = false;
fkconstraint->initially_valid = true;
@@ -11160,9 +11179,10 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
* parent OIDs for similar triggers that will be created on the
* partition in addFkRecurseReferenced().
*/
- GetForeignKeyActionTriggers(trigrel, constrOid,
- constrForm->confrelid, constrForm->conrelid,
- &deleteTriggerOid, &updateTriggerOid);
+ if (constrForm->conenforced)
+ GetForeignKeyActionTriggers(trigrel, constrOid,
+ constrForm->confrelid, constrForm->conrelid,
+ &deleteTriggerOid, &updateTriggerOid);
/* Add this constraint ... */
address = addFkConstraint(addFkReferencedSide,
@@ -11295,8 +11315,8 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
Oid indexOid;
ObjectAddress address;
ListCell *lc;
- Oid insertTriggerOid,
- updateTriggerOid;
+ Oid insertTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
bool with_period;
tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(parentConstrOid));
@@ -11328,17 +11348,18 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
mapped_conkey[i] = attmap->attnums[conkey[i] - 1];
/*
- * Get the "check" triggers belonging to the constraint to pass as
- * parent OIDs for similar triggers that will be created on the
- * partition in addFkRecurseReferencing(). They are also passed to
- * tryAttachPartitionForeignKey() below to simply assign as parents to
- * the partition's existing "check" triggers, that is, if the
- * corresponding constraints is deemed attachable to the parent
- * constraint.
+ * Get the "check" triggers belonging to the constraint, if it is
+ * enforced, to pass as parent OIDs for similar triggers that will be
+ * created on the partition in addFkRecurseReferencing(). They are
+ * also passed to tryAttachPartitionForeignKey() below to simply
+ * assign as parents to the partition's existing "check" triggers,
+ * that is, if the corresponding constraints is deemed attachable to
+ * the parent constraint.
*/
- GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
- constrForm->confrelid, constrForm->conrelid,
- &insertTriggerOid, &updateTriggerOid);
+ if (constrForm->conenforced)
+ GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
+ constrForm->confrelid, constrForm->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
/*
* Before creating a new constraint, see whether any existing FKs are
@@ -11391,6 +11412,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
fkconstraint->fk_del_set_cols = NIL;
fkconstraint->old_conpfeqop = NIL;
fkconstraint->old_pktable_oid = InvalidOid;
+ fkconstraint->is_enforced = constrForm->conenforced;
fkconstraint->skip_validation = false;
fkconstraint->initially_valid = constrForm->convalidated;
for (int i = 0; i < numfks; i++)
@@ -11508,6 +11530,7 @@ tryAttachPartitionForeignKey(List **wqueue,
if (OidIsValid(partConstr->conparentid) ||
partConstr->condeferrable != parentConstr->condeferrable ||
partConstr->condeferred != parentConstr->condeferred ||
+ partConstr->conenforced != parentConstr->conenforced ||
partConstr->confupdtype != parentConstr->confupdtype ||
partConstr->confdeltype != parentConstr->confdeltype ||
partConstr->confmatchtype != parentConstr->confmatchtype)
@@ -11551,8 +11574,7 @@ AttachPartitionForeignKey(List **wqueue,
bool queueValidation;
Oid partConstrFrelid;
Oid partConstrRelid;
- Oid insertTriggerOid,
- updateTriggerOid;
+ bool parentConstrIsEnforced;
/* Fetch the parent constraint tuple */
parentConstrTup = SearchSysCache1(CONSTROID,
@@ -11560,6 +11582,7 @@ AttachPartitionForeignKey(List **wqueue,
if (!HeapTupleIsValid(parentConstrTup))
elog(ERROR, "cache lookup failed for constraint %u", parentConstrOid);
parentConstr = (Form_pg_constraint) GETSTRUCT(parentConstrTup);
+ parentConstrIsEnforced = parentConstr->conenforced;
/* Fetch the child constraint tuple */
partcontup = SearchSysCache1(CONSTROID,
@@ -11609,17 +11632,24 @@ AttachPartitionForeignKey(List **wqueue,
/*
* Like the constraint, attach partition's "check" triggers to the
- * corresponding parent triggers.
+ * corresponding parent triggers if the constraint is enforced.
+ * Non-enforced constraints do not have these triggers.
*/
- GetForeignKeyCheckTriggers(trigrel,
- partConstrOid, partConstrFrelid, partConstrRelid,
- &insertTriggerOid, &updateTriggerOid);
- Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
- TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
- RelationGetRelid(partition));
- Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
- TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
- RelationGetRelid(partition));
+ if (parentConstrIsEnforced)
+ {
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
+ GetForeignKeyCheckTriggers(trigrel,
+ partConstrOid, partConstrFrelid, partConstrRelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
+ RelationGetRelid(partition));
+ Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
+ RelationGetRelid(partition));
+ }
/*
* We updated this pg_constraint row above to set its parent; validating
@@ -11733,6 +11763,10 @@ RemoveInheritedConstraint(Relation conrel, Relation trigrel, Oid conoid,
*
* The subroutine for tryAttachPartitionForeignKey handles the deletion of
* action triggers for the foreign key constraint.
+ *
+ * If valid confrelid and conrelid values are not provided, the respective
+ * trigger check will be skipped, and the trigger will be considered for
+ * removal.
*/
static void
DropForeignKeyConstraintTriggers(Relation trigrel, Oid conoid, Oid confrelid,
@@ -11753,10 +11787,27 @@ DropForeignKeyConstraintTriggers(Relation trigrel, Oid conoid, Oid confrelid,
Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
ObjectAddress trigger;
- if (trgform->tgconstrrelid != conrelid)
+ /* Invalid if trigger is not for a referential integrity constraint */
+ if (!OidIsValid(trgform->tgconstrrelid))
continue;
- if (trgform->tgrelid != confrelid)
+ if (OidIsValid(conrelid) && trgform->tgconstrrelid != conrelid)
continue;
+ if (OidIsValid(confrelid) && trgform->tgrelid != confrelid)
+ continue;
+
+ /* We should be droping trigger related to foreign key constraint */
+ Assert(trgform->tgfoid == F_RI_FKEY_CHECK_INS ||
+ trgform->tgfoid == F_RI_FKEY_CHECK_UPD ||
+ trgform->tgfoid == F_RI_FKEY_CASCADE_DEL ||
+ trgform->tgfoid == F_RI_FKEY_CASCADE_UPD ||
+ trgform->tgfoid == F_RI_FKEY_RESTRICT_DEL ||
+ trgform->tgfoid == F_RI_FKEY_RESTRICT_UPD ||
+ trgform->tgfoid == F_RI_FKEY_SETNULL_DEL ||
+ trgform->tgfoid == F_RI_FKEY_SETNULL_UPD ||
+ trgform->tgfoid == F_RI_FKEY_SETDEFAULT_DEL ||
+ trgform->tgfoid == F_RI_FKEY_SETDEFAULT_UPD ||
+ trgform->tgfoid == F_RI_FKEY_NOACTION_DEL ||
+ trgform->tgfoid == F_RI_FKEY_NOACTION_UPD);
/*
* The constraint is originally set up to contain this trigger as an
@@ -11970,6 +12021,12 @@ ATExecAlterConstraint(List **wqueue, Relation rel, ATAlterConstraint *cmdcon,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("constraint \"%s\" of relation \"%s\" is not a foreign key constraint",
cmdcon->conname, RelationGetRelationName(rel))));
+ if (cmdcon->alterEnforceability && currcon->contype != CONSTRAINT_FOREIGN)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot alter enforceability of constraint \"%s\" of relation \"%s\"",
+ cmdcon->conname, RelationGetRelationName(rel)),
+ errdetail("Enforceability can only be altered for foreign key constraints.")));
if (cmdcon->alterInheritability &&
currcon->contype != CONSTRAINT_NOTNULL)
ereport(ERROR,
@@ -12037,7 +12094,9 @@ ATExecAlterConstraint(List **wqueue, Relation rel, ATAlterConstraint *cmdcon,
*/
if (ATExecAlterConstraintInternal(wqueue, cmdcon, conrel, tgrel,
currcon->conrelid, currcon->confrelid,
- contuple, recurse, &otherrelids, lockmode))
+ contuple, recurse, &otherrelids, lockmode,
+ InvalidOid, InvalidOid, InvalidOid,
+ InvalidOid))
ObjectAddressSet(address, ConstraintRelationId, currcon->oid);
/*
@@ -12072,7 +12131,11 @@ ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon,
Relation conrel, Relation tgrel,
const Oid fkrelid, const Oid pkrelid,
HeapTuple contuple, bool recurse,
- List **otherrelids, LOCKMODE lockmode)
+ List **otherrelids, LOCKMODE lockmode,
+ Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger)
{
Form_pg_constraint currcon;
Oid refrelid = InvalidOid;
@@ -12094,17 +12157,27 @@ ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon,
* If called to modify a constraint that's already in the desired state,
* silently do nothing.
*/
- if (cmdcon->alterDeferrability &&
- (currcon->condeferrable != cmdcon->deferrable ||
- currcon->condeferred != cmdcon->initdeferred))
+ if ((cmdcon->alterEnforceability &&
+ currcon->conenforced != cmdcon->is_enforced) ||
+ (cmdcon->alterDeferrability &&
+ (currcon->condeferrable != cmdcon->deferrable ||
+ currcon->condeferred != cmdcon->initdeferred)))
{
HeapTuple copyTuple;
Form_pg_constraint copy_con;
copyTuple = heap_copytuple(contuple);
copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
- copy_con->condeferrable = cmdcon->deferrable;
- copy_con->condeferred = cmdcon->initdeferred;
+
+ if (cmdcon->alterEnforceability)
+ copy_con->conenforced = cmdcon->is_enforced;
+
+ if (cmdcon->alterDeferrability)
+ {
+ copy_con->condeferrable = cmdcon->deferrable;
+ copy_con->condeferred = cmdcon->initdeferred;
+ }
+
CatalogTupleUpdate(conrel, ©Tuple->t_self, copyTuple);
InvokeObjectPostAlterHook(ConstraintRelationId, currcon->oid, 0);
@@ -12116,12 +12189,68 @@ ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon,
CacheInvalidateRelcache(rel);
/*
- * Now we need to update the multiple entries in pg_trigger that
- * implement the constraint.
+ * Note that even if deferrability is requested to be altered along with
+ * enforceability, we don't need to explicitly update multiple entries in
+ * pg_trigger related to deferrability.
+ *
+ * Modifying enforceability involves either creating or dropping the trigger,
+ * during which the deferrability setting will be adjusted automatically.
*/
- AlterConstrTriggerDeferrability(currcon->oid, tgrel, rel,
- cmdcon->deferrable,
- cmdcon->initdeferred, otherrelids);
+ if (cmdcon->alterEnforceability)
+ {
+ ATExecAlterConstrEnforceability(wqueue, cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, contuple, otherrelids, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
+ /*
+ * We don't need further recursion, as altering enforceability is
+ * already handled with the necessary adjustments.
+ */
+ recurse = false;
+
+ /*
+ * Tell Phase 3 to check that the constraint is satisfied by
+ * existing rows, but skip this step if the constraint is NOT
+ * VALID.
+ */
+ if (cmdcon->is_enforced && currcon->convalidated &&
+ rel->rd_rel->relkind == RELKIND_RELATION)
+ {
+ AlteredTableInfo *tab;
+ NewConstraint *newcon;
+ Constraint *fkconstraint;
+
+ /* Queue validation for phase 3 */
+ fkconstraint = makeNode(Constraint);
+ /* for now this is all we need */
+ fkconstraint->conname = pstrdup(NameStr(currcon->conname));
+
+ newcon = (NewConstraint *) palloc0(sizeof(NewConstraint));
+ newcon->name = fkconstraint->conname;
+ newcon->contype = CONSTR_FOREIGN;
+ newcon->refrelid = currcon->confrelid;
+ newcon->refindid = currcon->conindid;
+ newcon->conid = currcon->oid;
+ newcon->qual = (Node *) fkconstraint;
+
+ /* Find or create work queue entry for this table */
+ tab = ATGetQueueEntry(wqueue, rel);
+ tab->constraints = lappend(tab->constraints, newcon);
+ }
+ }
+ else
+ {
+ /*
+ * Update the multiple entries in pg_trigger that implement the
+ * constraint.
+ */
+ AlterConstrTriggerDeferrability(currcon->oid, tgrel,
+ RelationGetRelid(rel),
+ cmdcon->deferrable,
+ cmdcon->initdeferred, otherrelids);
+ }
}
/*
@@ -12208,6 +12337,121 @@ ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon,
return changed;
}
+/*
+ * A subroutine of ATExecAlterConstraintInternal that updates the
+ * enforceability of a foreign key constraint. Depending on whether the
+ * constraint is being set to enforced or not enforced, it creates or drops the
+ * trigger accordingly.
+ *
+ * The arguments to this function have the same meaning as the arguments to
+ * ATExecAlterConstraintInternal.
+ */
+static void
+ATExecAlterConstrEnforceability(List **wqueue, ATAlterConstraint *cmdcon,
+ Relation conrel, Relation tgrel,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, List **otherrelids,
+ LOCKMODE lockmode,
+ Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger)
+{
+ Form_pg_constraint currcon;
+ Oid conoid;
+
+ currcon = (Form_pg_constraint) GETSTRUCT(contuple);
+ conoid = currcon->oid;
+
+ /* Drop triggers */
+ if (!cmdcon->is_enforced)
+ {
+ /*
+ * When setting a constraint to NOT ENFORCED, the constraint triggers
+ * need to be dropped. Therefore, we must process the child relations
+ * first, followed by the parent, to account for dependencies.
+ */
+ if (get_rel_relkind(currcon->conrelid) == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE)
+ ATExecAlterChildConstr(wqueue, cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, contuple, true, otherrelids,
+ lockmode);
+
+ /* Drop all the triggers */
+ DropForeignKeyConstraintTriggers(tgrel, conoid, InvalidOid, InvalidOid);
+ }
+ else /* Create triggers */
+ {
+ Oid ReferencedDelTriggerOid = InvalidOid,
+ ReferencedUpdTriggerOid = InvalidOid,
+ ReferencingInsTriggerOid = InvalidOid,
+ ReferencingUpdTriggerOid = InvalidOid;
+
+ /* Prepare the minimal information required for trigger creation. */
+ Constraint *fkconstraint = makeNode(Constraint);
+
+ fkconstraint->fk_matchtype = currcon->confmatchtype;
+ fkconstraint->fk_upd_action = currcon->confupdtype;
+ fkconstraint->fk_del_action = currcon->confdeltype;
+
+ /* Create referenced triggers */
+ if (currcon->conrelid == fkrelid)
+ createForeignKeyActionTriggers(currcon->conrelid,
+ currcon->confrelid,
+ fkconstraint,
+ conoid,
+ currcon->conindid,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ &ReferencedDelTriggerOid,
+ &ReferencedUpdTriggerOid);
+
+ /* Create referencing triggers */
+ if (currcon->confrelid == pkrelid)
+ createForeignKeyCheckTriggers(currcon->conrelid,
+ pkrelid,
+ fkconstraint,
+ conoid,
+ currcon->conindid,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger,
+ &ReferencingInsTriggerOid,
+ &ReferencingUpdTriggerOid);
+
+ /*
+ * If the table at either end of the constraint is partitioned, we
+ * need to recurse and create triggers for each constraint that is a
+ * child of this one.
+ */
+ if (get_rel_relkind(currcon->conrelid) == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE)
+ {
+ ScanKeyData pkey;
+ SysScanDesc pscan;
+ HeapTuple childtup;
+
+ ScanKeyInit(&pkey,
+ Anum_pg_constraint_conparentid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+
+ pscan = systable_beginscan(conrel, ConstraintParentIndexId,
+ true, NULL, 1, &pkey);
+
+ while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
+ ATExecAlterConstraintInternal(wqueue, cmdcon, conrel, tgrel,
+ fkrelid, pkrelid, childtup, true,
+ otherrelids, lockmode,
+ ReferencedDelTriggerOid,
+ ReferencedUpdTriggerOid,
+ ReferencingInsTriggerOid,
+ ReferencingUpdTriggerOid);
+
+ systable_endscan(pscan);
+ }
+ }
+}
+
/*
* A subroutine of ATExecAlterConstrDeferrability that updated constraint
* trigger's deferrability.
@@ -12216,7 +12460,7 @@ ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon,
* ATExecAlterConstrDeferrability.
*/
static void
-AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
+AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Oid fkrel,
bool deferrable, bool initdeferred,
List **otherrelids)
{
@@ -12242,7 +12486,7 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
* other rels that don't have a trigger whose properties change, but
* let's be conservative.)
*/
- if (tgform->tgrelid != RelationGetRelid(rel))
+ if (tgform->tgrelid != fkrel)
*otherrelids = list_append_unique_oid(*otherrelids,
tgform->tgrelid);
@@ -12310,7 +12554,8 @@ ATExecAlterChildConstr(List **wqueue, ATAlterConstraint *cmdcon,
while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
ATExecAlterConstraintInternal(wqueue, cmdcon, conrel, tgrel, fkrelid,
pkrelid, childtup, recurse, otherrelids,
- lockmode);
+ lockmode, InvalidOid, InvalidOid,
+ InvalidOid, InvalidOid);
systable_endscan(pscan);
}
@@ -20380,8 +20625,6 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
Form_pg_constraint conform;
- Oid insertTriggerOid,
- updateTriggerOid;
contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
if (!HeapTupleIsValid(contup))
@@ -20408,17 +20651,25 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
/*
* Also, look up the partition's "check" triggers corresponding to the
- * constraint being detached and detach them from the parent triggers.
+ * enforced constraint being detached and detach them from the parent
+ * triggers. Non-enforced constraints do not have these triggers;
+ * therefore, this step is not needed.
*/
- GetForeignKeyCheckTriggers(trigrel,
- fk->conoid, fk->confrelid, fk->conrelid,
- &insertTriggerOid, &updateTriggerOid);
- Assert(OidIsValid(insertTriggerOid));
- TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
- RelationGetRelid(partRel));
- Assert(OidIsValid(updateTriggerOid));
- TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
- RelationGetRelid(partRel));
+ if (fk->conenforced)
+ {
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ Assert(OidIsValid(updateTriggerOid));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ }
/*
* Lastly, create the action triggers on the referenced table, using
@@ -20458,6 +20709,7 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
fkconstraint->conname = pstrdup(NameStr(conform->conname));
fkconstraint->deferrable = conform->condeferrable;
fkconstraint->initdeferred = conform->condeferred;
+ fkconstraint->is_enforced = conform->conenforced;
fkconstraint->skip_validation = true;
fkconstraint->initially_valid = true;
/* a few irrelevant fields omitted here */
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 042fbbdc078..a1fabea5d57 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -2662,11 +2662,15 @@ alter_table_cmd:
n->subtype = AT_AlterConstraint;
n->def = (Node *) c;
c->conname = $3;
- c->alterDeferrability = true;
+ c->alterEnforceability =
+ ($4 & (CAS_NOT_ENFORCED | CAS_ENFORCED)) != 0;
+ c->alterDeferrability =
+ ($4 & (CAS_NOT_DEFERRABLE | CAS_DEFERRABLE |
+ CAS_INITIALLY_IMMEDIATE | CAS_INITIALLY_IMMEDIATE)) != 0;
processCASbits($4, @4, "FOREIGN KEY",
&c->deferrable,
&c->initdeferred,
- NULL, NULL, NULL, yyscanner);
+ &c->is_enforced, NULL, NULL, yyscanner);
$$ = (Node *) n;
}
/* ALTER TABLE <name> ALTER CONSTRAINT SET INHERIT */
@@ -4344,8 +4348,8 @@ ConstraintElem:
n->fk_del_set_cols = ($11)->deleteAction->cols;
processCASbits($12, @12, "FOREIGN KEY",
&n->deferrable, &n->initdeferred,
- NULL, &n->skip_validation, NULL,
- yyscanner);
+ &n->is_enforced, &n->skip_validation,
+ NULL, yyscanner);
n->initially_valid = !n->skip_validation;
$$ = (Node *) n;
}
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 37becb62356..c579e56d967 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -3979,7 +3979,8 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
case CONSTR_ATTR_ENFORCED:
if (lastprimarycon == NULL ||
- lastprimarycon->contype != CONSTR_CHECK)
+ (lastprimarycon->contype != CONSTR_CHECK &&
+ lastprimarycon->contype != CONSTR_FOREIGN))
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("misplaced ENFORCED clause"),
@@ -3995,7 +3996,8 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
case CONSTR_ATTR_NOT_ENFORCED:
if (lastprimarycon == NULL ||
- lastprimarycon->contype != CONSTR_CHECK)
+ (lastprimarycon->contype != CONSTR_CHECK &&
+ lastprimarycon->contype != CONSTR_FOREIGN))
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("misplaced NOT ENFORCED clause"),
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 1a01f18f0d3..f4ff15e4e79 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4694,6 +4694,7 @@ RelationGetFKeyList(Relation relation)
info->conoid = constraint->oid;
info->conrelid = constraint->conrelid;
info->confrelid = constraint->confrelid;
+ info->conenforced = constraint->conenforced;
DeconstructFkConstraintRow(htup, &info->nkeys,
info->conkey,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 23c9e3c5abf..9e64e18b501 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2490,6 +2490,8 @@ typedef struct ATAlterConstraint
{
NodeTag type;
char *conname; /* Constraint name */
+ bool alterEnforceability; /* changing enforceability properties? */
+ bool is_enforced; /* ENFORCED? */
bool alterDeferrability; /* changing deferrability properties? */
bool deferrable; /* DEFERRABLE? */
bool initdeferred; /* INITIALLY DEFERRED? */
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index db3e504c3d2..dba0fb70146 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -284,6 +284,9 @@ typedef struct ForeignKeyCacheInfo
/* number of columns in the foreign key */
int nkeys;
+ /* Is enforced ? */
+ bool conenforced;
+
/*
* these arrays each have nkeys valid entries:
*/
diff --git a/src/test/regress/expected/constraints.out b/src/test/regress/expected/constraints.out
index 4f39100fcdf..b018e711123 100644
--- a/src/test/regress/expected/constraints.out
+++ b/src/test/regress/expected/constraints.out
@@ -745,13 +745,11 @@ ERROR: misplaced NOT ENFORCED clause
LINE 1: CREATE TABLE UNIQUE_NOTEN_TBL(i int UNIQUE NOT ENFORCED);
^
ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
-ERROR: FOREIGN KEY constraints cannot be marked ENFORCED
-LINE 1: ...TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
- ^
+ERROR: cannot alter enforceability of constraint "unique_tbl_i_key" of relation "unique_tbl"
+DETAIL: Enforceability can only be altered for foreign key constraints.
ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORCED;
-ERROR: FOREIGN KEY constraints cannot be marked NOT ENFORCED
-LINE 1: ...ABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORC...
- ^
+ERROR: cannot alter enforceability of constraint "unique_tbl_i_key" of relation "unique_tbl"
+DETAIL: Enforceability can only be altered for foreign key constraints.
DROP TABLE unique_tbl;
--
-- EXCLUDE constraints
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 6a3374d5152..d9dea078dca 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -1,12 +1,43 @@
--
-- FOREIGN KEY
--
--- MATCH FULL
+-- NOT ENFORCED
--
-- First test, check and cascade
--
CREATE TABLE PKTABLE ( ptest1 int PRIMARY KEY, ptest2 text );
-CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE, ftest2 int );
+CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE NOT ENFORCED,
+ ftest2 int );
+-- Inserting into the foreign key table will not result in an error, even if
+-- there is no matching key in the referenced table.
+INSERT INTO FKTABLE VALUES (1, 2);
+INSERT INTO FKTABLE VALUES (2, 3);
+-- Check FKTABLE
+SELECT * FROM FKTABLE;
+ ftest1 | ftest2
+--------+--------
+ 1 | 2
+ 2 | 3
+(2 rows)
+
+-- Reverting it back to ENFORCED will result in failure because constraint validation will be triggered,
+-- as it was previously in a valid state.
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_fkey"
+DETAIL: Key (ftest1)=(1) is not present in table "pktable".
+-- Remove any existing rows that violate the constraint, then attempt to change
+-- it.
+TRUNCATE FKTABLE;
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+-- Any further inserts will fail due to the enforcement.
+INSERT INTO FKTABLE VALUES (3, 4);
+ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_fkey"
+DETAIL: Key (ftest1)=(3) is not present in table "pktable".
+--
+-- MATCH FULL
+--
+-- First test, check and cascade
+--
-- Insert test data into PKTABLE
INSERT INTO PKTABLE VALUES (1, 'Test1');
INSERT INTO PKTABLE VALUES (2, 'Test2');
@@ -351,6 +382,41 @@ INSERT INTO FKTABLE VALUES (1, NULL);
ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL;
ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_ftest2_fkey"
DETAIL: MATCH FULL does not allow mixing of null and nonnull key values.
+-- Modifying other attributes of a constraint should not affect its enforceability, and vice versa
+ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE NOT VALID NOT ENFORCED;
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_ftest2_fkey DEFERRABLE INITIALLY DEFERRED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fktable_ftest1_ftest2_fkey';
+ condeferrable | condeferred | conenforced | convalidated
+---------------+-------------+-------------+--------------
+ t | t | f | f
+(1 row)
+
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_ftest2_fkey NOT ENFORCED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fktable_ftest1_ftest2_fkey';
+ condeferrable | condeferred | conenforced | convalidated
+---------------+-------------+-------------+--------------
+ t | t | f | f
+(1 row)
+
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_ftest2_fkey ENFORCED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fktable_ftest1_ftest2_fkey';
+ condeferrable | condeferred | conenforced | convalidated
+---------------+-------------+-------------+--------------
+ t | t | t | f
+(1 row)
+
+-- Can change enforceability and deferrability together
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_ftest2_fkey NOT ENFORCED NOT DEFERRABLE;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fktable_ftest1_ftest2_fkey';
+ condeferrable | condeferred | conenforced | convalidated
+---------------+-------------+-------------+--------------
+ f | f | f | f
+(1 row)
+
DROP TABLE FKTABLE;
DROP TABLE PKTABLE;
-- MATCH SIMPLE
@@ -1276,6 +1342,13 @@ INSERT INTO fktable VALUES (0, 20);
ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_fk_fkey"
DETAIL: Key (fk)=(20) is not present in table "pktable".
COMMIT;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT ENFORCED;
+BEGIN;
+-- doesn't match FK, no error.
+UPDATE pktable SET id = 10 WHERE id = 5;
+-- doesn't match PK, no error.
+INSERT INTO fktable VALUES (0, 20);
+ROLLBACK;
-- try additional syntax
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE;
-- illegal options
@@ -1291,6 +1364,14 @@ ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT VALID;
ERROR: FOREIGN KEY constraints cannot be marked NOT VALID
LINE 1: ...ER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT VALID;
^
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORCED;
+ERROR: conflicting constraint properties
+LINE 1: ...fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORC...
+ ^
+CREATE TEMP TABLE fktable2 (fk int references pktable ENFORCED NOT ENFORCED);
+ERROR: multiple ENFORCED/NOT ENFORCED clauses not allowed
+LINE 1: ...ABLE fktable2 (fk int references pktable ENFORCED NOT ENFORC...
+ ^
-- test order of firing of FK triggers when several RI-induced changes need to
-- be made to the same row. This was broken by subtransaction-related
-- changes in 8.0.
@@ -1588,10 +1669,12 @@ ALTER TABLE fk_partitioned_fk DROP COLUMN fdrop1;
CREATE TABLE fk_partitioned_fk_1 (fdrop1 int, fdrop2 int, a int, fdrop3 int, b int);
ALTER TABLE fk_partitioned_fk_1 DROP COLUMN fdrop1, DROP COLUMN fdrop2, DROP COLUMN fdrop3;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FROM (0,0) TO (1000,1000);
-ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk;
+ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
+ALTER TABLE fk_partitioned_fk_2 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
CREATE TABLE fk_partitioned_fk_3 (fdrop1 int, fdrop2 int, fdrop3 int, fdrop4 int, b int, a int)
PARTITION BY HASH (a);
ALTER TABLE fk_partitioned_fk_3 DROP COLUMN fdrop1, DROP COLUMN fdrop2,
@@ -1614,10 +1697,10 @@ INSERT INTO fk_partitioned_fk_1 (a,b) VALUES (500, 501);
ERROR: insert or update on table "fk_partitioned_fk_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
DETAIL: Key (a, b)=(500, 501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (1500, 1501);
-ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_2_a_b_fkey"
DETAIL: Key (a, b)=(1500, 1501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk_2 (a,b) VALUES (1500, 1501);
-ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_2_a_b_fkey"
DETAIL: Key (a, b)=(1500, 1501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (2500, 2502);
ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
@@ -1667,6 +1750,37 @@ Indexes:
Referenced by:
TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b)
+-- Check the exsting FK trigger
+CREATE TEMP TABLE tmp_trg_info AS SELECT tgtype, tgrelid::regclass, tgconstraint
+FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+SELECT count(1) FROM tmp_trg_info;
+ count
+-------
+ 14
+(1 row)
+
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+-- No triggers
+SELECT count(1) FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+ count
+-------
+ 0
+(1 row)
+
+-- Changing it back to ENFORCED will recreate the necessary triggers.
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+-- Should be exactly the same number of triggers found as before
+SELECT COUNT(1) FROM pg_trigger pgt JOIN tmp_trg_info tt
+ON (pgt.tgtype = tt.tgtype AND pgt.tgrelid = tt.tgrelid AND pgt.tgconstraint = tt.tgconstraint);
+ count
+-------
+ 14
+(1 row)
+
ALTER TABLE fk_partitioned_fk DROP CONSTRAINT fk_partitioned_fk_a_b_fkey;
-- done.
DROP TABLE fk_notpartitioned_pk, fk_partitioned_fk;
@@ -1964,6 +2078,40 @@ Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
Foreign-key constraints:
TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
+DROP TABLE fk_partitioned_fk_2;
+CREATE TABLE fk_partitioned_fk_2 (b int, a int,
+ FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
+BEGIN;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should be having two constraints
+\d fk_partitioned_fk_2
+ Table "public.fk_partitioned_fk_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ b | integer | | |
+ a | integer | | |
+Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
+Foreign-key constraints:
+ "fk_partitioned_fk_2_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED
+ TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
+
+DROP TABLE fk_partitioned_fk_2;
+ROLLBACK;
+BEGIN;
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have only one constraint
+\d fk_partitioned_fk_2
+ Table "public.fk_partitioned_fk_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ b | integer | | |
+ a | integer | | |
+Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
+Foreign-key constraints:
+ TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED
+
+ROLLBACK;
DROP TABLE fk_partitioned_fk_2;
CREATE TABLE fk_partitioned_fk_4 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE) PARTITION BY RANGE (b, a);
CREATE TABLE fk_partitioned_fk_4_1 PARTITION OF fk_partitioned_fk_4 FOR VALUES FROM (1,1) TO (100,100);
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index 44945b0453a..70991a6aabc 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -2,13 +2,39 @@
-- FOREIGN KEY
--
--- MATCH FULL
+-- NOT ENFORCED
--
-- First test, check and cascade
--
CREATE TABLE PKTABLE ( ptest1 int PRIMARY KEY, ptest2 text );
-CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE, ftest2 int );
+CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE NOT ENFORCED,
+ ftest2 int );
+-- Inserting into the foreign key table will not result in an error, even if
+-- there is no matching key in the referenced table.
+INSERT INTO FKTABLE VALUES (1, 2);
+INSERT INTO FKTABLE VALUES (2, 3);
+
+-- Check FKTABLE
+SELECT * FROM FKTABLE;
+
+-- Reverting it back to ENFORCED will result in failure because constraint validation will be triggered,
+-- as it was previously in a valid state.
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+
+-- Remove any existing rows that violate the constraint, then attempt to change
+-- it.
+TRUNCATE FKTABLE;
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+
+-- Any further inserts will fail due to the enforcement.
+INSERT INTO FKTABLE VALUES (3, 4);
+
+--
+-- MATCH FULL
+--
+-- First test, check and cascade
+--
-- Insert test data into PKTABLE
INSERT INTO PKTABLE VALUES (1, 'Test1');
INSERT INTO PKTABLE VALUES (2, 'Test2');
@@ -230,6 +256,25 @@ INSERT INTO FKTABLE VALUES (1, NULL);
ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL;
+-- Modifying other attributes of a constraint should not affect its enforceability, and vice versa
+ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE NOT VALID NOT ENFORCED;
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_ftest2_fkey DEFERRABLE INITIALLY DEFERRED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fktable_ftest1_ftest2_fkey';
+
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_ftest2_fkey NOT ENFORCED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fktable_ftest1_ftest2_fkey';
+
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_ftest2_fkey ENFORCED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fktable_ftest1_ftest2_fkey';
+
+-- Can change enforceability and deferrability together
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_ftest2_fkey NOT ENFORCED NOT DEFERRABLE;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fktable_ftest1_ftest2_fkey';
+
DROP TABLE FKTABLE;
DROP TABLE PKTABLE;
@@ -968,12 +1013,25 @@ INSERT INTO fktable VALUES (0, 20);
COMMIT;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT ENFORCED;
+
+BEGIN;
+
+-- doesn't match FK, no error.
+UPDATE pktable SET id = 10 WHERE id = 5;
+-- doesn't match PK, no error.
+INSERT INTO fktable VALUES (0, 20);
+
+ROLLBACK;
+
-- try additional syntax
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE;
-- illegal options
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE INITIALLY DEFERRED;
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NO INHERIT;
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT VALID;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORCED;
+CREATE TEMP TABLE fktable2 (fk int references pktable ENFORCED NOT ENFORCED);
-- test order of firing of FK triggers when several RI-induced changes need to
-- be made to the same row. This was broken by subtransaction-related
@@ -1184,11 +1242,12 @@ ALTER TABLE fk_partitioned_fk DROP COLUMN fdrop1;
CREATE TABLE fk_partitioned_fk_1 (fdrop1 int, fdrop2 int, a int, fdrop3 int, b int);
ALTER TABLE fk_partitioned_fk_1 DROP COLUMN fdrop1, DROP COLUMN fdrop2, DROP COLUMN fdrop3;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FROM (0,0) TO (1000,1000);
-ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk;
+ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
+ALTER TABLE fk_partitioned_fk_2 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
-
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
CREATE TABLE fk_partitioned_fk_3 (fdrop1 int, fdrop2 int, fdrop3 int, fdrop4 int, b int, a int)
PARTITION BY HASH (a);
ALTER TABLE fk_partitioned_fk_3 DROP COLUMN fdrop1, DROP COLUMN fdrop2,
@@ -1234,6 +1293,27 @@ UPDATE fk_notpartitioned_pk SET b = 1502 WHERE a = 1500;
UPDATE fk_notpartitioned_pk SET b = 2504 WHERE a = 2500;
-- check psql behavior
\d fk_notpartitioned_pk
+
+-- Check the exsting FK trigger
+CREATE TEMP TABLE tmp_trg_info AS SELECT tgtype, tgrelid::regclass, tgconstraint
+FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+SELECT count(1) FROM tmp_trg_info;
+
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+-- No triggers
+SELECT count(1) FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+
+-- Changing it back to ENFORCED will recreate the necessary triggers.
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+
+-- Should be exactly the same number of triggers found as before
+SELECT COUNT(1) FROM pg_trigger pgt JOIN tmp_trg_info tt
+ON (pgt.tgtype = tt.tgtype AND pgt.tgrelid = tt.tgrelid AND pgt.tgconstraint = tt.tgconstraint);
+
ALTER TABLE fk_partitioned_fk DROP CONSTRAINT fk_partitioned_fk_a_b_fkey;
-- done.
DROP TABLE fk_notpartitioned_pk, fk_partitioned_fk;
@@ -1441,6 +1521,22 @@ ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN
\d fk_partitioned_fk_2
DROP TABLE fk_partitioned_fk_2;
+CREATE TABLE fk_partitioned_fk_2 (b int, a int,
+ FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
+BEGIN;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should be having two constraints
+\d fk_partitioned_fk_2
+DROP TABLE fk_partitioned_fk_2;
+ROLLBACK;
+BEGIN;
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have only one constraint
+\d fk_partitioned_fk_2
+ROLLBACK;
+DROP TABLE fk_partitioned_fk_2;
+
CREATE TABLE fk_partitioned_fk_4 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE) PARTITION BY RANGE (b, a);
CREATE TABLE fk_partitioned_fk_4_1 PARTITION OF fk_partitioned_fk_4 FOR VALUES FROM (1,1) TO (100,100);
CREATE TABLE fk_partitioned_fk_4_2 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE SET NULL);
--
2.43.5
v16-0009-Merge-the-parent-and-child-constraints-with-diff.patchapplication/octet-stream; name=v16-0009-Merge-the-parent-and-child-constraints-with-diff.patchDownload
From d05416e9f2d56ff38d179544639c9b2869b9ff0d Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 10 Feb 2025 10:59:28 +0530
Subject: [PATCH v16 9/9] Merge the parent and child constraints with differing
enforcibility.
If an enforced parent constraint is attached to a non-enforced child
constraint, the child constraint will be made enforced, with
validation applied if the parent constraint is validated as well.
Otherwise, a new enforced constraint (with validation, if the parent
constraint is validated) would need to be created on the child table,
which would be unnecessary if a similar constraint already exists and
can be attached.
On the other hand, having a not-enforced parent constraint with an
enforced child constraint does not cause any issues, and no changes
are required.
----
NOTE: This patch is intended to reduce the diff noise from the main
patch and is not meant to be committed separately. It should be
squashed with the main patch that adds ENFORCED/NOT ENFORCED.
----
---
src/backend/commands/tablecmds.c | 177 ++++++++++++++++++++--
src/test/regress/expected/foreign_key.out | 77 ++++++++--
src/test/regress/sql/foreign_key.sql | 37 ++++-
3 files changed, 261 insertions(+), 30 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c8f2ba3f473..29db5bcae07 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -11530,7 +11530,6 @@ tryAttachPartitionForeignKey(List **wqueue,
if (OidIsValid(partConstr->conparentid) ||
partConstr->condeferrable != parentConstr->condeferrable ||
partConstr->condeferred != parentConstr->condeferred ||
- partConstr->conenforced != parentConstr->conenforced ||
partConstr->confupdtype != parentConstr->confupdtype ||
partConstr->confdeltype != parentConstr->confdeltype ||
partConstr->confmatchtype != parentConstr->confmatchtype)
@@ -11575,6 +11574,8 @@ AttachPartitionForeignKey(List **wqueue,
Oid partConstrFrelid;
Oid partConstrRelid;
bool parentConstrIsEnforced;
+ bool partConstrIsEnforced;
+ bool partConstrParentIsSet;
/* Fetch the parent constraint tuple */
parentConstrTup = SearchSysCache1(CONSTROID,
@@ -11590,13 +11591,47 @@ AttachPartitionForeignKey(List **wqueue,
if (!HeapTupleIsValid(partcontup))
elog(ERROR, "cache lookup failed for constraint %u", partConstrOid);
partConstr = (Form_pg_constraint) GETSTRUCT(partcontup);
+ partConstrIsEnforced = partConstr->conenforced;
partConstrFrelid = partConstr->confrelid;
partConstrRelid = partConstr->conrelid;
+ /*
+ * The case where the parent constraint is not enforced and the child
+ * constraint is enforced is acceptable because the non-enforced parent
+ * constraint lacks triggers, eliminating any redundancy issues with the
+ * enforced child constraint. In this scenario, the child constraint
+ * remains enforced, and its trigger is retained, ensuring that
+ * referential integrity checks for the child continue as before, even
+ * with the parent constraint not enforced. The relationship between the
+ * two constraints is preserved by setting the parent constraint, which
+ * allows us to locate the child constraint. This becomes important if the
+ * parent constraint is later changed to enforced, at which point the
+ * necessary trigger will be created for the parent, and any redundancy
+ * from these triggers will be appropriately handled.
+ */
+ if (!parentConstrIsEnforced && partConstrIsEnforced)
+ {
+ ReleaseSysCache(partcontup);
+ ReleaseSysCache(parentConstrTup);
+
+ ConstraintSetParentConstraint(partConstrOid, parentConstrOid,
+ RelationGetRelid(partition));
+ CommandCounterIncrement();
+
+ return;
+ }
+
/*
* If the referenced table is partitioned, then the partition we're
* attaching now has extra pg_constraint rows and action triggers that are
* no longer needed. Remove those.
+ *
+ * Note that this must be done beforehand, particularly in situations
+ * where we might decide to change the constraint to an enforced state
+ * which will create the required triggers and add the child constraint to
+ * the validation queue. To avoid generating unnecessary triggers and
+ * adding them to the validation queue, it is crucial to eliminate any
+ * redundant constraints beforehand.
*/
if (get_rel_relkind(partConstrFrelid) == RELKIND_PARTITIONED_TABLE)
{
@@ -11608,6 +11643,48 @@ AttachPartitionForeignKey(List **wqueue,
table_close(pg_constraint, RowShareLock);
}
+ /*
+ * The case where the parent constraint is enforced and the child
+ * constraint is not enforced is not acceptable, as it would violate
+ * referential integrity. In such cases, the child constraint will first
+ * be enforced before merging it with the enforced parent constraint.
+ * Subsequently, removing action triggers, setting up constraint triggers,
+ * and handling check triggers for the parent will be managed in the usual
+ * manner, similar to how two enforced constraints are merged.
+ */
+ if (parentConstrIsEnforced && !partConstrIsEnforced)
+ {
+ ATAlterConstraint *cmdcon = makeNode(ATAlterConstraint);
+ Relation conrel;
+ List *otherrelids = NIL;
+
+ cmdcon->conname = NameStr(partConstr->conname);
+ cmdcon->deferrable = partConstr->condeferrable;
+ cmdcon->initdeferred = partConstr->condeferred;
+ cmdcon->alterEnforceability = true;
+ cmdcon->is_enforced = true;
+
+ conrel = table_open(ConstraintRelationId, RowExclusiveLock);
+
+ ATExecAlterConstraintInternal(wqueue, cmdcon, conrel, trigrel,
+ partConstr->conrelid,
+ partConstr->confrelid,
+ partcontup, true, &otherrelids,
+ AccessExclusiveLock, InvalidOid,
+ InvalidOid, InvalidOid, InvalidOid);
+
+ table_close(conrel, RowExclusiveLock);
+
+ CommandCounterIncrement();
+ }
+
+ /*
+ * The constraint parent shouldn't be set beforehand, or if it's already
+ * set, it should be the specified parent.
+ */
+ partConstrParentIsSet = OidIsValid(partConstr->conparentid);
+ Assert(!partConstrParentIsSet || partConstr->conparentid == parentConstrOid);
+
/*
* Will we need to validate this constraint? A valid parent constraint
* implies that all child constraints have been validated, so if this one
@@ -11627,8 +11704,10 @@ AttachPartitionForeignKey(List **wqueue,
DropForeignKeyConstraintTriggers(trigrel, partConstrOid, partConstrFrelid,
partConstrRelid);
- ConstraintSetParentConstraint(partConstrOid, parentConstrOid,
- RelationGetRelid(partition));
+ /* Skip if the parent is already set */
+ if (!partConstrParentIsSet)
+ ConstraintSetParentConstraint(partConstrOid, parentConstrOid,
+ RelationGetRelid(partition));
/*
* Like the constraint, attach partition's "check" triggers to the
@@ -12254,11 +12333,17 @@ ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon,
}
/*
- * If the table at either end of the constraint is partitioned, we need to
- * handle every constraint that is a child of this one.
+ * If either table involved in the constraint is partitioned, we need to
+ * handle every child constraint associated with it. This must be done
+ * regardless of whether the constraint entry has been modified.
+ *
+ * For example, if the parent constraint is marked as NOT ENFORCED and an
+ * ALTER command attempts to set it as NOT ENFORCED again, there is no
+ * change for the parent constraint itself. However, we still need to
+ * recurse through all child constraints, as a NOT ENFORCED parent
+ * constraint may have ENFORCED child constraints.
*/
- if (recurse && changed &&
- (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
+ if (recurse && (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
(OidIsValid(refrelid) &&
get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)))
ATExecAlterChildConstr(wqueue, cmdcon, conrel, tgrel, fkrelid, pkrelid,
@@ -12379,6 +12464,17 @@ ATExecAlterConstrEnforceability(List **wqueue, ATAlterConstraint *cmdcon,
/* Drop all the triggers */
DropForeignKeyConstraintTriggers(tgrel, conoid, InvalidOid, InvalidOid);
+
+ /*
+ * If the referenced table is partitioned, the child constraint we're
+ * changing to not-enforced may have additional pg_constraint rows and
+ * action triggers that remain untouched while this child constraint
+ * is attached to the not-enforced parent. These must now be removed.
+ * For more details, see AttachPartitionForeignKey().
+ */
+ if (OidIsValid(currcon->conparentid) &&
+ get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE)
+ RemoveInheritedConstraint(conrel, tgrel, currcon->conrelid, conoid);
}
else /* Create triggers */
{
@@ -12439,13 +12535,42 @@ ATExecAlterConstrEnforceability(List **wqueue, ATAlterConstraint *cmdcon,
true, NULL, 1, &pkey);
while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
- ATExecAlterConstraintInternal(wqueue, cmdcon, conrel, tgrel,
- fkrelid, pkrelid, childtup, true,
- otherrelids, lockmode,
- ReferencedDelTriggerOid,
- ReferencedUpdTriggerOid,
- ReferencingInsTriggerOid,
- ReferencingUpdTriggerOid);
+ {
+ Form_pg_constraint childcon;
+
+ childcon = (Form_pg_constraint) GETSTRUCT(childtup);
+
+ if (childcon->conenforced)
+ {
+ /*
+ * The child constraint is attached to the parent
+ * constraint, which is already enforced. Now, as the
+ * parent constraint is being modified to be enforced,
+ * some constraints and action triggers on the child table
+ * may become redundant and need to be removed.
+ */
+ if (currcon->confrelid == pkrelid)
+ {
+ Relation rel = table_open(childcon->conrelid, lockmode);
+
+ AttachPartitionForeignKey(wqueue, rel, childcon->oid,
+ conoid,
+ ReferencingInsTriggerOid,
+ ReferencingUpdTriggerOid,
+ tgrel);
+
+ table_close(rel, NoLock);
+ }
+ }
+ else
+ ATExecAlterConstraintInternal(wqueue, cmdcon, conrel, tgrel,
+ fkrelid, pkrelid, childtup, true,
+ otherrelids, lockmode,
+ ReferencedDelTriggerOid,
+ ReferencedUpdTriggerOid,
+ ReferencingInsTriggerOid,
+ ReferencingUpdTriggerOid);
+ }
systable_endscan(pscan);
}
@@ -20624,7 +20749,9 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
{
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
+ HeapTuple parentContup;
Form_pg_constraint conform;
+ Oid parentConstrIsEnforced;
contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
if (!HeapTupleIsValid(contup))
@@ -20643,12 +20770,34 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
continue;
}
+ /* Get the enforcibility of the parent constraint */
+ parentContup = SearchSysCache1(CONSTROID,
+ ObjectIdGetDatum(conform->conparentid));
+ if (!HeapTupleIsValid(parentContup))
+ elog(ERROR, "cache lookup failed for constraint %u",
+ conform->conparentid);
+ parentConstrIsEnforced =
+ ((Form_pg_constraint) GETSTRUCT(parentContup))->conenforced;
+ ReleaseSysCache(parentContup);
+
/*
* The constraint on this table must be marked no longer a child of
* the parent's constraint, as do its check triggers.
*/
ConstraintSetParentConstraint(fk->conoid, InvalidOid, InvalidOid);
+ /*
+ * Unsetting the parent is sufficient when the parent constraint is
+ * not enforced and the child constraint is enforced, as we link them
+ * by setting the constraint parent, while leaving the rest unchanged.
+ * For more details, see AttachPartitionForeignKey().
+ */
+ if (!parentConstrIsEnforced && fk->conenforced)
+ {
+ ReleaseSysCache(contup);
+ continue;
+ }
+
/*
* Also, look up the partition's "check" triggers corresponding to the
* enforced constraint being detached and detach them from the parent
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index d9dea078dca..46ad8dac0d5 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -1672,7 +1672,6 @@ ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FR
ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
-ALTER TABLE fk_partitioned_fk_2 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
CREATE TABLE fk_partitioned_fk_3 (fdrop1 int, fdrop2 int, fdrop3 int, fdrop4 int, b int, a int)
@@ -1681,8 +1680,65 @@ ALTER TABLE fk_partitioned_fk_3 DROP COLUMN fdrop1, DROP COLUMN fdrop2,
DROP COLUMN fdrop3, DROP COLUMN fdrop4;
CREATE TABLE fk_partitioned_fk_3_0 PARTITION OF fk_partitioned_fk_3 FOR VALUES WITH (MODULUS 5, REMAINDER 0);
CREATE TABLE fk_partitioned_fk_3_1 PARTITION OF fk_partitioned_fk_3 FOR VALUES WITH (MODULUS 5, REMAINDER 1);
+-- Merge the not-enforced parent constraint with the enforced and not-enforced child constraints
+ALTER TABLE fk_partitioned_fk_3_0 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ENFORCED;
+ALTER TABLE fk_partitioned_fk_3_1 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk_3 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+-- Merge the non-enforced parent constraint with both the enforced and
+-- non-enforced child constraints, where the referenced table is partitioned.
+CREATE TABLE fk_partitioned_pk (a int, b int, PRIMARY KEY (a, b)) PARTITION BY RANGE (a, b);
+CREATE TABLE fk_partitioned_pk_1 PARTITION OF fk_partitioned_pk FOR VALUES FROM (0,0) TO (1000,1000);
+-- Merge the enforced parent constraint with the enforced and not-enforced child constraints.
+ALTER TABLE fk_partitioned_fk_3_0 ADD FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk ENFORCED;
+ALTER TABLE fk_partitioned_fk_3_1 ADD FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk_3 ADD FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk ENFORCED;
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+ conname | conenforced | convalidated | conrelid | confrelid
+---------------------------------+-------------+--------------+-----------------------+----------------------
+ fk_partitioned_fk_3_0_a_b_fkey | t | t | fk_partitioned_fk_3_0 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey | f | t | fk_partitioned_fk_3_1 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_a_b_fkey | f | t | fk_partitioned_fk_3 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_0_a_b_fkey1 | t | t | fk_partitioned_fk_3_0 | fk_partitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey1 | t | t | fk_partitioned_fk_3_1 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey1 | t | t | fk_partitioned_fk_3 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey2 | t | t | fk_partitioned_fk_3 | fk_partitioned_pk_1
+(7 rows)
+
+ALTER TABLE fk_partitioned_fk_3 DETACH PARTITION fk_partitioned_fk_3_0;
+ALTER TABLE fk_partitioned_fk_3 ATTACH PARTITION fk_partitioned_fk_3_0 FOR VALUES WITH (MODULUS 5, REMAINDER 0);
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+ conname | conenforced | convalidated | conrelid | confrelid
+---------------------------------+-------------+--------------+-----------------------+----------------------
+ fk_partitioned_fk_3_0_a_b_fkey | t | t | fk_partitioned_fk_3_0 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey | f | t | fk_partitioned_fk_3_1 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_a_b_fkey | f | t | fk_partitioned_fk_3 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_0_a_b_fkey1 | t | t | fk_partitioned_fk_3_0 | fk_partitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey1 | t | t | fk_partitioned_fk_3_1 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey1 | t | t | fk_partitioned_fk_3 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey2 | t | t | fk_partitioned_fk_3 | fk_partitioned_pk_1
+(7 rows)
+
+ALTER TABLE fk_partitioned_fk_3 ALTER CONSTRAINT fk_partitioned_fk_3_a_b_fkey1 NOT ENFORCED;
+-- Merging an enforced parent constraint (validated) with a not-enforced child
+-- constraint will implicitly change the child constraint to enforced and apply
+-- the validation as well.
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_3
FOR VALUES FROM (2000,2000) TO (3000,3000);
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+ conname | conenforced | convalidated | conrelid | confrelid
+---------------------------------+-------------+--------------+-----------------------+----------------------
+ fk_partitioned_fk_3_0_a_b_fkey | t | t | fk_partitioned_fk_3_0 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey | t | t | fk_partitioned_fk_3_1 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_a_b_fkey | t | t | fk_partitioned_fk_3 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_0_a_b_fkey1 | f | t | fk_partitioned_fk_3_0 | fk_partitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey1 | f | t | fk_partitioned_fk_3_1 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey1 | f | t | fk_partitioned_fk_3 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey2 | f | t | fk_partitioned_fk_3 | fk_partitioned_pk_1
+(7 rows)
+
-- Creating a foreign key with ONLY on a partitioned table referencing
-- a non-partitioned table fails.
ALTER TABLE ONLY fk_partitioned_fk ADD FOREIGN KEY (a, b)
@@ -1697,22 +1753,22 @@ INSERT INTO fk_partitioned_fk_1 (a,b) VALUES (500, 501);
ERROR: insert or update on table "fk_partitioned_fk_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
DETAIL: Key (a, b)=(500, 501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (1500, 1501);
-ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_2_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
DETAIL: Key (a, b)=(1500, 1501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk_2 (a,b) VALUES (1500, 1501);
-ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_2_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
DETAIL: Key (a, b)=(1500, 1501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (2500, 2502);
-ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_3_1_a_b_fkey"
DETAIL: Key (a, b)=(2500, 2502) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk_3 (a,b) VALUES (2500, 2502);
-ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_3_1_a_b_fkey"
DETAIL: Key (a, b)=(2500, 2502) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (2501, 2503);
-ERROR: insert or update on table "fk_partitioned_fk_3_0" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_0" violates foreign key constraint "fk_partitioned_fk_3_0_a_b_fkey"
DETAIL: Key (a, b)=(2501, 2503) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk_3 (a,b) VALUES (2501, 2503);
-ERROR: insert or update on table "fk_partitioned_fk_3_0" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_0" violates foreign key constraint "fk_partitioned_fk_3_0_a_b_fkey"
DETAIL: Key (a, b)=(2501, 2503) is not present in table "fk_notpartitioned_pk".
-- but if we insert the values that make them valid, then they work
INSERT INTO fk_notpartitioned_pk VALUES (500, 501), (1500, 1501),
@@ -1723,7 +1779,7 @@ INSERT INTO fk_partitioned_fk (a,b) VALUES (2500, 2502);
INSERT INTO fk_partitioned_fk (a,b) VALUES (2501, 2503);
-- this update fails because there is no referenced row
UPDATE fk_partitioned_fk SET a = a + 1 WHERE a = 2501;
-ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_3_1_a_b_fkey"
DETAIL: Key (a, b)=(2502, 2503) is not present in table "fk_notpartitioned_pk".
-- but we can fix it thusly:
INSERT INTO fk_notpartitioned_pk (a,b) VALUES (2502, 2503);
@@ -1866,8 +1922,6 @@ WHERE conrelid::regclass::text like 'fk_partitioned_fk%' ORDER BY oid::regclass:
DROP TABLE fk_partitioned_fk, fk_notpartitioned_pk;
-- NOT VALID foreign key on a non-partitioned table referencing a partitioned table
-CREATE TABLE fk_partitioned_pk (a int, b int, PRIMARY KEY (a, b)) PARTITION BY RANGE (a, b);
-CREATE TABLE fk_partitioned_pk_1 PARTITION OF fk_partitioned_pk FOR VALUES FROM (0,0) TO (1000,1000);
CREATE TABLE fk_notpartitioned_fk (b int, a int);
ALTER TABLE fk_notpartitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk NOT VALID;
-- Constraint will be invalid.
@@ -2083,7 +2137,7 @@ CREATE TABLE fk_partitioned_fk_2 (b int, a int,
FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
BEGIN;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
--- should be having two constraints
+-- should only have one constraint
\d fk_partitioned_fk_2
Table "public.fk_partitioned_fk_2"
Column | Type | Collation | Nullable | Default
@@ -2092,7 +2146,6 @@ ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN
a | integer | | |
Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
Foreign-key constraints:
- "fk_partitioned_fk_2_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED
TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
DROP TABLE fk_partitioned_fk_2;
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index 70991a6aabc..538eb2b9070 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -1245,18 +1245,49 @@ ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FR
ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
-ALTER TABLE fk_partitioned_fk_2 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+
CREATE TABLE fk_partitioned_fk_3 (fdrop1 int, fdrop2 int, fdrop3 int, fdrop4 int, b int, a int)
PARTITION BY HASH (a);
ALTER TABLE fk_partitioned_fk_3 DROP COLUMN fdrop1, DROP COLUMN fdrop2,
DROP COLUMN fdrop3, DROP COLUMN fdrop4;
CREATE TABLE fk_partitioned_fk_3_0 PARTITION OF fk_partitioned_fk_3 FOR VALUES WITH (MODULUS 5, REMAINDER 0);
CREATE TABLE fk_partitioned_fk_3_1 PARTITION OF fk_partitioned_fk_3 FOR VALUES WITH (MODULUS 5, REMAINDER 1);
+-- Merge the not-enforced parent constraint with the enforced and not-enforced child constraints
+ALTER TABLE fk_partitioned_fk_3_0 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ENFORCED;
+ALTER TABLE fk_partitioned_fk_3_1 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk_3 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+
+-- Merge the non-enforced parent constraint with both the enforced and
+-- non-enforced child constraints, where the referenced table is partitioned.
+CREATE TABLE fk_partitioned_pk (a int, b int, PRIMARY KEY (a, b)) PARTITION BY RANGE (a, b);
+CREATE TABLE fk_partitioned_pk_1 PARTITION OF fk_partitioned_pk FOR VALUES FROM (0,0) TO (1000,1000);
+-- Merge the enforced parent constraint with the enforced and not-enforced child constraints.
+ALTER TABLE fk_partitioned_fk_3_0 ADD FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk ENFORCED;
+ALTER TABLE fk_partitioned_fk_3_1 ADD FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk_3 ADD FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk ENFORCED;
+
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+
+ALTER TABLE fk_partitioned_fk_3 DETACH PARTITION fk_partitioned_fk_3_0;
+ALTER TABLE fk_partitioned_fk_3 ATTACH PARTITION fk_partitioned_fk_3_0 FOR VALUES WITH (MODULUS 5, REMAINDER 0);
+
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+
+ALTER TABLE fk_partitioned_fk_3 ALTER CONSTRAINT fk_partitioned_fk_3_a_b_fkey1 NOT ENFORCED;
+
+-- Merging an enforced parent constraint (validated) with a not-enforced child
+-- constraint will implicitly change the child constraint to enforced and apply
+-- the validation as well.
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_3
FOR VALUES FROM (2000,2000) TO (3000,3000);
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+
-- Creating a foreign key with ONLY on a partitioned table referencing
-- a non-partitioned table fails.
ALTER TABLE ONLY fk_partitioned_fk ADD FOREIGN KEY (a, b)
@@ -1383,8 +1414,6 @@ WHERE conrelid::regclass::text like 'fk_partitioned_fk%' ORDER BY oid::regclass:
DROP TABLE fk_partitioned_fk, fk_notpartitioned_pk;
-- NOT VALID foreign key on a non-partitioned table referencing a partitioned table
-CREATE TABLE fk_partitioned_pk (a int, b int, PRIMARY KEY (a, b)) PARTITION BY RANGE (a, b);
-CREATE TABLE fk_partitioned_pk_1 PARTITION OF fk_partitioned_pk FOR VALUES FROM (0,0) TO (1000,1000);
CREATE TABLE fk_notpartitioned_fk (b int, a int);
ALTER TABLE fk_notpartitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk NOT VALID;
@@ -1525,7 +1554,7 @@ CREATE TABLE fk_partitioned_fk_2 (b int, a int,
FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
BEGIN;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
--- should be having two constraints
+-- should only have one constraint
\d fk_partitioned_fk_2
DROP TABLE fk_partitioned_fk_2;
ROLLBACK;
--
2.43.5
On 2025-Feb-28, Amul Sul wrote:
Yeah, that was intentional. I wanted to avoid recursion again by
hitting ATExecAlterChildConstr() at the end of
ATExecAlterConstraintInternal(). Also, I realized the value doesn’t
matter since recurse = false is explicitly set inside the
cmdcon->alterEnforceability condition. I wasn’t fully satisfied with
how we handled the recursion decision (code design), so I’ll give it
more thought. If I don’t find a better approach, I’ll add clearer
comments to explain the reasoning.
So, did you have a chance to rethink the recursion model here? TBH I do
not like what you have one bit.
--
Álvaro Herrera 48°01'N 7°57'E — https://www.EnterpriseDB.com/
"Para tener más hay que desear menos"
I have committed the first three refactoring patches (v16-0001,
v16-0002, v16-0003). (I guess Álvaro didn't like the first one, so I
suppose I'll revert that one, but it's a simple one, so you can proceed
either way.)
I think the next step here is that you work to fix Álvaro's concerns
about the recursion structure.
I have a few other review comments here in the meantime:
* patch v16-0007 "Ease the restriction that a NOT ENFORCED constraint
must be INVALID."
I don't understand the purpose of this one. And the commit message also
doesn't explain the reason, only what it does. I think we have settled
on three states (not enforced and not valid; enforced but not yet valid;
enforced and valid), so it seems sensible to keep valid as false if
enforced is also false. Did I miss something?
Specifically, this test case change
-ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten_not_enforced CHECK
(b > 10) NOT ENFORCED; -- succeeds
+ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten_not_enforced CHECK
(b > 10) NOT ENFORCED; -- fail
+ERROR: check constraint "b_greater_than_ten_not_enforced" of relation
"attmp3" is violated by some row
+ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten_not_enforced CHECK
(b > 10) NOT VALID NOT ENFORCED; -- succeeds
seems very wrong to me.
* doc/src/sgml/advanced.sgml
Let's skip that. This material is too advanced for a tutorial.
* doc/src/sgml/ddl.sgml
Let's move the material about NOT ENFORCED into a separate section or
paragraph, not in the first paragraph that introduces foreign keys. I
suggest a separate sect2-level section at the end of the "Constraints"
section.
* src/backend/catalog/sql_features.txt
The SQL standard has NOT ENFORCED only for check and foreign-key
constraints, so you could flip this to "YES" here. (Hmm, do we need
to support not-null constraints, though (which are grouped under check
constraints in the standard)? Maybe turn the comment around and say
"except not-null constraints" or something like that.)
* src/backend/commands/tablecmds.c
I would omit this detail message:
errdetail("Enforceability can only be altered for foreign key constraints.")
We have generally tried to get rid of detail messages that say "cannot
do this on this object type, but you could do it on a different object
type", since that is not actually useful.
* src/test/regress/expected/foreign_key.out
This error message is confusing, since no insert or update is happening:
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+ERROR: insert or update on table "fktable" violates foreign key
constraint "fktable_ftest1_fkey"
Could we give a differently worded error message in this case?
Here, you are relying on the automatic constraint naming, which seems
fragile and confusing:
+ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE
NOT VALID NOT ENFORCED;
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_ftest2_fkey
DEFERRABLE INITIALLY DEFERRED;
Better name the constraint explicitly in the first command.
On Tue, Mar 11, 2025 at 11:13 PM Peter Eisentraut <peter@eisentraut.org> wrote:
I have committed the first three refactoring patches (v16-0001,
v16-0002, v16-0003). (I guess Álvaro didn't like the first one, so I
suppose I'll revert that one, but it's a simple one, so you can proceed
either way.)
Sure, thank you !
I think the next step here is that you work to fix Álvaro's concerns
about the recursion structure.
Yes, I worked on that in the attached version. I refactored
ATExecAlterConstraintInternal() and moved the code that updates the
pg_constraint entry into a separate function (see 0001), so it can be
called from the places where the entry needs to be updated, rather
than revisiting ATExecAlterConstraintInternal(). In 0002,
ATExecAlterConstraintInternal() is split into two functions:
ATExecAlterConstrDeferrability() and
ATExecAlterConstrInheritability(), which handle altering deferrability
and inheritability, respectively. These functions are expected to
recurse on themselves, rather than revisiting
ATExecAlterConstraintInternal() as before. This approach simplifies
things. Similarly can add ATExecAlterConstrEnforceability() which
recurses itself.
I have a few other review comments here in the meantime:
* patch v16-0007 "Ease the restriction that a NOT ENFORCED constraint
must be INVALID."I don't understand the purpose of this one. And the commit message also
doesn't explain the reason, only what it does. I think we have settled
on three states (not enforced and not valid; enforced but not yet valid;
enforced and valid), so it seems sensible to keep valid as false if
enforced is also false. Did I miss something?
I attempted to implement this [1], but later didn’t switch to your
suggested three-state approach [2] because I hadn’t received
confirmation for it.
Anyway, I’ve now tried the [2] approach in the attached patch. Could
you kindly confirm my understanding of the pg_constraint entry
updates:
When the constraint is changed to NOT ENFORCED, both conenforced and
convalidated should be set to false. Similarly, when the constraint is
changed to ENFORCED, validation must be performed, and both of these
flags should be set to true.
Specifically, this test case change
-ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten_not_enforced CHECK (b > 10) NOT ENFORCED; -- succeeds +ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten_not_enforced CHECK (b > 10) NOT ENFORCED; -- fail +ERROR: check constraint "b_greater_than_ten_not_enforced" of relation "attmp3" is violated by some row +ALTER TABLE attmp3 ADD CONSTRAINT b_greater_than_ten_not_enforced CHECK (b > 10) NOT VALID NOT ENFORCED; -- succeedsseems very wrong to me.
Agreed. I have dropped this patch since it is no longer needed with
your suggested approach[2].
* doc/src/sgml/advanced.sgml
Let's skip that. This material is too advanced for a tutorial.
Done.
* doc/src/sgml/ddl.sgml
Let's move the material about NOT ENFORCED into a separate section or
paragraph, not in the first paragraph that introduces foreign keys. I
suggest a separate sect2-level section at the end of the "Constraints"
section.
I skipped that as well, since I realized that there is no description
regarding deferrability in that patch. This information can be found
on the CREATE TABLE page, which this section references for more
details.
* src/backend/catalog/sql_features.txt
The SQL standard has NOT ENFORCED only for check and foreign-key
constraints, so you could flip this to "YES" here. (Hmm, do we need
to support not-null constraints, though (which are grouped under check
constraints in the standard)? Maybe turn the comment around and say
"except not-null constraints" or something like that.)
Done.
* src/backend/commands/tablecmds.c
I would omit this detail message:
errdetail("Enforceability can only be altered for foreign key constraints.")
We have generally tried to get rid of detail messages that say "cannot
do this on this object type, but you could do it on a different object
type", since that is not actually useful.* src/test/regress/expected/foreign_key.out
Done.
This error message is confusing, since no insert or update is happening:
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED; +ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_fkey"Could we give a differently worded error message in this case?
I noticed a similar error when adding a constraint through ALTER
TABLE, coming from ri_ReportViolation. I don’t have an immediate
solution, but I believe we need to pass some context to
ri_ReportViolation to indicate what has been done when it is called
from RI_PartitionRemove_Check.
Here, you are relying on the automatic constraint naming, which seems
fragile and confusing:+ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE NOT VALID NOT ENFORCED; +ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_ftest2_fkey DEFERRABLE INITIALLY DEFERRED;Better name the constraint explicitly in the first command.
Fixed in the attached version.
Regards,
Amul.
1] /messages/by-id/CAExHW5tqoQvkGbYJHQUz0ytVqT7JyT7MSq0xuc4-qSQaNPfRBQ@mail.gmail.com
2] /messages/by-id/50f46903-20e1-4e23-918c-a6cfdf1a9f4a@eisentraut.org
Attachments:
v17-0001-refactor-move-code-updates-pg_constraint-entry-i.patchapplication/octet-stream; name=v17-0001-refactor-move-code-updates-pg_constraint-entry-i.patchDownload
From 6f3feef9c98d0b99622a770cc028afbb5bc16e5f Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Tue, 11 Mar 2025 18:19:39 +0530
Subject: [PATCH v17 1/6] refactor: move code updates pg_constraint entry in a
separate function
---
src/backend/commands/tablecmds.c | 62 ++++++++++++++++++++------------
1 file changed, 39 insertions(+), 23 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 18ff8956577..df4e239b651 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -402,6 +402,8 @@ static void ATExecAlterChildConstr(List **wqueue, ATAlterConstraint *cmdcon,
Relation conrel, Relation tgrel, Relation rel,
HeapTuple contuple, bool recurse, List **otherrelids,
LOCKMODE lockmode);
+static void UpdateConstraintEntry(ATAlterConstraint *cmdcon, Relation conrel,
+ HeapTuple contuple);
static ObjectAddress ATExecValidateConstraint(List **wqueue,
Relation rel, char *constrName,
bool recurse, bool recursing, LOCKMODE lockmode);
@@ -12095,23 +12097,9 @@ ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon,
(currcon->condeferrable != cmdcon->deferrable ||
currcon->condeferred != cmdcon->initdeferred))
{
- HeapTuple copyTuple;
- Form_pg_constraint copy_con;
-
- copyTuple = heap_copytuple(contuple);
- copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
- copy_con->condeferrable = cmdcon->deferrable;
- copy_con->condeferred = cmdcon->initdeferred;
- CatalogTupleUpdate(conrel, ©Tuple->t_self, copyTuple);
-
- InvokeObjectPostAlterHook(ConstraintRelationId, currcon->oid, 0);
-
- heap_freetuple(copyTuple);
+ UpdateConstraintEntry(cmdcon, conrel, contuple);
changed = true;
- /* Make new constraint flags visible to others */
- CacheInvalidateRelcache(rel);
-
/*
* Now we need to update the multiple entries in pg_trigger that
* implement the constraint.
@@ -12142,19 +12130,12 @@ ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon,
AttrNumber colNum;
char *colName;
List *children;
- HeapTuple copyTuple;
- Form_pg_constraint copy_con;
/* The current implementation only works for NOT NULL constraints */
Assert(currcon->contype == CONSTRAINT_NOTNULL);
- copyTuple = heap_copytuple(contuple);
- copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
- copy_con->connoinherit = cmdcon->noinherit;
-
- CatalogTupleUpdate(conrel, ©Tuple->t_self, copyTuple);
+ UpdateConstraintEntry(cmdcon, conrel, contuple);
CommandCounterIncrement();
- heap_freetuple(copyTuple);
changed = true;
/* Fetch the column number and name */
@@ -12316,6 +12297,41 @@ ATExecAlterChildConstr(List **wqueue, ATAlterConstraint *cmdcon,
systable_endscan(pscan);
}
+/*
+ * Update the constraint entry.
+ */
+static void
+UpdateConstraintEntry(ATAlterConstraint *cmdcon, Relation conrel,
+ HeapTuple contuple)
+{
+ Form_pg_constraint currcon;
+ HeapTuple copyTuple;
+ Form_pg_constraint copy_con;
+
+ Assert(cmdcon->alterDeferrability || cmdcon->alterInheritability);
+
+ currcon = (Form_pg_constraint) GETSTRUCT(contuple);
+
+ copyTuple = heap_copytuple(contuple);
+ copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
+
+ if (cmdcon->alterDeferrability)
+ {
+ copy_con->condeferrable = cmdcon->deferrable;
+ copy_con->condeferred = cmdcon->initdeferred;
+ }
+ if (cmdcon->alterInheritability)
+ copy_con->connoinherit = cmdcon->noinherit;
+
+ CatalogTupleUpdate(conrel, ©Tuple->t_self, copyTuple);
+ InvokeObjectPostAlterHook(ConstraintRelationId, currcon->oid, 0);
+
+ heap_freetuple(copyTuple);
+
+ /* Make new constraint flags visible to others */
+ CacheInvalidateRelcacheByRelid(currcon->conrelid);
+}
+
/*
* ALTER TABLE VALIDATE CONSTRAINT
*
--
2.43.5
v17-0002-refactor-Split-ATExecAlterConstraintInternal.patchapplication/octet-stream; name=v17-0002-refactor-Split-ATExecAlterConstraintInternal.patchDownload
From e4789109b40be236b517b5feeabfd2872fc40033 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Tue, 11 Mar 2025 20:41:14 +0530
Subject: [PATCH v17 2/6] refactor: Split ATExecAlterConstraintInternal().
Split ATExecAlterConstraintInternal() into two functions:
ATExecAlterConstrDeferrability() and ATExecAlterConstrInheritability().
This simplifies the code and avoids unnecessary confusion caused by
recursive code, which isn't needed for ATExecAlterConstrInheritability.
---
src/backend/commands/tablecmds.c | 244 +++++++++++++++++++------------
1 file changed, 150 insertions(+), 94 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index df4e239b651..cc462aa831b 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -394,14 +394,21 @@ static ObjectAddress ATExecAlterConstraint(List **wqueue, Relation rel,
bool recurse, LOCKMODE lockmode);
static bool ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon, Relation conrel,
Relation tgrel, Relation rel, HeapTuple contuple,
- bool recurse, List **otherrelids, LOCKMODE lockmode);
+ bool recurse, LOCKMODE lockmode);
+static bool ATExecAlterConstrDeferrability(List **wqueue, ATAlterConstraint *cmdcon,
+ Relation conrel, Relation tgrel, Relation rel,
+ HeapTuple contuple, bool recurse,
+ List **otherrelids, LOCKMODE lockmode);
+static bool ATExecAlterConstrInheritability(List **wqueue, ATAlterConstraint *cmdcon,
+ Relation conrel, Relation rel,
+ HeapTuple contuple, LOCKMODE lockmode);
static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
bool deferrable, bool initdeferred,
List **otherrelids);
-static void ATExecAlterChildConstr(List **wqueue, ATAlterConstraint *cmdcon,
- Relation conrel, Relation tgrel, Relation rel,
- HeapTuple contuple, bool recurse, List **otherrelids,
- 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 UpdateConstraintEntry(ATAlterConstraint *cmdcon, Relation conrel,
HeapTuple contuple);
static ObjectAddress ATExecValidateConstraint(List **wqueue,
@@ -11927,7 +11934,6 @@ ATExecAlterConstraint(List **wqueue, Relation rel, ATAlterConstraint *cmdcon,
HeapTuple contuple;
Form_pg_constraint currcon;
ObjectAddress address;
- List *otherrelids = NIL;
/*
* Disallow altering ONLY a partitioned table, as it would make no sense.
@@ -12040,17 +12046,9 @@ ATExecAlterConstraint(List **wqueue, Relation rel, ATAlterConstraint *cmdcon,
* Do the actual catalog work, and recurse if necessary.
*/
if (ATExecAlterConstraintInternal(wqueue, cmdcon, conrel, tgrel, rel,
- contuple, recurse, &otherrelids, lockmode))
+ contuple, recurse, lockmode))
ObjectAddressSet(address, ConstraintRelationId, currcon->oid);
- /*
- * ATExecAlterConstraintInternal already invalidated relcache for the
- * relations having the constraint itself; here we also invalidate for
- * relations that have any triggers that are part of the constraint.
- */
- foreach_oid(relid, otherrelids)
- CacheInvalidateRelcacheByRelid(relid);
-
systable_endscan(scan);
table_close(tgrel, RowExclusiveLock);
@@ -12060,8 +12058,50 @@ ATExecAlterConstraint(List **wqueue, Relation rel, ATAlterConstraint *cmdcon,
}
/*
- * Recursive subroutine of ATExecAlterConstraint. Returns true if the
- * constraint is altered.
+ * A subroutine of ATExecAlterConstraint that calls the respective routines for
+ * altering constraint attributes.
+ */
+static bool
+ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon,
+ Relation conrel, Relation tgrel, Relation rel,
+ HeapTuple contuple, bool recurse,
+ LOCKMODE lockmode)
+{
+ bool changed = false;
+ List *otherrelids = NIL;
+
+ /*
+ * Do the catalog work for the deferrability change, recurse if necessary.
+ */
+ if (cmdcon->alterDeferrability &&
+ ATExecAlterConstrDeferrability(wqueue, cmdcon, conrel, tgrel, rel,
+ contuple, recurse, &otherrelids,
+ lockmode))
+ {
+ /*
+ * UpdateConstraintEntry already invalidated relcache for the
+ * relations having the constraint itself; here we also invalidate for
+ * relations that have any triggers that are part of the constraint.
+ */
+ foreach_oid(relid, otherrelids)
+ CacheInvalidateRelcacheByRelid(relid);
+
+ changed = true;
+ }
+
+ /*
+ * Do the catalog work for the inheritability change.
+ */
+ if (cmdcon->alterInheritability &&
+ ATExecAlterConstrInheritability(wqueue, cmdcon, conrel, rel, contuple,
+ lockmode))
+ changed = true;
+
+ return changed;
+}
+
+/*
+ * Returns true if the constraint's deferrability is altered.
*
* *otherrelids is appended OIDs of relations containing affected triggers.
*
@@ -12071,31 +12111,32 @@ ATExecAlterConstraint(List **wqueue, Relation rel, ATAlterConstraint *cmdcon,
* but existing releases don't do that.)
*/
static bool
-ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon,
- Relation conrel, Relation tgrel, Relation rel,
- HeapTuple contuple, bool recurse,
- List **otherrelids, LOCKMODE lockmode)
+ATExecAlterConstrDeferrability(List **wqueue, ATAlterConstraint *cmdcon,
+ Relation conrel, Relation tgrel, Relation rel,
+ HeapTuple contuple, bool recurse,
+ List **otherrelids, LOCKMODE lockmode)
{
Form_pg_constraint currcon;
- Oid refrelid = InvalidOid;
+ Oid refrelid;
bool changed = false;
/* since this function recurses, it could be driven to stack overflow */
check_stack_depth();
+ Assert(cmdcon->alterDeferrability);
+
currcon = (Form_pg_constraint) GETSTRUCT(contuple);
- if (currcon->contype == CONSTRAINT_FOREIGN)
- refrelid = currcon->confrelid;
+ refrelid = currcon->confrelid;
+
+ /* Should be foreign key constraint */
+ Assert(currcon->contype == CONSTRAINT_FOREIGN);
/*
- * Update pg_constraint with the flags from cmdcon.
- *
* If called to modify a constraint that's already in the desired state,
* silently do nothing.
*/
- if (cmdcon->alterDeferrability &&
- (currcon->condeferrable != cmdcon->deferrable ||
- currcon->condeferred != cmdcon->initdeferred))
+ if (currcon->condeferrable != cmdcon->deferrable ||
+ currcon->condeferred != cmdcon->initdeferred)
{
UpdateConstraintEntry(cmdcon, conrel, contuple);
changed = true;
@@ -12113,75 +12154,89 @@ ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon,
* If the table at either end of the constraint is partitioned, we need to
* handle every constraint that is a child of this one.
*/
- if (recurse && changed &&
+ if (recurse &&
(rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
- (OidIsValid(refrelid) &&
- get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)))
- ATExecAlterChildConstr(wqueue, cmdcon, conrel, tgrel, rel, contuple,
- recurse, otherrelids, lockmode);
+ get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE))
+ AlterConstrDeferrabilityRecurse(wqueue, cmdcon, conrel, tgrel, rel,
+ contuple, recurse, otherrelids,
+ lockmode);
+
+ return changed;
+}
+
+/*
+ * Returns true if the constraint's inheritability is altered.
+ */
+static bool
+ATExecAlterConstrInheritability(List **wqueue, ATAlterConstraint *cmdcon,
+ Relation conrel, Relation rel,
+ HeapTuple contuple, LOCKMODE lockmode)
+{
+ Form_pg_constraint currcon;
+ AttrNumber colNum;
+ char *colName;
+ List *children;
+
+ Assert(cmdcon->alterInheritability);
+
+ currcon = (Form_pg_constraint) GETSTRUCT(contuple);
+
+ /* The current implementation only works for NOT NULL constraints */
+ Assert(currcon->contype == CONSTRAINT_NOTNULL);
+
+ /*
+ * If called to modify a constraint that's already in the desired state,
+ * silently do nothing.
+ */
+ if (cmdcon->noinherit == currcon->connoinherit)
+ return false;
+
+ UpdateConstraintEntry(cmdcon, conrel, contuple);
+ CommandCounterIncrement();
+
+ /* Fetch the column number and name */
+ colNum = extractNotNullColumn(contuple);
+ colName = get_attname(currcon->conrelid, colNum, false);
/*
- * Update the catalog for inheritability. No work if the constraint is
- * already in the requested state.
+ * Propagate the change to children. For SET NO INHERIT, we don't
+ * recursively affect children, just the immediate level.
*/
- if (cmdcon->alterInheritability &&
- (cmdcon->noinherit != currcon->connoinherit))
+ children = find_inheritance_children(RelationGetRelid(rel),
+ lockmode);
+ foreach_oid(childoid, children)
{
- AttrNumber colNum;
- char *colName;
- List *children;
+ ObjectAddress addr;
- /* The current implementation only works for NOT NULL constraints */
- Assert(currcon->contype == CONSTRAINT_NOTNULL);
-
- UpdateConstraintEntry(cmdcon, conrel, contuple);
- CommandCounterIncrement();
- changed = true;
-
- /* Fetch the column number and name */
- colNum = extractNotNullColumn(contuple);
- colName = get_attname(currcon->conrelid, colNum, false);
-
- /*
- * Propagate the change to children. For SET NO INHERIT, we don't
- * recursively affect children, just the immediate level.
- */
- children = find_inheritance_children(RelationGetRelid(rel),
- lockmode);
- foreach_oid(childoid, children)
+ if (cmdcon->noinherit)
{
- ObjectAddress addr;
+ HeapTuple childtup;
+ Form_pg_constraint childcon;
- if (cmdcon->noinherit)
- {
- HeapTuple childtup;
- Form_pg_constraint childcon;
-
- childtup = findNotNullConstraint(childoid, colName);
- if (!childtup)
- elog(ERROR, "cache lookup failed for not-null constraint on column \"%s\" of relation %u",
- colName, childoid);
- childcon = (Form_pg_constraint) GETSTRUCT(childtup);
- Assert(childcon->coninhcount > 0);
- childcon->coninhcount--;
- childcon->conislocal = true;
- CatalogTupleUpdate(conrel, &childtup->t_self, childtup);
- heap_freetuple(childtup);
- }
- else
- {
- Relation childrel = table_open(childoid, NoLock);
+ childtup = findNotNullConstraint(childoid, colName);
+ if (!childtup)
+ elog(ERROR, "cache lookup failed for not-null constraint on column \"%s\" of relation %u",
+ colName, childoid);
+ childcon = (Form_pg_constraint) GETSTRUCT(childtup);
+ Assert(childcon->coninhcount > 0);
+ childcon->coninhcount--;
+ childcon->conislocal = true;
+ CatalogTupleUpdate(conrel, &childtup->t_self, childtup);
+ heap_freetuple(childtup);
+ }
+ else
+ {
+ Relation childrel = table_open(childoid, NoLock);
- addr = ATExecSetNotNull(wqueue, childrel, NameStr(currcon->conname),
- colName, true, true, lockmode);
- if (OidIsValid(addr.objectId))
- CommandCounterIncrement();
- table_close(childrel, NoLock);
- }
+ addr = ATExecSetNotNull(wqueue, childrel, NameStr(currcon->conname),
+ colName, true, true, lockmode);
+ if (OidIsValid(addr.objectId))
+ CommandCounterIncrement();
+ table_close(childrel, NoLock);
}
}
- return changed;
+ return true;
}
/*
@@ -12250,7 +12305,7 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
}
/*
- * Invokes ATExecAlterConstraintInternal for each constraint that is a child of
+ * Invokes ATExecAlterConstrDeferrability for each constraint that is a child of
* the specified constraint.
*
* Note that this doesn't handle recursion the normal way, viz. by scanning the
@@ -12258,13 +12313,13 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
* relationships. This may need to be reconsidered.
*
* The arguments to this function have the same meaning as the arguments to
- * ATExecAlterConstraintInternal.
+ * ATExecAlterConstrDeferrability.
*/
static void
-ATExecAlterChildConstr(List **wqueue, ATAlterConstraint *cmdcon,
- Relation conrel, Relation tgrel, Relation rel,
- HeapTuple contuple, bool recurse, List **otherrelids,
- LOCKMODE lockmode)
+AlterConstrDeferrabilityRecurse(List **wqueue, ATAlterConstraint *cmdcon,
+ Relation conrel, Relation tgrel, Relation rel,
+ HeapTuple contuple, bool recurse,
+ List **otherrelids, LOCKMODE lockmode)
{
Form_pg_constraint currcon;
Oid conoid;
@@ -12289,8 +12344,9 @@ ATExecAlterChildConstr(List **wqueue, ATAlterConstraint *cmdcon,
Relation childrel;
childrel = table_open(childcon->conrelid, lockmode);
- ATExecAlterConstraintInternal(wqueue, cmdcon, conrel, tgrel, childrel,
- childtup, recurse, otherrelids, lockmode);
+
+ ATExecAlterConstrDeferrability(wqueue, cmdcon, conrel, tgrel, childrel,
+ childtup, recurse, otherrelids, lockmode);
table_close(childrel, NoLock);
}
--
2.43.5
v17-0003-refactor-Pass-Relid-instead-of-Relation-to-creat.patchapplication/octet-stream; name=v17-0003-refactor-Pass-Relid-instead-of-Relation-to-creat.patchDownload
From 1f436fa1112ef01fe11d11cf6c8b308f25a60785 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 20 Jan 2025 18:49:41 +0530
Subject: [PATCH v17 3/6] refactor: Pass Relid instead of Relation to
createForeignKeyCheckTriggers().
Currently, createForeignKeyCheckTriggers() takes a Relation type as
its first argument, but it doesn't use that argument directly.
Instead, it fetches the Relid by calling RelationGetRelid().
Therefore, it would be more consistent with other functions (e.g.,
createForeignKeyCheckTriggers()) to pass the Relid directly instead of
the whole Relation.
---
src/backend/commands/tablecmds.c | 20 ++++++++++----------
1 file changed, 10 insertions(+), 10 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index cc462aa831b..ae8c59f8147 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -580,7 +580,7 @@ static void createForeignKeyCheckTriggers(Oid myRelOid, Oid refRelOid,
Oid indexOid,
Oid parentInsTrigger, Oid parentUpdTrigger,
Oid *insertTrigOid, Oid *updateTrigOid);
-static void createForeignKeyActionTriggers(Relation rel, Oid refRelOid,
+static void createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid,
Constraint *fkconstraint, Oid constraintOid,
Oid indexOid,
Oid parentDelTrigger, Oid parentUpdTrigger,
@@ -10690,7 +10690,8 @@ addFkRecurseReferenced(Constraint *fkconstraint, Relation rel,
/*
* Create the action triggers that enforce the constraint.
*/
- createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
+ createForeignKeyActionTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
fkconstraint,
parentConstr, indexOid,
parentDelTrigger, parentUpdTrigger,
@@ -13219,10 +13220,11 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
* *updateTrigOid.
*/
static void
-createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstraint,
- Oid constraintOid, Oid indexOid,
- Oid parentDelTrigger, Oid parentUpdTrigger,
- Oid *deleteTrigOid, Oid *updateTrigOid)
+createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid,
+ Constraint *fkconstraint, Oid constraintOid,
+ Oid indexOid, Oid parentDelTrigger,
+ Oid parentUpdTrigger, Oid *deleteTrigOid,
+ Oid *updateTrigOid)
{
CreateTrigStmt *fk_trigger;
ObjectAddress trigAddress;
@@ -13278,8 +13280,7 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
break;
}
- trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid,
- RelationGetRelid(rel),
+ trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid, myRelOid,
constraintOid, indexOid, InvalidOid,
parentDelTrigger, NULL, true, false);
if (deleteTrigOid)
@@ -13339,8 +13340,7 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
break;
}
- trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid,
- RelationGetRelid(rel),
+ trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid, myRelOid,
constraintOid, indexOid, InvalidOid,
parentUpdTrigger, NULL, true, false);
if (updateTrigOid)
--
2.43.5
v17-0004-Remove-hastriggers-flag-check-before-fetching-FK.patchapplication/octet-stream; name=v17-0004-Remove-hastriggers-flag-check-before-fetching-FK.patchDownload
From aabdd92e9d2b3eb28e43c2110fbf773082b990f4 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 7 Oct 2024 12:40:07 +0530
Subject: [PATCH v17 4/6] Remove hastriggers flag check before fetching FK
constraints.
With NOT ENFORCED, foreign key constraints will be created without
triggers. Therefore, the criteria for fetching foreign keys based on
the presence of triggers no longer apply.
----
NOTE: This patch is intended to reduce the diff noise from the main
patch and is not meant to be committed separately. It should be
squashed with the main patch that adds ENFORCED/NOT ENFORCED.
----
---
src/backend/utils/cache/relcache.c | 5 -
src/bin/pg_dump/pg_dump.c | 8 +-
src/bin/psql/describe.c | 226 ++++++++++++++---------------
3 files changed, 108 insertions(+), 131 deletions(-)
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 9f54a9e72b7..e6721056536 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4666,11 +4666,6 @@ RelationGetFKeyList(Relation relation)
if (relation->rd_fkeyvalid)
return relation->rd_fkeylist;
- /* Fast path: non-partitioned tables without triggers can't have FKs */
- if (!relation->rd_rel->relhastriggers &&
- relation->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
- return NIL;
-
/*
* We build the list we intend to return (in the caller's context) while
* doing the scan. After successfully completing the scan, we copy that
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index c371570501a..59b2f513101 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -7973,13 +7973,7 @@ getConstraints(Archive *fout, TableInfo tblinfo[], int numTables)
{
TableInfo *tinfo = &tblinfo[i];
- /*
- * For partitioned tables, foreign keys have no triggers so they must
- * be included anyway in case some foreign keys are defined.
- */
- if ((!tinfo->hastriggers &&
- tinfo->relkind != RELKIND_PARTITIONED_TABLE) ||
- !(tinfo->dobj.dump & DUMP_COMPONENT_DEFINITION))
+ if (!(tinfo->dobj.dump & DUMP_COMPONENT_DEFINITION))
continue;
/* OK, we need info for this table */
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..a23abb2417c 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2550,136 +2550,124 @@ describeOneTableDetails(const char *schemaname,
PQclear(result);
}
- /*
- * Print foreign-key constraints (there are none if no triggers,
- * except if the table is partitioned, in which case the triggers
- * appear in the partitions)
- */
- if (tableinfo.hastriggers ||
- tableinfo.relkind == RELKIND_PARTITIONED_TABLE)
+ /* Print foreign-key constraints */
+ if (pset.sversion >= 120000 &&
+ (tableinfo.ispartition || tableinfo.relkind == RELKIND_PARTITIONED_TABLE))
{
- if (pset.sversion >= 120000 &&
- (tableinfo.ispartition || tableinfo.relkind == RELKIND_PARTITIONED_TABLE))
+ /*
+ * Put the constraints defined in this table first, followed
+ * by the constraints defined in ancestor partitioned tables.
+ */
+ printfPQExpBuffer(&buf,
+ "SELECT conrelid = '%s'::pg_catalog.regclass AS sametable,\n"
+ " conname,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef,\n"
+ " conrelid::pg_catalog.regclass AS ontable\n"
+ " FROM pg_catalog.pg_constraint,\n"
+ " pg_catalog.pg_partition_ancestors('%s')\n"
+ " WHERE conrelid = relid AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
+ "ORDER BY sametable DESC, conname;",
+ oid, oid);
+ }
+ else
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT true as sametable, conname,\n"
+ " pg_catalog.pg_get_constraintdef(r.oid, true) as condef,\n"
+ " conrelid::pg_catalog.regclass AS ontable\n"
+ "FROM pg_catalog.pg_constraint r\n"
+ "WHERE r.conrelid = '%s' AND r.contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n",
+ oid);
+
+ if (pset.sversion >= 120000)
+ appendPQExpBufferStr(&buf, " AND conparentid = 0\n");
+ appendPQExpBufferStr(&buf, "ORDER BY conname");
+ }
+
+ result = PSQLexec(buf.data);
+ if (!result)
+ goto error_return;
+ else
+ tuples = PQntuples(result);
+
+ if (tuples > 0)
+ {
+ int i_sametable = PQfnumber(result, "sametable"),
+ i_conname = PQfnumber(result, "conname"),
+ i_condef = PQfnumber(result, "condef"),
+ i_ontable = PQfnumber(result, "ontable");
+
+ printTableAddFooter(&cont, _("Foreign-key constraints:"));
+ for (i = 0; i < tuples; i++)
{
/*
- * Put the constraints defined in this table first, followed
- * by the constraints defined in ancestor partitioned tables.
+ * Print untranslated constraint name and definition. Use
+ * a "TABLE tab" prefix when the constraint is defined in
+ * a parent partitioned table.
*/
- printfPQExpBuffer(&buf,
- "SELECT conrelid = '%s'::pg_catalog.regclass AS sametable,\n"
- " conname,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef,\n"
- " conrelid::pg_catalog.regclass AS ontable\n"
- " FROM pg_catalog.pg_constraint,\n"
- " pg_catalog.pg_partition_ancestors('%s')\n"
- " WHERE conrelid = relid AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
- "ORDER BY sametable DESC, conname;",
- oid, oid);
- }
- else
- {
- printfPQExpBuffer(&buf,
- "SELECT true as sametable, conname,\n"
- " pg_catalog.pg_get_constraintdef(r.oid, true) as condef,\n"
- " conrelid::pg_catalog.regclass AS ontable\n"
- "FROM pg_catalog.pg_constraint r\n"
- "WHERE r.conrelid = '%s' AND r.contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n",
- oid);
-
- if (pset.sversion >= 120000)
- appendPQExpBufferStr(&buf, " AND conparentid = 0\n");
- appendPQExpBufferStr(&buf, "ORDER BY conname");
- }
-
- result = PSQLexec(buf.data);
- if (!result)
- goto error_return;
- else
- tuples = PQntuples(result);
-
- if (tuples > 0)
- {
- int i_sametable = PQfnumber(result, "sametable"),
- i_conname = PQfnumber(result, "conname"),
- i_condef = PQfnumber(result, "condef"),
- i_ontable = PQfnumber(result, "ontable");
-
- printTableAddFooter(&cont, _("Foreign-key constraints:"));
- for (i = 0; i < tuples; i++)
- {
- /*
- * Print untranslated constraint name and definition. Use
- * a "TABLE tab" prefix when the constraint is defined in
- * a parent partitioned table.
- */
- if (strcmp(PQgetvalue(result, i, i_sametable), "f") == 0)
- printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
- PQgetvalue(result, i, i_ontable),
- PQgetvalue(result, i, i_conname),
- PQgetvalue(result, i, i_condef));
- else
- printfPQExpBuffer(&buf, " \"%s\" %s",
- PQgetvalue(result, i, i_conname),
- PQgetvalue(result, i, i_condef));
-
- printTableAddFooter(&cont, buf.data);
- }
- }
- PQclear(result);
- }
-
- /* print incoming foreign-key references */
- if (tableinfo.hastriggers ||
- tableinfo.relkind == RELKIND_PARTITIONED_TABLE)
- {
- if (pset.sversion >= 120000)
- {
- printfPQExpBuffer(&buf,
- "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
- " FROM pg_catalog.pg_constraint c\n"
- " WHERE confrelid IN (SELECT pg_catalog.pg_partition_ancestors('%s')\n"
- " UNION ALL VALUES ('%s'::pg_catalog.regclass))\n"
- " AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
- "ORDER BY conname;",
- oid, oid);
- }
- else
- {
- printfPQExpBuffer(&buf,
- "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
- " FROM pg_catalog.pg_constraint\n"
- " WHERE confrelid = %s AND contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n"
- "ORDER BY conname;",
- oid);
- }
-
- result = PSQLexec(buf.data);
- if (!result)
- goto error_return;
- else
- tuples = PQntuples(result);
-
- if (tuples > 0)
- {
- int i_conname = PQfnumber(result, "conname"),
- i_ontable = PQfnumber(result, "ontable"),
- i_condef = PQfnumber(result, "condef");
-
- printTableAddFooter(&cont, _("Referenced by:"));
- for (i = 0; i < tuples; i++)
- {
+ if (strcmp(PQgetvalue(result, i, i_sametable), "f") == 0)
printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
PQgetvalue(result, i, i_ontable),
PQgetvalue(result, i, i_conname),
PQgetvalue(result, i, i_condef));
+ else
+ printfPQExpBuffer(&buf, " \"%s\" %s",
+ PQgetvalue(result, i, i_conname),
+ PQgetvalue(result, i, i_condef));
- printTableAddFooter(&cont, buf.data);
- }
+ printTableAddFooter(&cont, buf.data);
}
- PQclear(result);
}
+ PQclear(result);
+
+ /* print incoming foreign-key references */
+ if (pset.sversion >= 120000)
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
+ " FROM pg_catalog.pg_constraint c\n"
+ " WHERE confrelid IN (SELECT pg_catalog.pg_partition_ancestors('%s')\n"
+ " UNION ALL VALUES ('%s'::pg_catalog.regclass))\n"
+ " AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
+ "ORDER BY conname;",
+ oid, oid);
+ }
+ else
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
+ " FROM pg_catalog.pg_constraint\n"
+ " WHERE confrelid = %s AND contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n"
+ "ORDER BY conname;",
+ oid);
+ }
+
+ result = PSQLexec(buf.data);
+ if (!result)
+ goto error_return;
+ else
+ tuples = PQntuples(result);
+
+ if (tuples > 0)
+ {
+ int i_conname = PQfnumber(result, "conname"),
+ i_ontable = PQfnumber(result, "ontable"),
+ i_condef = PQfnumber(result, "condef");
+
+ printTableAddFooter(&cont, _("Referenced by:"));
+ for (i = 0; i < tuples; i++)
+ {
+ printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
+ PQgetvalue(result, i, i_ontable),
+ PQgetvalue(result, i, i_conname),
+ PQgetvalue(result, i, i_condef));
+
+ printTableAddFooter(&cont, buf.data);
+ }
+ }
+ PQclear(result);
/* print any row-level policies */
if (pset.sversion >= 90500)
--
2.43.5
v17-0005-Add-support-for-NOT-ENFORCED-in-foreign-key-cons.patchapplication/octet-stream; name=v17-0005-Add-support-for-NOT-ENFORCED-in-foreign-key-cons.patchDownload
From a20f03cce48828a3151d9779f53fdc0567a1adaf Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Fri, 7 Feb 2025 09:41:20 +0530
Subject: [PATCH v17 5/6] Add support for NOT ENFORCED in foreign key
constraints.
Typically, when a foreign key (FK) constraint is created on a table,
action and check triggers are added to maintain data integrity. With
this patch, if a constraint is marked as NOT ENFORCED, integrity
checks are no longer required, making these triggers unnecessary.
Consequently, when creating a NOT ENFORCED FK constraint, triggers
will not be created, and the constraint will be marked as NOT VALID.
Similarly, if an existing FK constraint is changed to NOT ENFORCED,
the associated triggers will be dropped, and the constraint will also
be marked as NOT VALID. Conversely, if a NOT ENFORCED FK constraint is
changed to ENFORCED, the necessary triggers will be created, and the
constraint will remain in the NOT VALID state.
----
NOTE: In this patch, the tryAttachPartitionForeignKey() function will
not merge the constraint if the enforcibility differs. This will be
addressed in the next patch.
----
---
doc/src/sgml/catalogs.sgml | 2 +-
doc/src/sgml/ref/alter_table.sgml | 5 +-
doc/src/sgml/ref/create_table.sgml | 2 +-
src/backend/catalog/pg_constraint.c | 5 +-
src/backend/catalog/sql_features.txt | 2 +-
src/backend/commands/tablecmds.c | 449 ++++++++++++++++++----
src/backend/parser/gram.y | 12 +-
src/backend/parser/parse_utilcmd.c | 14 +-
src/backend/utils/cache/relcache.c | 1 +
src/include/nodes/parsenodes.h | 2 +
src/include/utils/rel.h | 3 +
src/test/regress/expected/constraints.out | 8 +-
src/test/regress/expected/foreign_key.out | 161 +++++++-
src/test/regress/expected/inherit.out | 83 ++--
src/test/regress/sql/foreign_key.sql | 107 +++++-
src/test/regress/sql/inherit.sql | 9 +-
16 files changed, 721 insertions(+), 144 deletions(-)
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index fb050635551..014bb815665 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2620,7 +2620,7 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l
</para>
<para>
Is the constraint enforced?
- Currently, can be false only for CHECK constraints
+ Currently, can be false only for foreign keys and CHECK constraints
</para></entry>
</row>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index dceb7a7593c..4174ca94667 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -58,7 +58,7 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable>
ALTER [ COLUMN ] <replaceable class="parameter">column_name</replaceable> SET COMPRESSION <replaceable class="parameter">compression_method</replaceable>
ADD <replaceable class="parameter">table_constraint</replaceable> [ NOT VALID ]
ADD <replaceable class="parameter">table_constraint_using_index</replaceable>
- ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+ ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]
ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> SET [ INHERIT | NO INHERIT ]
VALIDATE CONSTRAINT <replaceable class="parameter">constraint_name</replaceable>
DROP CONSTRAINT [ IF EXISTS ] <replaceable class="parameter">constraint_name</replaceable> [ RESTRICT | CASCADE ]
@@ -589,7 +589,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
This form validates a foreign key or check constraint that was
previously created as <literal>NOT VALID</literal>, by scanning the
table to ensure there are no rows for which the constraint is not
- satisfied. Nothing happens if the constraint is already marked valid.
+ satisfied. If the constraint is not enforced, an error is thrown.
+ Nothing happens if the constraint is already marked valid.
(See <xref linkend="sql-altertable-notes"/> below for an explanation
of the usefulness of this command.)
</para>
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index 5304b738322..2b23942a51d 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -1409,7 +1409,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</para>
<para>
- This is currently only supported for <literal>CHECK</literal>
+ This is currently only supported for foreign key and <literal>CHECK</literal>
constraints.
</para>
</listitem>
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index ac80652baf2..0467e7442ff 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -100,8 +100,9 @@ CreateConstraintEntry(const char *constraintName,
ObjectAddresses *addrs_auto;
ObjectAddresses *addrs_normal;
- /* Only CHECK constraint can be not enforced */
- Assert(isEnforced || constraintType == CONSTRAINT_CHECK);
+ /* Only CHECK or FOREIGN KEY constraint can be not enforced */
+ Assert(isEnforced || constraintType == CONSTRAINT_CHECK ||
+ constraintType == CONSTRAINT_FOREIGN);
/* NOT ENFORCED constraint must be NOT VALID */
Assert(isEnforced || !isValidated);
diff --git a/src/backend/catalog/sql_features.txt b/src/backend/catalog/sql_features.txt
index 2f250d2c57b..ebe85337c28 100644
--- a/src/backend/catalog/sql_features.txt
+++ b/src/backend/catalog/sql_features.txt
@@ -281,7 +281,7 @@ F461 Named character sets NO
F471 Scalar subquery values YES
F481 Expanded NULL predicate YES
F491 Constraint management YES
-F492 Optional table constraint enforcement NO check constraints only
+F492 Optional table constraint enforcement YES except not-null constraints
F501 Features and conformance views YES
F501 Features and conformance views 01 SQL_FEATURES view YES
F501 Features and conformance views 02 SQL_SIZING view YES
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index ae8c59f8147..7bf28152fa0 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -395,6 +395,14 @@ static ObjectAddress ATExecAlterConstraint(List **wqueue, Relation rel,
static bool ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon, Relation conrel,
Relation tgrel, Relation rel, HeapTuple contuple,
bool recurse, LOCKMODE lockmode);
+static bool ATExecAlterConstrEnforceability(List **wqueue, ATAlterConstraint *cmdcon,
+ Relation conrel, Relation tgrel,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, LOCKMODE lockmode,
+ Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger);
static bool ATExecAlterConstrDeferrability(List **wqueue, ATAlterConstraint *cmdcon,
Relation conrel, Relation tgrel, Relation rel,
HeapTuple contuple, bool recurse,
@@ -405,6 +413,14 @@ static bool ATExecAlterConstrInheritability(List **wqueue, ATAlterConstraint *cm
static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
bool deferrable, bool initdeferred,
List **otherrelids);
+static void AlterConstrEnforceabilityRecurse(List **wqueue, ATAlterConstraint *cmdcon,
+ Relation conrel, Relation tgrel,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, LOCKMODE lockmode,
+ Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger);
static void AlterConstrDeferrabilityRecurse(List **wqueue, ATAlterConstraint *cmdcon,
Relation conrel, Relation tgrel, Relation rel,
HeapTuple contuple, bool recurse,
@@ -10563,7 +10579,7 @@ addFkConstraint(addFkConstraintSides fkside,
CONSTRAINT_FOREIGN,
fkconstraint->deferrable,
fkconstraint->initdeferred,
- true, /* Is Enforced */
+ fkconstraint->is_enforced,
fkconstraint->initially_valid,
parentConstr,
RelationGetRelid(rel),
@@ -10681,21 +10697,23 @@ addFkRecurseReferenced(Constraint *fkconstraint, Relation rel,
Oid parentDelTrigger, Oid parentUpdTrigger,
bool with_period)
{
- Oid deleteTriggerOid,
- updateTriggerOid;
+ Oid deleteTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
Assert(CheckRelationLockedByMe(pkrel, ShareRowExclusiveLock, true));
Assert(CheckRelationLockedByMe(rel, ShareRowExclusiveLock, true));
/*
- * Create the action triggers that enforce the constraint.
+ * Create action triggers to enforce the constraint, or skip them if the
+ * constraint is not enforced.
*/
- createForeignKeyActionTriggers(RelationGetRelid(rel),
- RelationGetRelid(pkrel),
- fkconstraint,
- parentConstr, indexOid,
- parentDelTrigger, parentUpdTrigger,
- &deleteTriggerOid, &updateTriggerOid);
+ if (fkconstraint->is_enforced)
+ createForeignKeyActionTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
+ fkconstraint,
+ parentConstr, indexOid,
+ parentDelTrigger, parentUpdTrigger,
+ &deleteTriggerOid, &updateTriggerOid);
/*
* If the referenced table is partitioned, recurse on ourselves to handle
@@ -10816,8 +10834,8 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
Oid parentInsTrigger, Oid parentUpdTrigger,
bool with_period)
{
- Oid insertTriggerOid,
- updateTriggerOid;
+ Oid insertTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
Assert(OidIsValid(parentConstr));
Assert(CheckRelationLockedByMe(rel, ShareRowExclusiveLock, true));
@@ -10829,29 +10847,32 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
errmsg("foreign key constraints are not supported on foreign tables")));
/*
- * Add the check triggers to it and, if necessary, schedule it to be
- * checked in Phase 3.
+ * Add check triggers if the constraint is enforced, and if needed,
+ * schedule them to be checked in Phase 3.
*
* If the relation is partitioned, drill down to do it to its partitions.
*/
- createForeignKeyCheckTriggers(RelationGetRelid(rel),
- RelationGetRelid(pkrel),
- fkconstraint,
- parentConstr,
- indexOid,
- parentInsTrigger, parentUpdTrigger,
- &insertTriggerOid, &updateTriggerOid);
+ if (fkconstraint->is_enforced)
+ createForeignKeyCheckTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
+ fkconstraint,
+ parentConstr,
+ indexOid,
+ parentInsTrigger, parentUpdTrigger,
+ &insertTriggerOid, &updateTriggerOid);
if (rel->rd_rel->relkind == RELKIND_RELATION)
{
/*
* Tell Phase 3 to check that the constraint is satisfied by existing
- * rows. We can skip this during table creation, when requested
- * explicitly by specifying NOT VALID in an ADD FOREIGN KEY command,
- * and when we're recreating a constraint following a SET DATA TYPE
- * operation that did not impugn its validity.
+ * rows. We can skip this during table creation, when constraint is
+ * specified as NOT ENFORCED, or when requested explicitly by
+ * specifying NOT VALID in an ADD FOREIGN KEY command, and when we're
+ * recreating a constraint following a SET DATA TYPE operation that
+ * did not impugn its validity.
*/
- if (wqueue && !old_check_ok && !fkconstraint->skip_validation)
+ if (wqueue && !old_check_ok && !fkconstraint->skip_validation &&
+ fkconstraint->is_enforced)
{
NewConstraint *newcon;
AlteredTableInfo *tab;
@@ -11082,8 +11103,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
AttrNumber confdelsetcols[INDEX_MAX_KEYS];
Constraint *fkconstraint;
ObjectAddress address;
- Oid deleteTriggerOid,
- updateTriggerOid;
+ Oid deleteTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(constrOid));
if (!HeapTupleIsValid(tuple))
@@ -11143,8 +11164,9 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
fkconstraint->fk_del_set_cols = NIL;
fkconstraint->old_conpfeqop = NIL;
fkconstraint->old_pktable_oid = InvalidOid;
+ fkconstraint->is_enforced = constrForm->conenforced;
fkconstraint->skip_validation = false;
- fkconstraint->initially_valid = true;
+ fkconstraint->initially_valid = constrForm->convalidated;
/* set up colnames that are used to generate the constraint name */
for (int i = 0; i < numfks; i++)
@@ -11172,9 +11194,10 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
* parent OIDs for similar triggers that will be created on the
* partition in addFkRecurseReferenced().
*/
- GetForeignKeyActionTriggers(trigrel, constrOid,
- constrForm->confrelid, constrForm->conrelid,
- &deleteTriggerOid, &updateTriggerOid);
+ if (constrForm->conenforced)
+ GetForeignKeyActionTriggers(trigrel, constrOid,
+ constrForm->confrelid, constrForm->conrelid,
+ &deleteTriggerOid, &updateTriggerOid);
/* Add this constraint ... */
address = addFkConstraint(addFkReferencedSide,
@@ -11307,8 +11330,8 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
Oid indexOid;
ObjectAddress address;
ListCell *lc;
- Oid insertTriggerOid,
- updateTriggerOid;
+ Oid insertTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
bool with_period;
tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(parentConstrOid));
@@ -11340,17 +11363,18 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
mapped_conkey[i] = attmap->attnums[conkey[i] - 1];
/*
- * Get the "check" triggers belonging to the constraint to pass as
- * parent OIDs for similar triggers that will be created on the
- * partition in addFkRecurseReferencing(). They are also passed to
- * tryAttachPartitionForeignKey() below to simply assign as parents to
- * the partition's existing "check" triggers, that is, if the
- * corresponding constraints is deemed attachable to the parent
- * constraint.
+ * Get the "check" triggers belonging to the constraint, if it is
+ * enforced, to pass as parent OIDs for similar triggers that will be
+ * created on the partition in addFkRecurseReferencing(). They are
+ * also passed to tryAttachPartitionForeignKey() below to simply
+ * assign as parents to the partition's existing "check" triggers,
+ * that is, if the corresponding constraints is deemed attachable to
+ * the parent constraint.
*/
- GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
- constrForm->confrelid, constrForm->conrelid,
- &insertTriggerOid, &updateTriggerOid);
+ if (constrForm->conenforced)
+ GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
+ constrForm->confrelid, constrForm->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
/*
* Before creating a new constraint, see whether any existing FKs are
@@ -11403,6 +11427,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
fkconstraint->fk_del_set_cols = NIL;
fkconstraint->old_conpfeqop = NIL;
fkconstraint->old_pktable_oid = InvalidOid;
+ fkconstraint->is_enforced = constrForm->conenforced;
fkconstraint->skip_validation = false;
fkconstraint->initially_valid = constrForm->convalidated;
for (int i = 0; i < numfks; i++)
@@ -11520,6 +11545,7 @@ tryAttachPartitionForeignKey(List **wqueue,
if (OidIsValid(partConstr->conparentid) ||
partConstr->condeferrable != parentConstr->condeferrable ||
partConstr->condeferred != parentConstr->condeferred ||
+ partConstr->conenforced != parentConstr->conenforced ||
partConstr->confupdtype != parentConstr->confupdtype ||
partConstr->confdeltype != parentConstr->confdeltype ||
partConstr->confmatchtype != parentConstr->confmatchtype)
@@ -11563,8 +11589,7 @@ AttachPartitionForeignKey(List **wqueue,
bool queueValidation;
Oid partConstrFrelid;
Oid partConstrRelid;
- Oid insertTriggerOid,
- updateTriggerOid;
+ bool parentConstrIsEnforced;
/* Fetch the parent constraint tuple */
parentConstrTup = SearchSysCache1(CONSTROID,
@@ -11572,6 +11597,7 @@ AttachPartitionForeignKey(List **wqueue,
if (!HeapTupleIsValid(parentConstrTup))
elog(ERROR, "cache lookup failed for constraint %u", parentConstrOid);
parentConstr = (Form_pg_constraint) GETSTRUCT(parentConstrTup);
+ parentConstrIsEnforced = parentConstr->conenforced;
/* Fetch the child constraint tuple */
partcontup = SearchSysCache1(CONSTROID,
@@ -11621,17 +11647,24 @@ AttachPartitionForeignKey(List **wqueue,
/*
* Like the constraint, attach partition's "check" triggers to the
- * corresponding parent triggers.
+ * corresponding parent triggers if the constraint is enforced.
+ * Non-enforced constraints do not have these triggers.
*/
- GetForeignKeyCheckTriggers(trigrel,
- partConstrOid, partConstrFrelid, partConstrRelid,
- &insertTriggerOid, &updateTriggerOid);
- Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
- TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
- RelationGetRelid(partition));
- Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
- TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
- RelationGetRelid(partition));
+ if (parentConstrIsEnforced)
+ {
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
+ GetForeignKeyCheckTriggers(trigrel,
+ partConstrOid, partConstrFrelid, partConstrRelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
+ RelationGetRelid(partition));
+ Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
+ RelationGetRelid(partition));
+ }
/*
* We updated this pg_constraint row above to set its parent; validating
@@ -11745,6 +11778,10 @@ RemoveInheritedConstraint(Relation conrel, Relation trigrel, Oid conoid,
*
* The subroutine for tryAttachPartitionForeignKey handles the deletion of
* action triggers for the foreign key constraint.
+ *
+ * If valid confrelid and conrelid values are not provided, the respective
+ * trigger check will be skipped, and the trigger will be considered for
+ * removal.
*/
static void
DropForeignKeyConstraintTriggers(Relation trigrel, Oid conoid, Oid confrelid,
@@ -11765,10 +11802,27 @@ DropForeignKeyConstraintTriggers(Relation trigrel, Oid conoid, Oid confrelid,
Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
ObjectAddress trigger;
- if (trgform->tgconstrrelid != conrelid)
+ /* Invalid if trigger is not for a referential integrity constraint */
+ if (!OidIsValid(trgform->tgconstrrelid))
continue;
- if (trgform->tgrelid != confrelid)
+ if (OidIsValid(conrelid) && trgform->tgconstrrelid != conrelid)
continue;
+ if (OidIsValid(confrelid) && trgform->tgrelid != confrelid)
+ continue;
+
+ /* We should be droping trigger related to foreign key constraint */
+ Assert(trgform->tgfoid == F_RI_FKEY_CHECK_INS ||
+ trgform->tgfoid == F_RI_FKEY_CHECK_UPD ||
+ trgform->tgfoid == F_RI_FKEY_CASCADE_DEL ||
+ trgform->tgfoid == F_RI_FKEY_CASCADE_UPD ||
+ trgform->tgfoid == F_RI_FKEY_RESTRICT_DEL ||
+ trgform->tgfoid == F_RI_FKEY_RESTRICT_UPD ||
+ trgform->tgfoid == F_RI_FKEY_SETNULL_DEL ||
+ trgform->tgfoid == F_RI_FKEY_SETNULL_UPD ||
+ trgform->tgfoid == F_RI_FKEY_SETDEFAULT_DEL ||
+ trgform->tgfoid == F_RI_FKEY_SETDEFAULT_UPD ||
+ trgform->tgfoid == F_RI_FKEY_NOACTION_DEL ||
+ trgform->tgfoid == F_RI_FKEY_NOACTION_UPD);
/*
* The constraint is originally set up to contain this trigger as an
@@ -11981,6 +12035,11 @@ ATExecAlterConstraint(List **wqueue, Relation rel, ATAlterConstraint *cmdcon,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("constraint \"%s\" of relation \"%s\" is not a foreign key constraint",
cmdcon->conname, RelationGetRelationName(rel))));
+ if (cmdcon->alterEnforceability && currcon->contype != CONSTRAINT_FOREIGN)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot alter enforceability of constraint \"%s\" of relation \"%s\"",
+ cmdcon->conname, RelationGetRelationName(rel))));
if (cmdcon->alterInheritability &&
currcon->contype != CONSTRAINT_NOTNULL)
ereport(ERROR,
@@ -12060,7 +12119,7 @@ ATExecAlterConstraint(List **wqueue, Relation rel, ATAlterConstraint *cmdcon,
/*
* A subroutine of ATExecAlterConstraint that calls the respective routines for
- * altering constraint attributes.
+ * altering constraint's enforceability, deferrability or inheritability.
*/
static bool
ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon,
@@ -12068,16 +12127,35 @@ ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon,
HeapTuple contuple, bool recurse,
LOCKMODE lockmode)
{
+ Form_pg_constraint currcon;
bool changed = false;
List *otherrelids = NIL;
+ currcon = (Form_pg_constraint) GETSTRUCT(contuple);
+
/*
- * Do the catalog work for the deferrability change, recurse if necessary.
+ * Do the catalog work for the enforceability or deferrability change,
+ * recurse if necessary.
+ *
+ * Note that even if deferrability is requested to be altered along with
+ * enforceability, we don't need to explicitly update multiple entries in
+ * pg_trigger related to deferrability.
+ *
+ * Modifying enforceability involves either creating or dropping the
+ * trigger, during which the deferrability setting will be adjusted
+ * automatically.
*/
- if (cmdcon->alterDeferrability &&
- ATExecAlterConstrDeferrability(wqueue, cmdcon, conrel, tgrel, rel,
- contuple, recurse, &otherrelids,
- lockmode))
+ if (cmdcon->alterEnforceability &&
+ ATExecAlterConstrEnforceability(wqueue, cmdcon, conrel, tgrel,
+ currcon->conrelid, currcon->confrelid,
+ contuple, lockmode, InvalidOid,
+ InvalidOid, InvalidOid, InvalidOid))
+ changed = true;
+
+ else if (cmdcon->alterDeferrability &&
+ ATExecAlterConstrDeferrability(wqueue, cmdcon, conrel, tgrel, rel,
+ contuple, recurse, &otherrelids,
+ lockmode))
{
/*
* UpdateConstraintEntry already invalidated relcache for the
@@ -12101,6 +12179,151 @@ ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon,
return changed;
}
+/*
+ * Returns true if the constraint's enforceability is altered.
+ *
+ * Depending on whether the constraint is being set to enforced or not
+ * enforced, it creates or drops the trigger accordingly.
+ *
+ * Note that we must recurse even when trying to change a 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." 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.
+ */
+static bool
+ATExecAlterConstrEnforceability(List **wqueue, ATAlterConstraint *cmdcon,
+ Relation conrel, Relation tgrel,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, LOCKMODE lockmode,
+ Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger)
+{
+ Form_pg_constraint currcon;
+ Oid conoid;
+ Relation rel;
+ bool changed = false;
+
+ /* Since this function recurses, it could be driven to stack overflow */
+ check_stack_depth();
+
+ Assert(cmdcon->alterEnforceability);
+
+ currcon = (Form_pg_constraint) GETSTRUCT(contuple);
+ conoid = currcon->oid;
+
+ /* Should be foreign key constraint */
+ Assert(currcon->contype == CONSTRAINT_FOREIGN);
+
+ rel = table_open(currcon->conrelid, lockmode);
+
+ if (currcon->conenforced != cmdcon->is_enforced)
+ {
+ UpdateConstraintEntry(cmdcon, conrel, contuple);
+ changed = true;
+ }
+
+ /* Drop triggers */
+ if (!cmdcon->is_enforced)
+ {
+ /*
+ * When setting a constraint to NOT ENFORCED, the constraint triggers
+ * need to be dropped. Therefore, we must process the child relations
+ * first, followed by the parent, to account for dependencies.
+ */
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE)
+ AlterConstrEnforceabilityRecurse(wqueue, cmdcon, conrel, tgrel,
+ fkrelid, pkrelid, contuple,
+ lockmode, InvalidOid, InvalidOid,
+ InvalidOid, InvalidOid);
+
+ /* Drop all the triggers */
+ DropForeignKeyConstraintTriggers(tgrel, conoid, InvalidOid, InvalidOid);
+ }
+ else if (changed) /* Create triggers */
+ {
+ Oid ReferencedDelTriggerOid = InvalidOid,
+ ReferencedUpdTriggerOid = InvalidOid,
+ ReferencingInsTriggerOid = InvalidOid,
+ ReferencingUpdTriggerOid = InvalidOid;
+
+ /* Prepare the minimal information required for trigger creation. */
+ Constraint *fkconstraint = makeNode(Constraint);
+
+ fkconstraint->conname = pstrdup(NameStr(currcon->conname));
+ fkconstraint->fk_matchtype = currcon->confmatchtype;
+ fkconstraint->fk_upd_action = currcon->confupdtype;
+ fkconstraint->fk_del_action = currcon->confdeltype;
+
+ /* Create referenced triggers */
+ if (currcon->conrelid == fkrelid)
+ createForeignKeyActionTriggers(currcon->conrelid,
+ currcon->confrelid,
+ fkconstraint,
+ conoid,
+ currcon->conindid,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ &ReferencedDelTriggerOid,
+ &ReferencedUpdTriggerOid);
+
+ /* Create referencing triggers */
+ if (currcon->confrelid == pkrelid)
+ createForeignKeyCheckTriggers(currcon->conrelid,
+ pkrelid,
+ fkconstraint,
+ conoid,
+ currcon->conindid,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger,
+ &ReferencingInsTriggerOid,
+ &ReferencingUpdTriggerOid);
+
+ /*
+ * Tell Phase 3 to check that the constraint is satisfied by existing
+ * rows.
+ */
+ if (rel->rd_rel->relkind == RELKIND_RELATION)
+ {
+ AlteredTableInfo *tab;
+ NewConstraint *newcon;
+
+ newcon = (NewConstraint *) palloc0(sizeof(NewConstraint));
+ newcon->name = fkconstraint->conname;
+ newcon->contype = CONSTR_FOREIGN;
+ newcon->refrelid = currcon->confrelid;
+ newcon->refindid = currcon->conindid;
+ newcon->conid = currcon->oid;
+ newcon->qual = (Node *) fkconstraint;
+
+ /* Find or create work queue entry for this table */
+ tab = ATGetQueueEntry(wqueue, rel);
+ tab->constraints = lappend(tab->constraints, newcon);
+ }
+
+ /*
+ * If the table at either end of the constraint is partitioned, we
+ * need to recurse and create triggers for each constraint that is a
+ * child of this one.
+ */
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE)
+ AlterConstrEnforceabilityRecurse(wqueue, cmdcon, conrel, tgrel,
+ fkrelid, pkrelid, contuple,
+ lockmode, ReferencedDelTriggerOid,
+ ReferencedUpdTriggerOid,
+ ReferencingInsTriggerOid,
+ ReferencingUpdTriggerOid);
+ }
+
+ table_close(rel, NoLock);
+
+ return changed;
+}
+
/*
* Returns true if the constraint's deferrability is altered.
*
@@ -12305,6 +12528,55 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
systable_endscan(tgscan);
}
+/*
+ * Invokes ATExecAlterConstrEnforceability for each constraint that is a child of
+ * the specified constraint.
+ *
+ * Note that this doesn't handle recursion the normal way, viz. by scanning the
+ * list of child relations and recursing; instead it uses the conparentid
+ * relationships. This may need to be reconsidered.
+ *
+ * The arguments to this function have the same meaning as the arguments to
+ * ATExecAlterConstrEnforceability.
+ */
+static void
+AlterConstrEnforceabilityRecurse(List **wqueue, ATAlterConstraint *cmdcon,
+ Relation conrel, Relation tgrel,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, LOCKMODE lockmode,
+ Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger)
+{
+ Form_pg_constraint currcon;
+ Oid conoid;
+ ScanKeyData pkey;
+ SysScanDesc pscan;
+ HeapTuple childtup;
+
+ currcon = (Form_pg_constraint) GETSTRUCT(contuple);
+ conoid = currcon->oid;
+
+ ScanKeyInit(&pkey,
+ Anum_pg_constraint_conparentid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+
+ pscan = systable_beginscan(conrel, ConstraintParentIndexId,
+ true, NULL, 1, &pkey);
+
+ while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
+ ATExecAlterConstrEnforceability(wqueue, cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, childtup, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
+
+ systable_endscan(pscan);
+}
+
/*
* Invokes ATExecAlterConstrDeferrability for each constraint that is a child of
* the specified constraint.
@@ -12365,13 +12637,27 @@ UpdateConstraintEntry(ATAlterConstraint *cmdcon, Relation conrel,
HeapTuple copyTuple;
Form_pg_constraint copy_con;
- Assert(cmdcon->alterDeferrability || cmdcon->alterInheritability);
+ Assert(cmdcon->alterEnforceability || cmdcon->alterDeferrability ||
+ cmdcon->alterInheritability);
currcon = (Form_pg_constraint) GETSTRUCT(contuple);
copyTuple = heap_copytuple(contuple);
copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
+ if (cmdcon->alterEnforceability)
+ {
+ copy_con->conenforced = cmdcon->is_enforced;
+
+ /*
+ * 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.
+ */
+ copy_con->convalidated = cmdcon->is_enforced;
+ }
if (cmdcon->alterDeferrability)
{
copy_con->condeferrable = cmdcon->deferrable;
@@ -20461,8 +20747,6 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
Form_pg_constraint conform;
- Oid insertTriggerOid,
- updateTriggerOid;
contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
if (!HeapTupleIsValid(contup))
@@ -20489,17 +20773,25 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
/*
* Also, look up the partition's "check" triggers corresponding to the
- * constraint being detached and detach them from the parent triggers.
+ * enforced constraint being detached and detach them from the parent
+ * triggers. Non-enforced constraints do not have these triggers;
+ * therefore, this step is not needed.
*/
- GetForeignKeyCheckTriggers(trigrel,
- fk->conoid, fk->confrelid, fk->conrelid,
- &insertTriggerOid, &updateTriggerOid);
- Assert(OidIsValid(insertTriggerOid));
- TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
- RelationGetRelid(partRel));
- Assert(OidIsValid(updateTriggerOid));
- TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
- RelationGetRelid(partRel));
+ if (fk->conenforced)
+ {
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ Assert(OidIsValid(updateTriggerOid));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ }
/*
* Lastly, create the action triggers on the referenced table, using
@@ -20539,8 +20831,9 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
fkconstraint->conname = pstrdup(NameStr(conform->conname));
fkconstraint->deferrable = conform->condeferrable;
fkconstraint->initdeferred = conform->condeferred;
+ fkconstraint->is_enforced = conform->conenforced;
fkconstraint->skip_validation = true;
- fkconstraint->initially_valid = true;
+ fkconstraint->initially_valid = conform->convalidated;
/* a few irrelevant fields omitted here */
fkconstraint->pktable = NULL;
fkconstraint->fk_attrs = NIL;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 271ae26cbaf..1bef387aad4 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -2662,11 +2662,15 @@ alter_table_cmd:
n->subtype = AT_AlterConstraint;
n->def = (Node *) c;
c->conname = $3;
- c->alterDeferrability = true;
+ c->alterEnforceability =
+ ($4 & (CAS_NOT_ENFORCED | CAS_ENFORCED)) != 0;
+ c->alterDeferrability =
+ ($4 & (CAS_NOT_DEFERRABLE | CAS_DEFERRABLE |
+ CAS_INITIALLY_IMMEDIATE | CAS_INITIALLY_IMMEDIATE)) != 0;
processCASbits($4, @4, "FOREIGN KEY",
&c->deferrable,
&c->initdeferred,
- NULL, NULL, NULL, yyscanner);
+ &c->is_enforced, NULL, NULL, yyscanner);
$$ = (Node *) n;
}
/* ALTER TABLE <name> ALTER CONSTRAINT SET INHERIT */
@@ -4344,8 +4348,8 @@ ConstraintElem:
n->fk_del_set_cols = ($11)->deleteAction->cols;
processCASbits($12, @12, "FOREIGN KEY",
&n->deferrable, &n->initdeferred,
- NULL, &n->skip_validation, NULL,
- yyscanner);
+ &n->is_enforced, &n->skip_validation,
+ NULL, yyscanner);
n->initially_valid = !n->skip_validation;
$$ = (Node *) n;
}
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index abbe1bb45a3..45d65359c54 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -2977,8 +2977,10 @@ transformFKConstraints(CreateStmtContext *cxt,
/*
* If CREATE TABLE or adding a column with NULL default, we can safely
- * skip validation of FK constraints, and nonetheless mark them valid.
- * (This will override any user-supplied NOT VALID flag.)
+ * skip validation of FK constraints, and mark them as valid based on the
+ * constraint enforcement flag, since NOT ENFORCED constraints must always
+ * be marked as NOT VALID. (This will override any user-supplied NOT VALID
+ * flag.)
*/
if (skipValidation)
{
@@ -2987,7 +2989,7 @@ transformFKConstraints(CreateStmtContext *cxt,
Constraint *constraint = (Constraint *) lfirst(fkclist);
constraint->skip_validation = true;
- constraint->initially_valid = true;
+ constraint->initially_valid = constraint->is_enforced;
}
}
@@ -3982,7 +3984,8 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
case CONSTR_ATTR_ENFORCED:
if (lastprimarycon == NULL ||
- lastprimarycon->contype != CONSTR_CHECK)
+ (lastprimarycon->contype != CONSTR_CHECK &&
+ lastprimarycon->contype != CONSTR_FOREIGN))
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("misplaced ENFORCED clause"),
@@ -3998,7 +4001,8 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
case CONSTR_ATTR_NOT_ENFORCED:
if (lastprimarycon == NULL ||
- lastprimarycon->contype != CONSTR_CHECK)
+ (lastprimarycon->contype != CONSTR_CHECK &&
+ lastprimarycon->contype != CONSTR_FOREIGN))
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("misplaced NOT ENFORCED clause"),
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index e6721056536..18a14ae186e 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4697,6 +4697,7 @@ RelationGetFKeyList(Relation relation)
info->conoid = constraint->oid;
info->conrelid = constraint->conrelid;
info->confrelid = constraint->confrelid;
+ info->conenforced = constraint->conenforced;
DeconstructFkConstraintRow(htup, &info->nkeys,
info->conkey,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 23c9e3c5abf..9e64e18b501 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2490,6 +2490,8 @@ typedef struct ATAlterConstraint
{
NodeTag type;
char *conname; /* Constraint name */
+ bool alterEnforceability; /* changing enforceability properties? */
+ bool is_enforced; /* ENFORCED? */
bool alterDeferrability; /* changing deferrability properties? */
bool deferrable; /* DEFERRABLE? */
bool initdeferred; /* INITIALLY DEFERRED? */
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index db3e504c3d2..dba0fb70146 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -284,6 +284,9 @@ typedef struct ForeignKeyCacheInfo
/* number of columns in the foreign key */
int nkeys;
+ /* Is enforced ? */
+ bool conenforced;
+
/*
* these arrays each have nkeys valid entries:
*/
diff --git a/src/test/regress/expected/constraints.out b/src/test/regress/expected/constraints.out
index 4f39100fcdf..a719d2f74e9 100644
--- a/src/test/regress/expected/constraints.out
+++ b/src/test/regress/expected/constraints.out
@@ -745,13 +745,9 @@ ERROR: misplaced NOT ENFORCED clause
LINE 1: CREATE TABLE UNIQUE_NOTEN_TBL(i int UNIQUE NOT ENFORCED);
^
ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
-ERROR: FOREIGN KEY constraints cannot be marked ENFORCED
-LINE 1: ...TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
- ^
+ERROR: cannot alter enforceability of constraint "unique_tbl_i_key" of relation "unique_tbl"
ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORCED;
-ERROR: FOREIGN KEY constraints cannot be marked NOT ENFORCED
-LINE 1: ...ABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORC...
- ^
+ERROR: cannot alter enforceability of constraint "unique_tbl_i_key" of relation "unique_tbl"
DROP TABLE unique_tbl;
--
-- EXCLUDE constraints
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 6a3374d5152..39744da7fd6 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -1,12 +1,43 @@
--
-- FOREIGN KEY
--
--- MATCH FULL
+-- NOT ENFORCED
--
-- First test, check and cascade
--
CREATE TABLE PKTABLE ( ptest1 int PRIMARY KEY, ptest2 text );
-CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE, ftest2 int );
+CREATE TABLE FKTABLE ( ftest1 int CONSTRAINT fktable_ftest1_fkey REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE NOT ENFORCED,
+ ftest2 int );
+-- Inserting into the foreign key table will not result in an error, even if
+-- there is no matching key in the referenced table.
+INSERT INTO FKTABLE VALUES (1, 2);
+INSERT INTO FKTABLE VALUES (2, 3);
+-- Check FKTABLE
+SELECT * FROM FKTABLE;
+ ftest1 | ftest2
+--------+--------
+ 1 | 2
+ 2 | 3
+(2 rows)
+
+-- Reverting it back to ENFORCED will result in failure because constraint validation will be triggered,
+-- as it was previously in a valid state.
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_fkey"
+DETAIL: Key (ftest1)=(1) is not present in table "pktable".
+-- Remove any existing rows that violate the constraint, then attempt to change
+-- it.
+TRUNCATE FKTABLE;
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+-- Any further inserts will fail due to the enforcement.
+INSERT INTO FKTABLE VALUES (3, 4);
+ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_fkey"
+DETAIL: Key (ftest1)=(3) is not present in table "pktable".
+--
+-- MATCH FULL
+--
+-- First test, check and cascade
+--
-- Insert test data into PKTABLE
INSERT INTO PKTABLE VALUES (1, 'Test1');
INSERT INTO PKTABLE VALUES (2, 'Test2');
@@ -351,6 +382,43 @@ INSERT INTO FKTABLE VALUES (1, NULL);
ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL;
ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_ftest2_fkey"
DETAIL: MATCH FULL does not allow mixing of null and nonnull key values.
+-- Modifying other attributes of a constraint should not affect its enforceability, and vice versa
+ALTER TABLE FKTABLE ADD CONSTRAINT fk_con FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE NOT VALID NOT ENFORCED;
+ALTER TABLE FKTABLE ALTER CONSTRAINT fk_con DEFERRABLE INITIALLY DEFERRED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fk_con';
+ condeferrable | condeferred | conenforced | convalidated
+---------------+-------------+-------------+--------------
+ t | t | f | f
+(1 row)
+
+ALTER TABLE FKTABLE ALTER CONSTRAINT fk_con NOT ENFORCED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fk_con';
+ condeferrable | condeferred | conenforced | convalidated
+---------------+-------------+-------------+--------------
+ t | t | f | f
+(1 row)
+
+-- Enforceability also changes the validate state, as data validation will be
+-- performed during this transformation.
+ALTER TABLE FKTABLE ALTER CONSTRAINT fk_con ENFORCED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fk_con';
+ condeferrable | condeferred | conenforced | convalidated
+---------------+-------------+-------------+--------------
+ t | t | t | t
+(1 row)
+
+-- Can change enforceability and deferrability together
+ALTER TABLE FKTABLE ALTER CONSTRAINT fk_con NOT ENFORCED NOT DEFERRABLE;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fk_con';
+ condeferrable | condeferred | conenforced | convalidated
+---------------+-------------+-------------+--------------
+ f | f | f | f
+(1 row)
+
DROP TABLE FKTABLE;
DROP TABLE PKTABLE;
-- MATCH SIMPLE
@@ -1276,6 +1344,13 @@ INSERT INTO fktable VALUES (0, 20);
ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_fk_fkey"
DETAIL: Key (fk)=(20) is not present in table "pktable".
COMMIT;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT ENFORCED;
+BEGIN;
+-- doesn't match FK, no error.
+UPDATE pktable SET id = 10 WHERE id = 5;
+-- doesn't match PK, no error.
+INSERT INTO fktable VALUES (0, 20);
+ROLLBACK;
-- try additional syntax
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE;
-- illegal options
@@ -1291,6 +1366,14 @@ ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT VALID;
ERROR: FOREIGN KEY constraints cannot be marked NOT VALID
LINE 1: ...ER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT VALID;
^
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORCED;
+ERROR: conflicting constraint properties
+LINE 1: ...fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORC...
+ ^
+CREATE TEMP TABLE fktable2 (fk int references pktable ENFORCED NOT ENFORCED);
+ERROR: multiple ENFORCED/NOT ENFORCED clauses not allowed
+LINE 1: ...ABLE fktable2 (fk int references pktable ENFORCED NOT ENFORC...
+ ^
-- test order of firing of FK triggers when several RI-induced changes need to
-- be made to the same row. This was broken by subtransaction-related
-- changes in 8.0.
@@ -1588,10 +1671,13 @@ ALTER TABLE fk_partitioned_fk DROP COLUMN fdrop1;
CREATE TABLE fk_partitioned_fk_1 (fdrop1 int, fdrop2 int, a int, fdrop3 int, b int);
ALTER TABLE fk_partitioned_fk_1 DROP COLUMN fdrop1, DROP COLUMN fdrop2, DROP COLUMN fdrop3;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FROM (0,0) TO (1000,1000);
-ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk;
+ALTER TABLE fk_partitioned_fk ADD CONSTRAINT fk_partitioned_fk_a_b_fkey FOREIGN KEY (a, b)
+ REFERENCES fk_notpartitioned_pk NOT ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
+ALTER TABLE fk_partitioned_fk_2 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
CREATE TABLE fk_partitioned_fk_3 (fdrop1 int, fdrop2 int, fdrop3 int, fdrop4 int, b int, a int)
PARTITION BY HASH (a);
ALTER TABLE fk_partitioned_fk_3 DROP COLUMN fdrop1, DROP COLUMN fdrop2,
@@ -1614,10 +1700,10 @@ INSERT INTO fk_partitioned_fk_1 (a,b) VALUES (500, 501);
ERROR: insert or update on table "fk_partitioned_fk_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
DETAIL: Key (a, b)=(500, 501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (1500, 1501);
-ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_2_a_b_fkey"
DETAIL: Key (a, b)=(1500, 1501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk_2 (a,b) VALUES (1500, 1501);
-ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_2_a_b_fkey"
DETAIL: Key (a, b)=(1500, 1501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (2500, 2502);
ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
@@ -1667,6 +1753,37 @@ Indexes:
Referenced by:
TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b)
+-- Check the exsting FK trigger
+CREATE TEMP TABLE tmp_trg_info AS SELECT tgtype, tgrelid::regclass, tgconstraint
+FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+SELECT count(1) FROM tmp_trg_info;
+ count
+-------
+ 14
+(1 row)
+
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+-- No triggers
+SELECT count(1) FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+ count
+-------
+ 0
+(1 row)
+
+-- Changing it back to ENFORCED will recreate the necessary triggers.
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+-- Should be exactly the same number of triggers found as before
+SELECT COUNT(1) FROM pg_trigger pgt JOIN tmp_trg_info tt
+ON (pgt.tgtype = tt.tgtype AND pgt.tgrelid = tt.tgrelid AND pgt.tgconstraint = tt.tgconstraint);
+ count
+-------
+ 14
+(1 row)
+
ALTER TABLE fk_partitioned_fk DROP CONSTRAINT fk_partitioned_fk_a_b_fkey;
-- done.
DROP TABLE fk_notpartitioned_pk, fk_partitioned_fk;
@@ -1964,6 +2081,40 @@ Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
Foreign-key constraints:
TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
+DROP TABLE fk_partitioned_fk_2;
+CREATE TABLE fk_partitioned_fk_2 (b int, a int,
+ FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
+BEGIN;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should be having two constraints
+\d fk_partitioned_fk_2
+ Table "public.fk_partitioned_fk_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ b | integer | | |
+ a | integer | | |
+Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
+Foreign-key constraints:
+ "fk_partitioned_fk_2_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED
+ TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
+
+DROP TABLE fk_partitioned_fk_2;
+ROLLBACK;
+BEGIN;
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have only one constraint
+\d fk_partitioned_fk_2
+ Table "public.fk_partitioned_fk_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ b | integer | | |
+ a | integer | | |
+Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
+Foreign-key constraints:
+ TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED
+
+ROLLBACK;
DROP TABLE fk_partitioned_fk_2;
CREATE TABLE fk_partitioned_fk_4 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE) PARTITION BY RANGE (b, a);
CREATE TABLE fk_partitioned_fk_4_1 PARTITION OF fk_partitioned_fk_4 FOR VALUES FROM (1,1) TO (100,100);
diff --git a/src/test/regress/expected/inherit.out b/src/test/regress/expected/inherit.out
index e671975a281..910668ecc4a 100644
--- a/src/test/regress/expected/inherit.out
+++ b/src/test/regress/expected/inherit.out
@@ -1332,6 +1332,13 @@ NOTICE: merging constraint "inh_check_constraint5" with inherited definition
alter table p1 add constraint inh_check_constraint6 check (f1 < 10) not enforced;
alter table p1_c1 add constraint inh_check_constraint6 check (f1 < 10) enforced;
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 invalid state of the child constraint will be ignored here.
+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
create table p1_c2(f1 int constraint inh_check_constraint4 check (f1 < 10)) inherits(p1);
NOTICE: merging column "f1" with inherited definition
NOTICE: merging constraint "inh_check_constraint4" with inherited definition
@@ -1353,42 +1360,50 @@ create table p1_fail(f1 int constraint inh_check_constraint6 check (f1 < 10) not
NOTICE: merging multiple inherited definitions of column "f1"
NOTICE: merging column "f1" with inherited definition
ERROR: constraint "inh_check_constraint6" conflicts with NOT ENFORCED constraint on relation "p1_fail"
-select conrelid::regclass::text as relname, conname, conislocal, coninhcount, conenforced
+select conrelid::regclass::text as relname, conname, conislocal, coninhcount, conenforced, convalidated
from pg_constraint where conname like 'inh\_check\_constraint%'
order by 1, 2;
- relname | conname | conislocal | coninhcount | conenforced
----------+-----------------------+------------+-------------+-------------
- p1 | inh_check_constraint1 | t | 0 | t
- p1 | inh_check_constraint2 | t | 0 | t
- p1 | inh_check_constraint3 | t | 0 | f
- p1 | inh_check_constraint4 | t | 0 | f
- p1 | inh_check_constraint5 | t | 0 | f
- p1 | inh_check_constraint6 | t | 0 | f
- p1 | inh_check_constraint8 | t | 0 | t
- p1_c1 | inh_check_constraint1 | t | 1 | t
- p1_c1 | inh_check_constraint2 | t | 1 | t
- p1_c1 | inh_check_constraint3 | t | 1 | f
- p1_c1 | inh_check_constraint4 | t | 1 | f
- p1_c1 | inh_check_constraint5 | t | 1 | t
- p1_c1 | inh_check_constraint6 | t | 1 | t
- p1_c1 | inh_check_constraint7 | t | 0 | f
- p1_c1 | inh_check_constraint8 | f | 1 | t
- p1_c2 | inh_check_constraint1 | f | 1 | t
- p1_c2 | inh_check_constraint2 | f | 1 | t
- p1_c2 | inh_check_constraint3 | f | 1 | f
- p1_c2 | inh_check_constraint4 | t | 1 | t
- p1_c2 | inh_check_constraint5 | f | 1 | f
- p1_c2 | inh_check_constraint6 | f | 1 | f
- p1_c2 | inh_check_constraint8 | f | 1 | t
- p1_c3 | inh_check_constraint1 | f | 2 | t
- p1_c3 | inh_check_constraint2 | f | 2 | t
- p1_c3 | inh_check_constraint3 | f | 2 | f
- p1_c3 | inh_check_constraint4 | f | 2 | f
- p1_c3 | inh_check_constraint5 | f | 2 | t
- p1_c3 | inh_check_constraint6 | f | 2 | t
- p1_c3 | inh_check_constraint7 | f | 1 | f
- p1_c3 | inh_check_constraint8 | f | 2 | t
-(30 rows)
+ relname | conname | conislocal | coninhcount | conenforced | convalidated
+---------+------------------------+------------+-------------+-------------+--------------
+ p1 | inh_check_constraint1 | t | 0 | t | t
+ p1 | inh_check_constraint10 | t | 0 | f | f
+ p1 | inh_check_constraint2 | t | 0 | t | t
+ p1 | inh_check_constraint3 | t | 0 | f | f
+ p1 | inh_check_constraint4 | t | 0 | f | f
+ p1 | inh_check_constraint5 | t | 0 | f | f
+ p1 | inh_check_constraint6 | t | 0 | f | f
+ 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_constraint2 | t | 1 | t | t
+ p1_c1 | inh_check_constraint3 | t | 1 | f | f
+ p1_c1 | inh_check_constraint4 | t | 1 | f | f
+ p1_c1 | inh_check_constraint5 | t | 1 | t | t
+ p1_c1 | inh_check_constraint6 | t | 1 | t | t
+ p1_c1 | inh_check_constraint7 | t | 0 | f | f
+ p1_c1 | inh_check_constraint8 | f | 1 | t | t
+ p1_c1 | inh_check_constraint9 | t | 1 | t | f
+ p1_c2 | inh_check_constraint1 | f | 1 | t | t
+ p1_c2 | inh_check_constraint10 | f | 1 | f | f
+ p1_c2 | inh_check_constraint2 | f | 1 | t | t
+ p1_c2 | inh_check_constraint3 | f | 1 | f | f
+ p1_c2 | inh_check_constraint4 | t | 1 | t | t
+ p1_c2 | inh_check_constraint5 | f | 1 | f | f
+ p1_c2 | inh_check_constraint6 | f | 1 | f | f
+ p1_c2 | inh_check_constraint8 | f | 1 | t | t
+ p1_c2 | inh_check_constraint9 | f | 1 | f | f
+ p1_c3 | inh_check_constraint1 | f | 2 | t | t
+ p1_c3 | inh_check_constraint10 | f | 2 | t | t
+ p1_c3 | inh_check_constraint2 | f | 2 | t | t
+ p1_c3 | inh_check_constraint3 | f | 2 | f | f
+ p1_c3 | inh_check_constraint4 | f | 2 | f | f
+ p1_c3 | inh_check_constraint5 | f | 2 | t | t
+ p1_c3 | inh_check_constraint6 | f | 2 | t | t
+ p1_c3 | inh_check_constraint7 | f | 1 | f | f
+ p1_c3 | inh_check_constraint8 | f | 2 | t | t
+ p1_c3 | inh_check_constraint9 | f | 2 | t | t
+(38 rows)
drop table p1 cascade;
NOTICE: drop cascades to 3 other objects
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index 44945b0453a..7e5be0702da 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -2,13 +2,39 @@
-- FOREIGN KEY
--
--- MATCH FULL
+-- NOT ENFORCED
--
-- First test, check and cascade
--
CREATE TABLE PKTABLE ( ptest1 int PRIMARY KEY, ptest2 text );
-CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE, ftest2 int );
+CREATE TABLE FKTABLE ( ftest1 int CONSTRAINT fktable_ftest1_fkey REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE NOT ENFORCED,
+ ftest2 int );
+-- Inserting into the foreign key table will not result in an error, even if
+-- there is no matching key in the referenced table.
+INSERT INTO FKTABLE VALUES (1, 2);
+INSERT INTO FKTABLE VALUES (2, 3);
+
+-- Check FKTABLE
+SELECT * FROM FKTABLE;
+
+-- Reverting it back to ENFORCED will result in failure because constraint validation will be triggered,
+-- as it was previously in a valid state.
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+
+-- Remove any existing rows that violate the constraint, then attempt to change
+-- it.
+TRUNCATE FKTABLE;
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+
+-- Any further inserts will fail due to the enforcement.
+INSERT INTO FKTABLE VALUES (3, 4);
+
+--
+-- MATCH FULL
+--
+-- First test, check and cascade
+--
-- Insert test data into PKTABLE
INSERT INTO PKTABLE VALUES (1, 'Test1');
INSERT INTO PKTABLE VALUES (2, 'Test2');
@@ -230,6 +256,27 @@ INSERT INTO FKTABLE VALUES (1, NULL);
ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL;
+-- Modifying other attributes of a constraint should not affect its enforceability, and vice versa
+ALTER TABLE FKTABLE ADD CONSTRAINT fk_con FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE NOT VALID NOT ENFORCED;
+ALTER TABLE FKTABLE ALTER CONSTRAINT fk_con DEFERRABLE INITIALLY DEFERRED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fk_con';
+
+ALTER TABLE FKTABLE ALTER CONSTRAINT fk_con NOT ENFORCED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fk_con';
+
+-- Enforceability also changes the validate state, as data validation will be
+-- performed during this transformation.
+ALTER TABLE FKTABLE ALTER CONSTRAINT fk_con ENFORCED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fk_con';
+
+-- Can change enforceability and deferrability together
+ALTER TABLE FKTABLE ALTER CONSTRAINT fk_con NOT ENFORCED NOT DEFERRABLE;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fk_con';
+
DROP TABLE FKTABLE;
DROP TABLE PKTABLE;
@@ -968,12 +1015,25 @@ INSERT INTO fktable VALUES (0, 20);
COMMIT;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT ENFORCED;
+
+BEGIN;
+
+-- doesn't match FK, no error.
+UPDATE pktable SET id = 10 WHERE id = 5;
+-- doesn't match PK, no error.
+INSERT INTO fktable VALUES (0, 20);
+
+ROLLBACK;
+
-- try additional syntax
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE;
-- illegal options
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE INITIALLY DEFERRED;
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NO INHERIT;
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT VALID;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORCED;
+CREATE TEMP TABLE fktable2 (fk int references pktable ENFORCED NOT ENFORCED);
-- test order of firing of FK triggers when several RI-induced changes need to
-- be made to the same row. This was broken by subtransaction-related
@@ -1184,11 +1244,13 @@ ALTER TABLE fk_partitioned_fk DROP COLUMN fdrop1;
CREATE TABLE fk_partitioned_fk_1 (fdrop1 int, fdrop2 int, a int, fdrop3 int, b int);
ALTER TABLE fk_partitioned_fk_1 DROP COLUMN fdrop1, DROP COLUMN fdrop2, DROP COLUMN fdrop3;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FROM (0,0) TO (1000,1000);
-ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk;
+ALTER TABLE fk_partitioned_fk ADD CONSTRAINT fk_partitioned_fk_a_b_fkey FOREIGN KEY (a, b)
+ REFERENCES fk_notpartitioned_pk NOT ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
+ALTER TABLE fk_partitioned_fk_2 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
-
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
CREATE TABLE fk_partitioned_fk_3 (fdrop1 int, fdrop2 int, fdrop3 int, fdrop4 int, b int, a int)
PARTITION BY HASH (a);
ALTER TABLE fk_partitioned_fk_3 DROP COLUMN fdrop1, DROP COLUMN fdrop2,
@@ -1234,6 +1296,27 @@ UPDATE fk_notpartitioned_pk SET b = 1502 WHERE a = 1500;
UPDATE fk_notpartitioned_pk SET b = 2504 WHERE a = 2500;
-- check psql behavior
\d fk_notpartitioned_pk
+
+-- Check the exsting FK trigger
+CREATE TEMP TABLE tmp_trg_info AS SELECT tgtype, tgrelid::regclass, tgconstraint
+FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+SELECT count(1) FROM tmp_trg_info;
+
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+-- No triggers
+SELECT count(1) FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+
+-- Changing it back to ENFORCED will recreate the necessary triggers.
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+
+-- Should be exactly the same number of triggers found as before
+SELECT COUNT(1) FROM pg_trigger pgt JOIN tmp_trg_info tt
+ON (pgt.tgtype = tt.tgtype AND pgt.tgrelid = tt.tgrelid AND pgt.tgconstraint = tt.tgconstraint);
+
ALTER TABLE fk_partitioned_fk DROP CONSTRAINT fk_partitioned_fk_a_b_fkey;
-- done.
DROP TABLE fk_notpartitioned_pk, fk_partitioned_fk;
@@ -1441,6 +1524,22 @@ ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN
\d fk_partitioned_fk_2
DROP TABLE fk_partitioned_fk_2;
+CREATE TABLE fk_partitioned_fk_2 (b int, a int,
+ FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
+BEGIN;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should be having two constraints
+\d fk_partitioned_fk_2
+DROP TABLE fk_partitioned_fk_2;
+ROLLBACK;
+BEGIN;
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have only one constraint
+\d fk_partitioned_fk_2
+ROLLBACK;
+DROP TABLE fk_partitioned_fk_2;
+
CREATE TABLE fk_partitioned_fk_4 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE) PARTITION BY RANGE (b, a);
CREATE TABLE fk_partitioned_fk_4_1 PARTITION OF fk_partitioned_fk_4 FOR VALUES FROM (1,1) TO (100,100);
CREATE TABLE fk_partitioned_fk_4_2 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE SET NULL);
diff --git a/src/test/regress/sql/inherit.sql b/src/test/regress/sql/inherit.sql
index 4e73c70495c..bc548199d1e 100644
--- a/src/test/regress/sql/inherit.sql
+++ b/src/test/regress/sql/inherit.sql
@@ -481,6 +481,13 @@ alter table p1 add constraint inh_check_constraint5 check (f1 < 10) not enforced
alter table p1 add constraint inh_check_constraint6 check (f1 < 10) not enforced;
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 invalid state of the child constraint will be ignored here.
+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;
+
create table p1_c2(f1 int constraint inh_check_constraint4 check (f1 < 10)) inherits(p1);
-- but reverse is not allowed
@@ -498,7 +505,7 @@ create table p1_c3() inherits(p1, p1_c1);
-- but not allowed if the child constraint is explicitly asked to be NOT ENFORCED
create table p1_fail(f1 int constraint inh_check_constraint6 check (f1 < 10) not enforced) inherits(p1, p1_c1);
-select conrelid::regclass::text as relname, conname, conislocal, coninhcount, conenforced
+select conrelid::regclass::text as relname, conname, conislocal, coninhcount, conenforced, convalidated
from pg_constraint where conname like 'inh\_check\_constraint%'
order by 1, 2;
--
2.43.5
v17-0006-Merge-the-parent-and-child-constraints-with-diff.patchapplication/octet-stream; name=v17-0006-Merge-the-parent-and-child-constraints-with-diff.patchDownload
From d2912dbbe7b16a7da620fdcd9a760b7759415012 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 10 Feb 2025 10:59:28 +0530
Subject: [PATCH v17 6/6] Merge the parent and child constraints with differing
enforcibility.
If an enforced parent constraint is attached to a non-enforced child
constraint, the child constraint will be made enforced, with
validation applied if the parent constraint is validated as well.
Otherwise, a new enforced constraint (with validation, if the parent
constraint is validated) would need to be created on the child table,
which would be unnecessary if a similar constraint already exists and
can be attached.
On the other hand, having a not-enforced parent constraint with an
enforced child constraint does not cause any issues, and no changes
are required.
----
NOTE: This patch is intended to reduce the diff noise from the main
patch and is not meant to be committed separately. It should be
squashed with the main patch that adds ENFORCED/NOT ENFORCED.
----
---
src/backend/commands/tablecmds.c | 164 ++++++++++++++++++++--
src/test/regress/expected/foreign_key.out | 77 ++++++++--
src/test/regress/sql/foreign_key.sql | 37 ++++-
3 files changed, 253 insertions(+), 25 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 7bf28152fa0..8ed6ec70937 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -11545,7 +11545,6 @@ tryAttachPartitionForeignKey(List **wqueue,
if (OidIsValid(partConstr->conparentid) ||
partConstr->condeferrable != parentConstr->condeferrable ||
partConstr->condeferred != parentConstr->condeferred ||
- partConstr->conenforced != parentConstr->conenforced ||
partConstr->confupdtype != parentConstr->confupdtype ||
partConstr->confdeltype != parentConstr->confdeltype ||
partConstr->confmatchtype != parentConstr->confmatchtype)
@@ -11590,6 +11589,8 @@ AttachPartitionForeignKey(List **wqueue,
Oid partConstrFrelid;
Oid partConstrRelid;
bool parentConstrIsEnforced;
+ bool partConstrIsEnforced;
+ bool partConstrParentIsSet;
/* Fetch the parent constraint tuple */
parentConstrTup = SearchSysCache1(CONSTROID,
@@ -11605,13 +11606,47 @@ AttachPartitionForeignKey(List **wqueue,
if (!HeapTupleIsValid(partcontup))
elog(ERROR, "cache lookup failed for constraint %u", partConstrOid);
partConstr = (Form_pg_constraint) GETSTRUCT(partcontup);
+ partConstrIsEnforced = partConstr->conenforced;
partConstrFrelid = partConstr->confrelid;
partConstrRelid = partConstr->conrelid;
+ /*
+ * The case where the parent constraint is not enforced and the child
+ * constraint is enforced is acceptable because the non-enforced parent
+ * constraint lacks triggers, eliminating any redundancy issues with the
+ * enforced child constraint. In this scenario, the child constraint
+ * remains enforced, and its trigger is retained, ensuring that
+ * referential integrity checks for the child continue as before, even
+ * with the parent constraint not enforced. The relationship between the
+ * two constraints is preserved by setting the parent constraint, which
+ * allows us to locate the child constraint. This becomes important if the
+ * parent constraint is later changed to enforced, at which point the
+ * necessary trigger will be created for the parent, and any redundancy
+ * from these triggers will be appropriately handled.
+ */
+ if (!parentConstrIsEnforced && partConstrIsEnforced)
+ {
+ ReleaseSysCache(partcontup);
+ ReleaseSysCache(parentConstrTup);
+
+ ConstraintSetParentConstraint(partConstrOid, parentConstrOid,
+ RelationGetRelid(partition));
+ CommandCounterIncrement();
+
+ return;
+ }
+
/*
* If the referenced table is partitioned, then the partition we're
* attaching now has extra pg_constraint rows and action triggers that are
* no longer needed. Remove those.
+ *
+ * Note that this must be done beforehand, particularly in situations
+ * where we might decide to change the constraint to an enforced state
+ * which will create the required triggers and add the child constraint to
+ * the validation queue. To avoid generating unnecessary triggers and
+ * adding them to the validation queue, it is crucial to eliminate any
+ * redundant constraints beforehand.
*/
if (get_rel_relkind(partConstrFrelid) == RELKIND_PARTITIONED_TABLE)
{
@@ -11630,6 +11665,53 @@ AttachPartitionForeignKey(List **wqueue,
*/
queueValidation = parentConstr->convalidated && !partConstr->convalidated;
+ /*
+ * The case where the parent constraint is enforced and the child
+ * constraint is not enforced is not acceptable, as it would violate
+ * referential integrity. In such cases, the child constraint will first
+ * be enforced before merging it with the enforced parent constraint.
+ * Subsequently, removing action triggers, setting up constraint triggers,
+ * and handling check triggers for the parent will be managed in the usual
+ * manner, similar to how two enforced constraints are merged.
+ */
+ if (parentConstrIsEnforced && !partConstrIsEnforced)
+ {
+ ATAlterConstraint *cmdcon = makeNode(ATAlterConstraint);
+ Relation conrel;
+
+ cmdcon->conname = NameStr(partConstr->conname);
+ cmdcon->deferrable = partConstr->condeferrable;
+ cmdcon->initdeferred = partConstr->condeferred;
+ cmdcon->alterEnforceability = true;
+ cmdcon->is_enforced = true;
+
+ conrel = table_open(ConstraintRelationId, RowExclusiveLock);
+
+ ATExecAlterConstrEnforceability(wqueue, cmdcon, conrel, trigrel,
+ partConstr->conrelid,
+ partConstr->confrelid,
+ partcontup, AccessExclusiveLock,
+ InvalidOid, InvalidOid, InvalidOid,
+ InvalidOid);
+
+ table_close(conrel, RowExclusiveLock);
+
+ CommandCounterIncrement();
+
+ /*
+ * No further validation is needed, as changing the constraint to
+ * enforced will implicitly trigger the same validation.
+ */
+ queueValidation = false;
+ }
+
+ /*
+ * The constraint parent shouldn't be set beforehand, or if it's already
+ * set, it should be the specified parent.
+ */
+ partConstrParentIsSet = OidIsValid(partConstr->conparentid);
+ Assert(!partConstrParentIsSet || partConstr->conparentid == parentConstrOid);
+
ReleaseSysCache(partcontup);
ReleaseSysCache(parentConstrTup);
@@ -11642,8 +11724,10 @@ AttachPartitionForeignKey(List **wqueue,
DropForeignKeyConstraintTriggers(trigrel, partConstrOid, partConstrFrelid,
partConstrRelid);
- ConstraintSetParentConstraint(partConstrOid, parentConstrOid,
- RelationGetRelid(partition));
+ /* Skip if the parent is already set */
+ if (!partConstrParentIsSet)
+ ConstraintSetParentConstraint(partConstrOid, parentConstrOid,
+ RelationGetRelid(partition));
/*
* Like the constraint, attach partition's "check" triggers to the
@@ -12242,6 +12326,17 @@ ATExecAlterConstrEnforceability(List **wqueue, ATAlterConstraint *cmdcon,
/* Drop all the triggers */
DropForeignKeyConstraintTriggers(tgrel, conoid, InvalidOid, InvalidOid);
+
+ /*
+ * If the referenced table is partitioned, the child constraint we're
+ * changing to not-enforced may have additional pg_constraint rows and
+ * action triggers that remain untouched while this child constraint
+ * is attached to the not-enforced parent. These must now be removed.
+ * For more details, see AttachPartitionForeignKey().
+ */
+ if (OidIsValid(currcon->conparentid) &&
+ get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE)
+ RemoveInheritedConstraint(conrel, tgrel, currcon->conrelid, conoid);
}
else if (changed) /* Create triggers */
{
@@ -12567,13 +12662,40 @@ AlterConstrEnforceabilityRecurse(List **wqueue, ATAlterConstraint *cmdcon,
true, NULL, 1, &pkey);
while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
- ATExecAlterConstrEnforceability(wqueue, cmdcon, conrel, tgrel, fkrelid,
- pkrelid, childtup, lockmode,
- ReferencedParentDelTrigger,
- ReferencedParentUpdTrigger,
- ReferencingParentInsTrigger,
- ReferencingParentUpdTrigger);
+ {
+ Form_pg_constraint childcon;
+ childcon = (Form_pg_constraint) GETSTRUCT(childtup);
+
+ /*
+ * When the parent constraint is modified to be enforced, and the
+ * child constraint is attached to the parent constraint (which is
+ * already enforced), some constraints and action triggers on the
+ * child table may become redundant and need to be removed.
+ */
+ if (cmdcon->is_enforced && childcon->conenforced)
+ {
+ if (currcon->confrelid == pkrelid)
+ {
+ Relation rel = table_open(childcon->conrelid, lockmode);
+
+ AttachPartitionForeignKey(wqueue, rel, childcon->oid,
+ conoid,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger,
+ tgrel);
+
+ table_close(rel, NoLock);
+ }
+ }
+ else
+ ATExecAlterConstrEnforceability(wqueue, cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, childtup, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
+ }
systable_endscan(pscan);
}
@@ -20746,7 +20868,9 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
{
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
+ HeapTuple parentContup;
Form_pg_constraint conform;
+ Oid parentConstrIsEnforced;
contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
if (!HeapTupleIsValid(contup))
@@ -20765,12 +20889,34 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
continue;
}
+ /* Get the enforcibility of the parent constraint */
+ parentContup = SearchSysCache1(CONSTROID,
+ ObjectIdGetDatum(conform->conparentid));
+ if (!HeapTupleIsValid(parentContup))
+ elog(ERROR, "cache lookup failed for constraint %u",
+ conform->conparentid);
+ parentConstrIsEnforced =
+ ((Form_pg_constraint) GETSTRUCT(parentContup))->conenforced;
+ ReleaseSysCache(parentContup);
+
/*
* The constraint on this table must be marked no longer a child of
* the parent's constraint, as do its check triggers.
*/
ConstraintSetParentConstraint(fk->conoid, InvalidOid, InvalidOid);
+ /*
+ * Unsetting the parent is sufficient when the parent constraint is
+ * not enforced and the child constraint is enforced, as we link them
+ * by setting the constraint parent, while leaving the rest unchanged.
+ * For more details, see AttachPartitionForeignKey().
+ */
+ if (!parentConstrIsEnforced && fk->conenforced)
+ {
+ ReleaseSysCache(contup);
+ continue;
+ }
+
/*
* Also, look up the partition's "check" triggers corresponding to the
* enforced constraint being detached and detach them from the parent
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 39744da7fd6..0d6d7838c79 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -1675,7 +1675,6 @@ ALTER TABLE fk_partitioned_fk ADD CONSTRAINT fk_partitioned_fk_a_b_fkey FOREIGN
REFERENCES fk_notpartitioned_pk NOT ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
-ALTER TABLE fk_partitioned_fk_2 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
CREATE TABLE fk_partitioned_fk_3 (fdrop1 int, fdrop2 int, fdrop3 int, fdrop4 int, b int, a int)
@@ -1684,8 +1683,65 @@ ALTER TABLE fk_partitioned_fk_3 DROP COLUMN fdrop1, DROP COLUMN fdrop2,
DROP COLUMN fdrop3, DROP COLUMN fdrop4;
CREATE TABLE fk_partitioned_fk_3_0 PARTITION OF fk_partitioned_fk_3 FOR VALUES WITH (MODULUS 5, REMAINDER 0);
CREATE TABLE fk_partitioned_fk_3_1 PARTITION OF fk_partitioned_fk_3 FOR VALUES WITH (MODULUS 5, REMAINDER 1);
+-- Merge the not-enforced parent constraint with the enforced and not-enforced child constraints
+ALTER TABLE fk_partitioned_fk_3_0 ADD CONSTRAINT fk_partitioned_fk_3_0_a_b_fkey FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ENFORCED;
+ALTER TABLE fk_partitioned_fk_3_1 ADD CONSTRAINT fk_partitioned_fk_3_1_a_b_fkey FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk_3 ADD CONSTRAINT fk_partitioned_fk_3_a_b_fkey FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+-- Merge the non-enforced parent constraint with both the enforced and
+-- non-enforced child constraints, where the referenced table is partitioned.
+CREATE TABLE fk_partitioned_pk (a int, b int, PRIMARY KEY (a, b)) PARTITION BY RANGE (a, b);
+CREATE TABLE fk_partitioned_pk_1 PARTITION OF fk_partitioned_pk FOR VALUES FROM (0,0) TO (1000,1000);
+-- Merge the enforced parent constraint with the enforced and not-enforced child constraints.
+ALTER TABLE fk_partitioned_fk_3_0 ADD CONSTRAINT fk_partitioned_fk_3_0_a_b_fkey1 FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk ENFORCED;
+ALTER TABLE fk_partitioned_fk_3_1 ADD CONSTRAINT fk_partitioned_fk_3_1_a_b_fkey1 FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk_3 ADD CONSTRAINT fk_partitioned_fk_3_a_b_fkey1 FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk ENFORCED;
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+ conname | conenforced | convalidated | conrelid | confrelid
+---------------------------------+-------------+--------------+-----------------------+----------------------
+ fk_partitioned_fk_3_0_a_b_fkey | t | t | fk_partitioned_fk_3_0 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey | f | f | fk_partitioned_fk_3_1 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_a_b_fkey | f | f | fk_partitioned_fk_3 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_0_a_b_fkey1 | t | t | fk_partitioned_fk_3_0 | fk_partitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey1 | t | t | fk_partitioned_fk_3_1 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey1 | t | t | fk_partitioned_fk_3 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey2 | t | t | fk_partitioned_fk_3 | fk_partitioned_pk_1
+(7 rows)
+
+ALTER TABLE fk_partitioned_fk_3 DETACH PARTITION fk_partitioned_fk_3_0;
+ALTER TABLE fk_partitioned_fk_3 ATTACH PARTITION fk_partitioned_fk_3_0 FOR VALUES WITH (MODULUS 5, REMAINDER 0);
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+ conname | conenforced | convalidated | conrelid | confrelid
+---------------------------------+-------------+--------------+-----------------------+----------------------
+ fk_partitioned_fk_3_0_a_b_fkey | t | t | fk_partitioned_fk_3_0 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey | f | f | fk_partitioned_fk_3_1 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_a_b_fkey | f | f | fk_partitioned_fk_3 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_0_a_b_fkey1 | t | t | fk_partitioned_fk_3_0 | fk_partitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey1 | t | t | fk_partitioned_fk_3_1 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey1 | t | t | fk_partitioned_fk_3 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey2 | t | t | fk_partitioned_fk_3 | fk_partitioned_pk_1
+(7 rows)
+
+ALTER TABLE fk_partitioned_fk_3 ALTER CONSTRAINT fk_partitioned_fk_3_a_b_fkey1 NOT ENFORCED;
+-- Merging an enforced parent constraint (validated) with a not-enforced child
+-- constraint will implicitly change the child constraint to enforced and apply
+-- the validation as well.
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_3
FOR VALUES FROM (2000,2000) TO (3000,3000);
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+ conname | conenforced | convalidated | conrelid | confrelid
+---------------------------------+-------------+--------------+-----------------------+----------------------
+ fk_partitioned_fk_3_0_a_b_fkey | t | t | fk_partitioned_fk_3_0 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey | t | t | fk_partitioned_fk_3_1 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_a_b_fkey | t | t | fk_partitioned_fk_3 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_0_a_b_fkey1 | f | f | fk_partitioned_fk_3_0 | fk_partitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey1 | f | f | fk_partitioned_fk_3_1 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey1 | f | f | fk_partitioned_fk_3 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey2 | f | f | fk_partitioned_fk_3 | fk_partitioned_pk_1
+(7 rows)
+
-- Creating a foreign key with ONLY on a partitioned table referencing
-- a non-partitioned table fails.
ALTER TABLE ONLY fk_partitioned_fk ADD FOREIGN KEY (a, b)
@@ -1700,22 +1756,22 @@ INSERT INTO fk_partitioned_fk_1 (a,b) VALUES (500, 501);
ERROR: insert or update on table "fk_partitioned_fk_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
DETAIL: Key (a, b)=(500, 501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (1500, 1501);
-ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_2_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
DETAIL: Key (a, b)=(1500, 1501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk_2 (a,b) VALUES (1500, 1501);
-ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_2_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
DETAIL: Key (a, b)=(1500, 1501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (2500, 2502);
-ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_3_1_a_b_fkey"
DETAIL: Key (a, b)=(2500, 2502) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk_3 (a,b) VALUES (2500, 2502);
-ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_3_1_a_b_fkey"
DETAIL: Key (a, b)=(2500, 2502) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (2501, 2503);
-ERROR: insert or update on table "fk_partitioned_fk_3_0" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_0" violates foreign key constraint "fk_partitioned_fk_3_0_a_b_fkey"
DETAIL: Key (a, b)=(2501, 2503) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk_3 (a,b) VALUES (2501, 2503);
-ERROR: insert or update on table "fk_partitioned_fk_3_0" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_0" violates foreign key constraint "fk_partitioned_fk_3_0_a_b_fkey"
DETAIL: Key (a, b)=(2501, 2503) is not present in table "fk_notpartitioned_pk".
-- but if we insert the values that make them valid, then they work
INSERT INTO fk_notpartitioned_pk VALUES (500, 501), (1500, 1501),
@@ -1726,7 +1782,7 @@ INSERT INTO fk_partitioned_fk (a,b) VALUES (2500, 2502);
INSERT INTO fk_partitioned_fk (a,b) VALUES (2501, 2503);
-- this update fails because there is no referenced row
UPDATE fk_partitioned_fk SET a = a + 1 WHERE a = 2501;
-ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_3_1_a_b_fkey"
DETAIL: Key (a, b)=(2502, 2503) is not present in table "fk_notpartitioned_pk".
-- but we can fix it thusly:
INSERT INTO fk_notpartitioned_pk (a,b) VALUES (2502, 2503);
@@ -1869,8 +1925,6 @@ WHERE conrelid::regclass::text like 'fk_partitioned_fk%' ORDER BY oid::regclass:
DROP TABLE fk_partitioned_fk, fk_notpartitioned_pk;
-- NOT VALID foreign key on a non-partitioned table referencing a partitioned table
-CREATE TABLE fk_partitioned_pk (a int, b int, PRIMARY KEY (a, b)) PARTITION BY RANGE (a, b);
-CREATE TABLE fk_partitioned_pk_1 PARTITION OF fk_partitioned_pk FOR VALUES FROM (0,0) TO (1000,1000);
CREATE TABLE fk_notpartitioned_fk (b int, a int);
ALTER TABLE fk_notpartitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk NOT VALID;
-- Constraint will be invalid.
@@ -2086,7 +2140,7 @@ CREATE TABLE fk_partitioned_fk_2 (b int, a int,
FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
BEGIN;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
--- should be having two constraints
+-- should only have one constraint
\d fk_partitioned_fk_2
Table "public.fk_partitioned_fk_2"
Column | Type | Collation | Nullable | Default
@@ -2095,7 +2149,6 @@ ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN
a | integer | | |
Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
Foreign-key constraints:
- "fk_partitioned_fk_2_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED
TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
DROP TABLE fk_partitioned_fk_2;
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index 7e5be0702da..fa51c7110ef 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -1248,18 +1248,49 @@ ALTER TABLE fk_partitioned_fk ADD CONSTRAINT fk_partitioned_fk_a_b_fkey FOREIGN
REFERENCES fk_notpartitioned_pk NOT ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
-ALTER TABLE fk_partitioned_fk_2 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+
CREATE TABLE fk_partitioned_fk_3 (fdrop1 int, fdrop2 int, fdrop3 int, fdrop4 int, b int, a int)
PARTITION BY HASH (a);
ALTER TABLE fk_partitioned_fk_3 DROP COLUMN fdrop1, DROP COLUMN fdrop2,
DROP COLUMN fdrop3, DROP COLUMN fdrop4;
CREATE TABLE fk_partitioned_fk_3_0 PARTITION OF fk_partitioned_fk_3 FOR VALUES WITH (MODULUS 5, REMAINDER 0);
CREATE TABLE fk_partitioned_fk_3_1 PARTITION OF fk_partitioned_fk_3 FOR VALUES WITH (MODULUS 5, REMAINDER 1);
+-- Merge the not-enforced parent constraint with the enforced and not-enforced child constraints
+ALTER TABLE fk_partitioned_fk_3_0 ADD CONSTRAINT fk_partitioned_fk_3_0_a_b_fkey FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ENFORCED;
+ALTER TABLE fk_partitioned_fk_3_1 ADD CONSTRAINT fk_partitioned_fk_3_1_a_b_fkey FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk_3 ADD CONSTRAINT fk_partitioned_fk_3_a_b_fkey FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+
+-- Merge the non-enforced parent constraint with both the enforced and
+-- non-enforced child constraints, where the referenced table is partitioned.
+CREATE TABLE fk_partitioned_pk (a int, b int, PRIMARY KEY (a, b)) PARTITION BY RANGE (a, b);
+CREATE TABLE fk_partitioned_pk_1 PARTITION OF fk_partitioned_pk FOR VALUES FROM (0,0) TO (1000,1000);
+-- Merge the enforced parent constraint with the enforced and not-enforced child constraints.
+ALTER TABLE fk_partitioned_fk_3_0 ADD CONSTRAINT fk_partitioned_fk_3_0_a_b_fkey1 FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk ENFORCED;
+ALTER TABLE fk_partitioned_fk_3_1 ADD CONSTRAINT fk_partitioned_fk_3_1_a_b_fkey1 FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk_3 ADD CONSTRAINT fk_partitioned_fk_3_a_b_fkey1 FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk ENFORCED;
+
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+
+ALTER TABLE fk_partitioned_fk_3 DETACH PARTITION fk_partitioned_fk_3_0;
+ALTER TABLE fk_partitioned_fk_3 ATTACH PARTITION fk_partitioned_fk_3_0 FOR VALUES WITH (MODULUS 5, REMAINDER 0);
+
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+
+ALTER TABLE fk_partitioned_fk_3 ALTER CONSTRAINT fk_partitioned_fk_3_a_b_fkey1 NOT ENFORCED;
+
+-- Merging an enforced parent constraint (validated) with a not-enforced child
+-- constraint will implicitly change the child constraint to enforced and apply
+-- the validation as well.
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_3
FOR VALUES FROM (2000,2000) TO (3000,3000);
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+
-- Creating a foreign key with ONLY on a partitioned table referencing
-- a non-partitioned table fails.
ALTER TABLE ONLY fk_partitioned_fk ADD FOREIGN KEY (a, b)
@@ -1386,8 +1417,6 @@ WHERE conrelid::regclass::text like 'fk_partitioned_fk%' ORDER BY oid::regclass:
DROP TABLE fk_partitioned_fk, fk_notpartitioned_pk;
-- NOT VALID foreign key on a non-partitioned table referencing a partitioned table
-CREATE TABLE fk_partitioned_pk (a int, b int, PRIMARY KEY (a, b)) PARTITION BY RANGE (a, b);
-CREATE TABLE fk_partitioned_pk_1 PARTITION OF fk_partitioned_pk FOR VALUES FROM (0,0) TO (1000,1000);
CREATE TABLE fk_notpartitioned_fk (b int, a int);
ALTER TABLE fk_notpartitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk NOT VALID;
@@ -1528,7 +1557,7 @@ CREATE TABLE fk_partitioned_fk_2 (b int, a int,
FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
BEGIN;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
--- should be having two constraints
+-- should only have one constraint
\d fk_partitioned_fk_2
DROP TABLE fk_partitioned_fk_2;
ROLLBACK;
--
2.43.5
On 2025-Mar-12, Amul Sul wrote:
On Tue, Mar 11, 2025 at 11:13 PM Peter Eisentraut <peter@eisentraut.org> wrote:
I think the next step here is that you work to fix Álvaro's concerns
about the recursion structure.Yes, I worked on that in the attached version. I refactored
ATExecAlterConstraintInternal() and moved the code that updates the
pg_constraint entry into a separate function (see 0001), so it can be
called from the places where the entry needs to be updated, rather
than revisiting ATExecAlterConstraintInternal(). In 0002,
ATExecAlterConstraintInternal() is split into two functions:
ATExecAlterConstrDeferrability() and
ATExecAlterConstrInheritability(), which handle altering deferrability
and inheritability, respectively. These functions are expected to
recurse on themselves, rather than revisiting
ATExecAlterConstraintInternal() as before. This approach simplifies
things. Similarly can add ATExecAlterConstrEnforceability() which
recurses itself.
Yeah, I gave this a look and I think this code layout is good. There
are more functions now, but the code flow is simpler.
Thanks!
--
Álvaro Herrera 48°01'N 7°57'E — https://www.EnterpriseDB.com/
On Wed, Mar 19, 2025 at 12:33 AM Álvaro Herrera <alvherre@alvh.no-ip.org> wrote:
On 2025-Mar-12, Amul Sul wrote:
On Tue, Mar 11, 2025 at 11:13 PM Peter Eisentraut <peter@eisentraut.org> wrote:
I think the next step here is that you work to fix Álvaro's concerns
about the recursion structure.Yes, I worked on that in the attached version. I refactored
ATExecAlterConstraintInternal() and moved the code that updates the
pg_constraint entry into a separate function (see 0001), so it can be
called from the places where the entry needs to be updated, rather
than revisiting ATExecAlterConstraintInternal(). In 0002,
ATExecAlterConstraintInternal() is split into two functions:
ATExecAlterConstrDeferrability() and
ATExecAlterConstrInheritability(), which handle altering deferrability
and inheritability, respectively. These functions are expected to
recurse on themselves, rather than revisiting
ATExecAlterConstraintInternal() as before. This approach simplifies
things. Similarly can add ATExecAlterConstrEnforceability() which
recurses itself.Yeah, I gave this a look and I think this code layout is good. There
are more functions now, but the code flow is simpler.
Thank you !
Attached is the updated version, where the commit messages for patch
0005 and 0006 have been slightly corrected. Additionally, a few code
comments have been updated to consistently use the ENFORCED/NOT
ENFORCED keywords. The rest of the patches and all the code are
unchanged.
Regards,
Amul
Attachments:
v18-0001-refactor-move-code-updates-pg_constraint-entry-i.patchapplication/octet-stream; name=v18-0001-refactor-move-code-updates-pg_constraint-entry-i.patchDownload
From 537e8eb29a422410036393b6cbefbb26ef4ef8bd Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Tue, 11 Mar 2025 18:19:39 +0530
Subject: [PATCH v18 1/6] refactor: move code updates pg_constraint entry in a
separate function
---
src/backend/commands/tablecmds.c | 62 ++++++++++++++++++++------------
1 file changed, 39 insertions(+), 23 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 129c97fdf28..196a2c524b4 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -402,6 +402,8 @@ static void ATExecAlterChildConstr(List **wqueue, ATAlterConstraint *cmdcon,
Relation conrel, Relation tgrel, Relation rel,
HeapTuple contuple, bool recurse, List **otherrelids,
LOCKMODE lockmode);
+static void UpdateConstraintEntry(ATAlterConstraint *cmdcon, Relation conrel,
+ HeapTuple contuple);
static ObjectAddress ATExecValidateConstraint(List **wqueue,
Relation rel, char *constrName,
bool recurse, bool recursing, LOCKMODE lockmode);
@@ -12095,23 +12097,9 @@ ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon,
(currcon->condeferrable != cmdcon->deferrable ||
currcon->condeferred != cmdcon->initdeferred))
{
- HeapTuple copyTuple;
- Form_pg_constraint copy_con;
-
- copyTuple = heap_copytuple(contuple);
- copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
- copy_con->condeferrable = cmdcon->deferrable;
- copy_con->condeferred = cmdcon->initdeferred;
- CatalogTupleUpdate(conrel, ©Tuple->t_self, copyTuple);
-
- InvokeObjectPostAlterHook(ConstraintRelationId, currcon->oid, 0);
-
- heap_freetuple(copyTuple);
+ UpdateConstraintEntry(cmdcon, conrel, contuple);
changed = true;
- /* Make new constraint flags visible to others */
- CacheInvalidateRelcache(rel);
-
/*
* Now we need to update the multiple entries in pg_trigger that
* implement the constraint.
@@ -12142,19 +12130,12 @@ ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon,
AttrNumber colNum;
char *colName;
List *children;
- HeapTuple copyTuple;
- Form_pg_constraint copy_con;
/* The current implementation only works for NOT NULL constraints */
Assert(currcon->contype == CONSTRAINT_NOTNULL);
- copyTuple = heap_copytuple(contuple);
- copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
- copy_con->connoinherit = cmdcon->noinherit;
-
- CatalogTupleUpdate(conrel, ©Tuple->t_self, copyTuple);
+ UpdateConstraintEntry(cmdcon, conrel, contuple);
CommandCounterIncrement();
- heap_freetuple(copyTuple);
changed = true;
/* Fetch the column number and name */
@@ -12316,6 +12297,41 @@ ATExecAlterChildConstr(List **wqueue, ATAlterConstraint *cmdcon,
systable_endscan(pscan);
}
+/*
+ * Update the constraint entry.
+ */
+static void
+UpdateConstraintEntry(ATAlterConstraint *cmdcon, Relation conrel,
+ HeapTuple contuple)
+{
+ Form_pg_constraint currcon;
+ HeapTuple copyTuple;
+ Form_pg_constraint copy_con;
+
+ Assert(cmdcon->alterDeferrability || cmdcon->alterInheritability);
+
+ currcon = (Form_pg_constraint) GETSTRUCT(contuple);
+
+ copyTuple = heap_copytuple(contuple);
+ copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
+
+ if (cmdcon->alterDeferrability)
+ {
+ copy_con->condeferrable = cmdcon->deferrable;
+ copy_con->condeferred = cmdcon->initdeferred;
+ }
+ if (cmdcon->alterInheritability)
+ copy_con->connoinherit = cmdcon->noinherit;
+
+ CatalogTupleUpdate(conrel, ©Tuple->t_self, copyTuple);
+ InvokeObjectPostAlterHook(ConstraintRelationId, currcon->oid, 0);
+
+ heap_freetuple(copyTuple);
+
+ /* Make new constraint flags visible to others */
+ CacheInvalidateRelcacheByRelid(currcon->conrelid);
+}
+
/*
* ALTER TABLE VALIDATE CONSTRAINT
*
--
2.43.5
v18-0002-refactor-Split-ATExecAlterConstraintInternal.patchapplication/octet-stream; name=v18-0002-refactor-Split-ATExecAlterConstraintInternal.patchDownload
From 97680ce503e4552bad5057d73c3953c4ea1696f0 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Tue, 11 Mar 2025 20:41:14 +0530
Subject: [PATCH v18 2/6] refactor: Split ATExecAlterConstraintInternal().
Split ATExecAlterConstraintInternal() into two functions:
ATExecAlterConstrDeferrability() and ATExecAlterConstrInheritability().
This simplifies the code and avoids unnecessary confusion caused by
recursive code, which isn't needed for ATExecAlterConstrInheritability.
---
src/backend/commands/tablecmds.c | 244 +++++++++++++++++++------------
1 file changed, 150 insertions(+), 94 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 196a2c524b4..f02778972c7 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -394,14 +394,21 @@ static ObjectAddress ATExecAlterConstraint(List **wqueue, Relation rel,
bool recurse, LOCKMODE lockmode);
static bool ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon, Relation conrel,
Relation tgrel, Relation rel, HeapTuple contuple,
- bool recurse, List **otherrelids, LOCKMODE lockmode);
+ bool recurse, LOCKMODE lockmode);
+static bool ATExecAlterConstrDeferrability(List **wqueue, ATAlterConstraint *cmdcon,
+ Relation conrel, Relation tgrel, Relation rel,
+ HeapTuple contuple, bool recurse,
+ List **otherrelids, LOCKMODE lockmode);
+static bool ATExecAlterConstrInheritability(List **wqueue, ATAlterConstraint *cmdcon,
+ Relation conrel, Relation rel,
+ HeapTuple contuple, LOCKMODE lockmode);
static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
bool deferrable, bool initdeferred,
List **otherrelids);
-static void ATExecAlterChildConstr(List **wqueue, ATAlterConstraint *cmdcon,
- Relation conrel, Relation tgrel, Relation rel,
- HeapTuple contuple, bool recurse, List **otherrelids,
- 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 UpdateConstraintEntry(ATAlterConstraint *cmdcon, Relation conrel,
HeapTuple contuple);
static ObjectAddress ATExecValidateConstraint(List **wqueue,
@@ -11927,7 +11934,6 @@ ATExecAlterConstraint(List **wqueue, Relation rel, ATAlterConstraint *cmdcon,
HeapTuple contuple;
Form_pg_constraint currcon;
ObjectAddress address;
- List *otherrelids = NIL;
/*
* Disallow altering ONLY a partitioned table, as it would make no sense.
@@ -12040,17 +12046,9 @@ ATExecAlterConstraint(List **wqueue, Relation rel, ATAlterConstraint *cmdcon,
* Do the actual catalog work, and recurse if necessary.
*/
if (ATExecAlterConstraintInternal(wqueue, cmdcon, conrel, tgrel, rel,
- contuple, recurse, &otherrelids, lockmode))
+ contuple, recurse, lockmode))
ObjectAddressSet(address, ConstraintRelationId, currcon->oid);
- /*
- * ATExecAlterConstraintInternal already invalidated relcache for the
- * relations having the constraint itself; here we also invalidate for
- * relations that have any triggers that are part of the constraint.
- */
- foreach_oid(relid, otherrelids)
- CacheInvalidateRelcacheByRelid(relid);
-
systable_endscan(scan);
table_close(tgrel, RowExclusiveLock);
@@ -12060,8 +12058,50 @@ ATExecAlterConstraint(List **wqueue, Relation rel, ATAlterConstraint *cmdcon,
}
/*
- * Recursive subroutine of ATExecAlterConstraint. Returns true if the
- * constraint is altered.
+ * A subroutine of ATExecAlterConstraint that calls the respective routines for
+ * altering constraint attributes.
+ */
+static bool
+ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon,
+ Relation conrel, Relation tgrel, Relation rel,
+ HeapTuple contuple, bool recurse,
+ LOCKMODE lockmode)
+{
+ bool changed = false;
+ List *otherrelids = NIL;
+
+ /*
+ * Do the catalog work for the deferrability change, recurse if necessary.
+ */
+ if (cmdcon->alterDeferrability &&
+ ATExecAlterConstrDeferrability(wqueue, cmdcon, conrel, tgrel, rel,
+ contuple, recurse, &otherrelids,
+ lockmode))
+ {
+ /*
+ * UpdateConstraintEntry already invalidated relcache for the
+ * relations having the constraint itself; here we also invalidate for
+ * relations that have any triggers that are part of the constraint.
+ */
+ foreach_oid(relid, otherrelids)
+ CacheInvalidateRelcacheByRelid(relid);
+
+ changed = true;
+ }
+
+ /*
+ * Do the catalog work for the inheritability change.
+ */
+ if (cmdcon->alterInheritability &&
+ ATExecAlterConstrInheritability(wqueue, cmdcon, conrel, rel, contuple,
+ lockmode))
+ changed = true;
+
+ return changed;
+}
+
+/*
+ * Returns true if the constraint's deferrability is altered.
*
* *otherrelids is appended OIDs of relations containing affected triggers.
*
@@ -12071,31 +12111,32 @@ ATExecAlterConstraint(List **wqueue, Relation rel, ATAlterConstraint *cmdcon,
* but existing releases don't do that.)
*/
static bool
-ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon,
- Relation conrel, Relation tgrel, Relation rel,
- HeapTuple contuple, bool recurse,
- List **otherrelids, LOCKMODE lockmode)
+ATExecAlterConstrDeferrability(List **wqueue, ATAlterConstraint *cmdcon,
+ Relation conrel, Relation tgrel, Relation rel,
+ HeapTuple contuple, bool recurse,
+ List **otherrelids, LOCKMODE lockmode)
{
Form_pg_constraint currcon;
- Oid refrelid = InvalidOid;
+ Oid refrelid;
bool changed = false;
/* since this function recurses, it could be driven to stack overflow */
check_stack_depth();
+ Assert(cmdcon->alterDeferrability);
+
currcon = (Form_pg_constraint) GETSTRUCT(contuple);
- if (currcon->contype == CONSTRAINT_FOREIGN)
- refrelid = currcon->confrelid;
+ refrelid = currcon->confrelid;
+
+ /* Should be foreign key constraint */
+ Assert(currcon->contype == CONSTRAINT_FOREIGN);
/*
- * Update pg_constraint with the flags from cmdcon.
- *
* If called to modify a constraint that's already in the desired state,
* silently do nothing.
*/
- if (cmdcon->alterDeferrability &&
- (currcon->condeferrable != cmdcon->deferrable ||
- currcon->condeferred != cmdcon->initdeferred))
+ if (currcon->condeferrable != cmdcon->deferrable ||
+ currcon->condeferred != cmdcon->initdeferred)
{
UpdateConstraintEntry(cmdcon, conrel, contuple);
changed = true;
@@ -12113,75 +12154,89 @@ ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon,
* If the table at either end of the constraint is partitioned, we need to
* handle every constraint that is a child of this one.
*/
- if (recurse && changed &&
+ if (recurse &&
(rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
- (OidIsValid(refrelid) &&
- get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)))
- ATExecAlterChildConstr(wqueue, cmdcon, conrel, tgrel, rel, contuple,
- recurse, otherrelids, lockmode);
+ get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE))
+ AlterConstrDeferrabilityRecurse(wqueue, cmdcon, conrel, tgrel, rel,
+ contuple, recurse, otherrelids,
+ lockmode);
+
+ return changed;
+}
+
+/*
+ * Returns true if the constraint's inheritability is altered.
+ */
+static bool
+ATExecAlterConstrInheritability(List **wqueue, ATAlterConstraint *cmdcon,
+ Relation conrel, Relation rel,
+ HeapTuple contuple, LOCKMODE lockmode)
+{
+ Form_pg_constraint currcon;
+ AttrNumber colNum;
+ char *colName;
+ List *children;
+
+ Assert(cmdcon->alterInheritability);
+
+ currcon = (Form_pg_constraint) GETSTRUCT(contuple);
+
+ /* The current implementation only works for NOT NULL constraints */
+ Assert(currcon->contype == CONSTRAINT_NOTNULL);
+
+ /*
+ * If called to modify a constraint that's already in the desired state,
+ * silently do nothing.
+ */
+ if (cmdcon->noinherit == currcon->connoinherit)
+ return false;
+
+ UpdateConstraintEntry(cmdcon, conrel, contuple);
+ CommandCounterIncrement();
+
+ /* Fetch the column number and name */
+ colNum = extractNotNullColumn(contuple);
+ colName = get_attname(currcon->conrelid, colNum, false);
/*
- * Update the catalog for inheritability. No work if the constraint is
- * already in the requested state.
+ * Propagate the change to children. For SET NO INHERIT, we don't
+ * recursively affect children, just the immediate level.
*/
- if (cmdcon->alterInheritability &&
- (cmdcon->noinherit != currcon->connoinherit))
+ children = find_inheritance_children(RelationGetRelid(rel),
+ lockmode);
+ foreach_oid(childoid, children)
{
- AttrNumber colNum;
- char *colName;
- List *children;
+ ObjectAddress addr;
- /* The current implementation only works for NOT NULL constraints */
- Assert(currcon->contype == CONSTRAINT_NOTNULL);
-
- UpdateConstraintEntry(cmdcon, conrel, contuple);
- CommandCounterIncrement();
- changed = true;
-
- /* Fetch the column number and name */
- colNum = extractNotNullColumn(contuple);
- colName = get_attname(currcon->conrelid, colNum, false);
-
- /*
- * Propagate the change to children. For SET NO INHERIT, we don't
- * recursively affect children, just the immediate level.
- */
- children = find_inheritance_children(RelationGetRelid(rel),
- lockmode);
- foreach_oid(childoid, children)
+ if (cmdcon->noinherit)
{
- ObjectAddress addr;
+ HeapTuple childtup;
+ Form_pg_constraint childcon;
- if (cmdcon->noinherit)
- {
- HeapTuple childtup;
- Form_pg_constraint childcon;
-
- childtup = findNotNullConstraint(childoid, colName);
- if (!childtup)
- elog(ERROR, "cache lookup failed for not-null constraint on column \"%s\" of relation %u",
- colName, childoid);
- childcon = (Form_pg_constraint) GETSTRUCT(childtup);
- Assert(childcon->coninhcount > 0);
- childcon->coninhcount--;
- childcon->conislocal = true;
- CatalogTupleUpdate(conrel, &childtup->t_self, childtup);
- heap_freetuple(childtup);
- }
- else
- {
- Relation childrel = table_open(childoid, NoLock);
+ childtup = findNotNullConstraint(childoid, colName);
+ if (!childtup)
+ elog(ERROR, "cache lookup failed for not-null constraint on column \"%s\" of relation %u",
+ colName, childoid);
+ childcon = (Form_pg_constraint) GETSTRUCT(childtup);
+ Assert(childcon->coninhcount > 0);
+ childcon->coninhcount--;
+ childcon->conislocal = true;
+ CatalogTupleUpdate(conrel, &childtup->t_self, childtup);
+ heap_freetuple(childtup);
+ }
+ else
+ {
+ Relation childrel = table_open(childoid, NoLock);
- addr = ATExecSetNotNull(wqueue, childrel, NameStr(currcon->conname),
- colName, true, true, lockmode);
- if (OidIsValid(addr.objectId))
- CommandCounterIncrement();
- table_close(childrel, NoLock);
- }
+ addr = ATExecSetNotNull(wqueue, childrel, NameStr(currcon->conname),
+ colName, true, true, lockmode);
+ if (OidIsValid(addr.objectId))
+ CommandCounterIncrement();
+ table_close(childrel, NoLock);
}
}
- return changed;
+ return true;
}
/*
@@ -12250,7 +12305,7 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
}
/*
- * Invokes ATExecAlterConstraintInternal for each constraint that is a child of
+ * Invokes ATExecAlterConstrDeferrability for each constraint that is a child of
* the specified constraint.
*
* Note that this doesn't handle recursion the normal way, viz. by scanning the
@@ -12258,13 +12313,13 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
* relationships. This may need to be reconsidered.
*
* The arguments to this function have the same meaning as the arguments to
- * ATExecAlterConstraintInternal.
+ * ATExecAlterConstrDeferrability.
*/
static void
-ATExecAlterChildConstr(List **wqueue, ATAlterConstraint *cmdcon,
- Relation conrel, Relation tgrel, Relation rel,
- HeapTuple contuple, bool recurse, List **otherrelids,
- LOCKMODE lockmode)
+AlterConstrDeferrabilityRecurse(List **wqueue, ATAlterConstraint *cmdcon,
+ Relation conrel, Relation tgrel, Relation rel,
+ HeapTuple contuple, bool recurse,
+ List **otherrelids, LOCKMODE lockmode)
{
Form_pg_constraint currcon;
Oid conoid;
@@ -12289,8 +12344,9 @@ ATExecAlterChildConstr(List **wqueue, ATAlterConstraint *cmdcon,
Relation childrel;
childrel = table_open(childcon->conrelid, lockmode);
- ATExecAlterConstraintInternal(wqueue, cmdcon, conrel, tgrel, childrel,
- childtup, recurse, otherrelids, lockmode);
+
+ ATExecAlterConstrDeferrability(wqueue, cmdcon, conrel, tgrel, childrel,
+ childtup, recurse, otherrelids, lockmode);
table_close(childrel, NoLock);
}
--
2.43.5
v18-0003-refactor-Pass-Relid-instead-of-Relation-to-creat.patchapplication/octet-stream; name=v18-0003-refactor-Pass-Relid-instead-of-Relation-to-creat.patchDownload
From 3cc56af2c1292743f377b75d90d97e614947b47d Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 20 Jan 2025 18:49:41 +0530
Subject: [PATCH v18 3/6] refactor: Pass Relid instead of Relation to
createForeignKeyCheckTriggers().
Currently, createForeignKeyCheckTriggers() takes a Relation type as
its first argument, but it doesn't use that argument directly.
Instead, it fetches the Relid by calling RelationGetRelid().
Therefore, it would be more consistent with other functions (e.g.,
createForeignKeyCheckTriggers()) to pass the Relid directly instead of
the whole Relation.
---
src/backend/commands/tablecmds.c | 20 ++++++++++----------
1 file changed, 10 insertions(+), 10 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index f02778972c7..81c7e230965 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -580,7 +580,7 @@ static void createForeignKeyCheckTriggers(Oid myRelOid, Oid refRelOid,
Oid indexOid,
Oid parentInsTrigger, Oid parentUpdTrigger,
Oid *insertTrigOid, Oid *updateTrigOid);
-static void createForeignKeyActionTriggers(Relation rel, Oid refRelOid,
+static void createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid,
Constraint *fkconstraint, Oid constraintOid,
Oid indexOid,
Oid parentDelTrigger, Oid parentUpdTrigger,
@@ -10690,7 +10690,8 @@ addFkRecurseReferenced(Constraint *fkconstraint, Relation rel,
/*
* Create the action triggers that enforce the constraint.
*/
- createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
+ createForeignKeyActionTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
fkconstraint,
parentConstr, indexOid,
parentDelTrigger, parentUpdTrigger,
@@ -13219,10 +13220,11 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
* *updateTrigOid.
*/
static void
-createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstraint,
- Oid constraintOid, Oid indexOid,
- Oid parentDelTrigger, Oid parentUpdTrigger,
- Oid *deleteTrigOid, Oid *updateTrigOid)
+createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid,
+ Constraint *fkconstraint, Oid constraintOid,
+ Oid indexOid, Oid parentDelTrigger,
+ Oid parentUpdTrigger, Oid *deleteTrigOid,
+ Oid *updateTrigOid)
{
CreateTrigStmt *fk_trigger;
ObjectAddress trigAddress;
@@ -13278,8 +13280,7 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
break;
}
- trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid,
- RelationGetRelid(rel),
+ trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid, myRelOid,
constraintOid, indexOid, InvalidOid,
parentDelTrigger, NULL, true, false);
if (deleteTrigOid)
@@ -13339,8 +13340,7 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
break;
}
- trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid,
- RelationGetRelid(rel),
+ trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid, myRelOid,
constraintOid, indexOid, InvalidOid,
parentUpdTrigger, NULL, true, false);
if (updateTrigOid)
--
2.43.5
v18-0004-Remove-hastriggers-flag-check-before-fetching-FK.patchapplication/octet-stream; name=v18-0004-Remove-hastriggers-flag-check-before-fetching-FK.patchDownload
From 540b88e41b54b1cabd06f21d7c405de67855fdfc Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 7 Oct 2024 12:40:07 +0530
Subject: [PATCH v18 4/6] Remove hastriggers flag check before fetching FK
constraints.
With NOT ENFORCED, foreign key constraints will be created without
triggers. Therefore, the criteria for fetching foreign keys based on
the presence of triggers no longer apply.
----
NOTE: This patch is intended to reduce the diff noise from the main
patch and is not meant to be committed separately. It should be
squashed with the main patch that adds ENFORCED/NOT ENFORCED.
----
---
src/backend/utils/cache/relcache.c | 5 -
src/bin/pg_dump/pg_dump.c | 8 +-
src/bin/psql/describe.c | 226 ++++++++++++++---------------
3 files changed, 108 insertions(+), 131 deletions(-)
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 9f54a9e72b7..e6721056536 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4666,11 +4666,6 @@ RelationGetFKeyList(Relation relation)
if (relation->rd_fkeyvalid)
return relation->rd_fkeylist;
- /* Fast path: non-partitioned tables without triggers can't have FKs */
- if (!relation->rd_rel->relhastriggers &&
- relation->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
- return NIL;
-
/*
* We build the list we intend to return (in the caller's context) while
* doing the scan. After successfully completing the scan, we copy that
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 428ed2d60fc..b8af71cbbef 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -7981,13 +7981,7 @@ getConstraints(Archive *fout, TableInfo tblinfo[], int numTables)
{
TableInfo *tinfo = &tblinfo[i];
- /*
- * For partitioned tables, foreign keys have no triggers so they must
- * be included anyway in case some foreign keys are defined.
- */
- if ((!tinfo->hastriggers &&
- tinfo->relkind != RELKIND_PARTITIONED_TABLE) ||
- !(tinfo->dobj.dump & DUMP_COMPONENT_DEFINITION))
+ if (!(tinfo->dobj.dump & DUMP_COMPONENT_DEFINITION))
continue;
/* OK, we need info for this table */
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..a23abb2417c 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2550,136 +2550,124 @@ describeOneTableDetails(const char *schemaname,
PQclear(result);
}
- /*
- * Print foreign-key constraints (there are none if no triggers,
- * except if the table is partitioned, in which case the triggers
- * appear in the partitions)
- */
- if (tableinfo.hastriggers ||
- tableinfo.relkind == RELKIND_PARTITIONED_TABLE)
+ /* Print foreign-key constraints */
+ if (pset.sversion >= 120000 &&
+ (tableinfo.ispartition || tableinfo.relkind == RELKIND_PARTITIONED_TABLE))
{
- if (pset.sversion >= 120000 &&
- (tableinfo.ispartition || tableinfo.relkind == RELKIND_PARTITIONED_TABLE))
+ /*
+ * Put the constraints defined in this table first, followed
+ * by the constraints defined in ancestor partitioned tables.
+ */
+ printfPQExpBuffer(&buf,
+ "SELECT conrelid = '%s'::pg_catalog.regclass AS sametable,\n"
+ " conname,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef,\n"
+ " conrelid::pg_catalog.regclass AS ontable\n"
+ " FROM pg_catalog.pg_constraint,\n"
+ " pg_catalog.pg_partition_ancestors('%s')\n"
+ " WHERE conrelid = relid AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
+ "ORDER BY sametable DESC, conname;",
+ oid, oid);
+ }
+ else
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT true as sametable, conname,\n"
+ " pg_catalog.pg_get_constraintdef(r.oid, true) as condef,\n"
+ " conrelid::pg_catalog.regclass AS ontable\n"
+ "FROM pg_catalog.pg_constraint r\n"
+ "WHERE r.conrelid = '%s' AND r.contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n",
+ oid);
+
+ if (pset.sversion >= 120000)
+ appendPQExpBufferStr(&buf, " AND conparentid = 0\n");
+ appendPQExpBufferStr(&buf, "ORDER BY conname");
+ }
+
+ result = PSQLexec(buf.data);
+ if (!result)
+ goto error_return;
+ else
+ tuples = PQntuples(result);
+
+ if (tuples > 0)
+ {
+ int i_sametable = PQfnumber(result, "sametable"),
+ i_conname = PQfnumber(result, "conname"),
+ i_condef = PQfnumber(result, "condef"),
+ i_ontable = PQfnumber(result, "ontable");
+
+ printTableAddFooter(&cont, _("Foreign-key constraints:"));
+ for (i = 0; i < tuples; i++)
{
/*
- * Put the constraints defined in this table first, followed
- * by the constraints defined in ancestor partitioned tables.
+ * Print untranslated constraint name and definition. Use
+ * a "TABLE tab" prefix when the constraint is defined in
+ * a parent partitioned table.
*/
- printfPQExpBuffer(&buf,
- "SELECT conrelid = '%s'::pg_catalog.regclass AS sametable,\n"
- " conname,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef,\n"
- " conrelid::pg_catalog.regclass AS ontable\n"
- " FROM pg_catalog.pg_constraint,\n"
- " pg_catalog.pg_partition_ancestors('%s')\n"
- " WHERE conrelid = relid AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
- "ORDER BY sametable DESC, conname;",
- oid, oid);
- }
- else
- {
- printfPQExpBuffer(&buf,
- "SELECT true as sametable, conname,\n"
- " pg_catalog.pg_get_constraintdef(r.oid, true) as condef,\n"
- " conrelid::pg_catalog.regclass AS ontable\n"
- "FROM pg_catalog.pg_constraint r\n"
- "WHERE r.conrelid = '%s' AND r.contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n",
- oid);
-
- if (pset.sversion >= 120000)
- appendPQExpBufferStr(&buf, " AND conparentid = 0\n");
- appendPQExpBufferStr(&buf, "ORDER BY conname");
- }
-
- result = PSQLexec(buf.data);
- if (!result)
- goto error_return;
- else
- tuples = PQntuples(result);
-
- if (tuples > 0)
- {
- int i_sametable = PQfnumber(result, "sametable"),
- i_conname = PQfnumber(result, "conname"),
- i_condef = PQfnumber(result, "condef"),
- i_ontable = PQfnumber(result, "ontable");
-
- printTableAddFooter(&cont, _("Foreign-key constraints:"));
- for (i = 0; i < tuples; i++)
- {
- /*
- * Print untranslated constraint name and definition. Use
- * a "TABLE tab" prefix when the constraint is defined in
- * a parent partitioned table.
- */
- if (strcmp(PQgetvalue(result, i, i_sametable), "f") == 0)
- printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
- PQgetvalue(result, i, i_ontable),
- PQgetvalue(result, i, i_conname),
- PQgetvalue(result, i, i_condef));
- else
- printfPQExpBuffer(&buf, " \"%s\" %s",
- PQgetvalue(result, i, i_conname),
- PQgetvalue(result, i, i_condef));
-
- printTableAddFooter(&cont, buf.data);
- }
- }
- PQclear(result);
- }
-
- /* print incoming foreign-key references */
- if (tableinfo.hastriggers ||
- tableinfo.relkind == RELKIND_PARTITIONED_TABLE)
- {
- if (pset.sversion >= 120000)
- {
- printfPQExpBuffer(&buf,
- "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
- " FROM pg_catalog.pg_constraint c\n"
- " WHERE confrelid IN (SELECT pg_catalog.pg_partition_ancestors('%s')\n"
- " UNION ALL VALUES ('%s'::pg_catalog.regclass))\n"
- " AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
- "ORDER BY conname;",
- oid, oid);
- }
- else
- {
- printfPQExpBuffer(&buf,
- "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
- " FROM pg_catalog.pg_constraint\n"
- " WHERE confrelid = %s AND contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n"
- "ORDER BY conname;",
- oid);
- }
-
- result = PSQLexec(buf.data);
- if (!result)
- goto error_return;
- else
- tuples = PQntuples(result);
-
- if (tuples > 0)
- {
- int i_conname = PQfnumber(result, "conname"),
- i_ontable = PQfnumber(result, "ontable"),
- i_condef = PQfnumber(result, "condef");
-
- printTableAddFooter(&cont, _("Referenced by:"));
- for (i = 0; i < tuples; i++)
- {
+ if (strcmp(PQgetvalue(result, i, i_sametable), "f") == 0)
printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
PQgetvalue(result, i, i_ontable),
PQgetvalue(result, i, i_conname),
PQgetvalue(result, i, i_condef));
+ else
+ printfPQExpBuffer(&buf, " \"%s\" %s",
+ PQgetvalue(result, i, i_conname),
+ PQgetvalue(result, i, i_condef));
- printTableAddFooter(&cont, buf.data);
- }
+ printTableAddFooter(&cont, buf.data);
}
- PQclear(result);
}
+ PQclear(result);
+
+ /* print incoming foreign-key references */
+ if (pset.sversion >= 120000)
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
+ " FROM pg_catalog.pg_constraint c\n"
+ " WHERE confrelid IN (SELECT pg_catalog.pg_partition_ancestors('%s')\n"
+ " UNION ALL VALUES ('%s'::pg_catalog.regclass))\n"
+ " AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
+ "ORDER BY conname;",
+ oid, oid);
+ }
+ else
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
+ " FROM pg_catalog.pg_constraint\n"
+ " WHERE confrelid = %s AND contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n"
+ "ORDER BY conname;",
+ oid);
+ }
+
+ result = PSQLexec(buf.data);
+ if (!result)
+ goto error_return;
+ else
+ tuples = PQntuples(result);
+
+ if (tuples > 0)
+ {
+ int i_conname = PQfnumber(result, "conname"),
+ i_ontable = PQfnumber(result, "ontable"),
+ i_condef = PQfnumber(result, "condef");
+
+ printTableAddFooter(&cont, _("Referenced by:"));
+ for (i = 0; i < tuples; i++)
+ {
+ printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
+ PQgetvalue(result, i, i_ontable),
+ PQgetvalue(result, i, i_conname),
+ PQgetvalue(result, i, i_condef));
+
+ printTableAddFooter(&cont, buf.data);
+ }
+ }
+ PQclear(result);
/* print any row-level policies */
if (pset.sversion >= 90500)
--
2.43.5
v18-0005-Add-support-for-NOT-ENFORCED-in-foreign-key-cons.patchapplication/octet-stream; name=v18-0005-Add-support-for-NOT-ENFORCED-in-foreign-key-cons.patchDownload
From 10235f3aaf569e24ec973962e395148013134bd0 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Fri, 7 Feb 2025 09:41:20 +0530
Subject: [PATCH v18 5/6] Add support for NOT ENFORCED in foreign key
constraints.
Typically, when a foreign key (FK) constraint is created on a table,
action and check triggers are added to maintain data integrity. With
this patch, if a constraint is marked as NOT ENFORCED, integrity
checks are no longer required, making these triggers unnecessary.
Consequently, when creating a NOT ENFORCED FK constraint, triggers
will not be created, and the constraint will be marked as NOT VALID.
Similarly, if an existing FK constraint is changed to NOT ENFORCED,
the associated triggers will be dropped, and the constraint will also
be marked as NOT VALID. Conversely, if a NOT ENFORCED FK constraint is
changed to ENFORCED, the necessary triggers will be created, and the
will be changed to VALID by performing necessary validation.
----
NOTE: In this patch, the tryAttachPartitionForeignKey() function will
not merge the constraint if the enforcibility differs. This will be
addressed in the next patch.
----
---
doc/src/sgml/catalogs.sgml | 2 +-
doc/src/sgml/ref/alter_table.sgml | 5 +-
doc/src/sgml/ref/create_table.sgml | 2 +-
src/backend/catalog/pg_constraint.c | 5 +-
src/backend/catalog/sql_features.txt | 2 +-
src/backend/commands/tablecmds.c | 455 ++++++++++++++++++----
src/backend/parser/gram.y | 12 +-
src/backend/parser/parse_utilcmd.c | 14 +-
src/backend/utils/cache/relcache.c | 1 +
src/include/nodes/parsenodes.h | 2 +
src/include/utils/rel.h | 3 +
src/test/regress/expected/constraints.out | 8 +-
src/test/regress/expected/foreign_key.out | 161 +++++++-
src/test/regress/expected/inherit.out | 83 ++--
src/test/regress/sql/foreign_key.sql | 107 ++++-
src/test/regress/sql/inherit.sql | 9 +-
16 files changed, 724 insertions(+), 147 deletions(-)
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index fb050635551..014bb815665 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2620,7 +2620,7 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l
</para>
<para>
Is the constraint enforced?
- Currently, can be false only for CHECK constraints
+ Currently, can be false only for foreign keys and CHECK constraints
</para></entry>
</row>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 4f15b89a98f..91dca4f8773 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -58,7 +58,7 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable>
ALTER [ COLUMN ] <replaceable class="parameter">column_name</replaceable> SET COMPRESSION <replaceable class="parameter">compression_method</replaceable>
ADD <replaceable class="parameter">table_constraint</replaceable> [ NOT VALID ]
ADD <replaceable class="parameter">table_constraint_using_index</replaceable>
- ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+ ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]
ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> SET [ INHERIT | NO INHERIT ]
VALIDATE CONSTRAINT <replaceable class="parameter">constraint_name</replaceable>
DROP CONSTRAINT [ IF EXISTS ] <replaceable class="parameter">constraint_name</replaceable> [ RESTRICT | CASCADE ]
@@ -589,7 +589,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
This form validates a foreign key or check constraint that was
previously created as <literal>NOT VALID</literal>, by scanning the
table to ensure there are no rows for which the constraint is not
- satisfied. Nothing happens if the constraint is already marked valid.
+ satisfied. If the constraint is not enforced, an error is thrown.
+ Nothing happens if the constraint is already marked valid.
(See <xref linkend="sql-altertable-notes"/> below for an explanation
of the usefulness of this command.)
</para>
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index e5c034d724e..4a41b2f5530 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -1409,7 +1409,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</para>
<para>
- This is currently only supported for <literal>CHECK</literal>
+ This is currently only supported for foreign key and <literal>CHECK</literal>
constraints.
</para>
</listitem>
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index ac80652baf2..0467e7442ff 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -100,8 +100,9 @@ CreateConstraintEntry(const char *constraintName,
ObjectAddresses *addrs_auto;
ObjectAddresses *addrs_normal;
- /* Only CHECK constraint can be not enforced */
- Assert(isEnforced || constraintType == CONSTRAINT_CHECK);
+ /* Only CHECK or FOREIGN KEY constraint can be not enforced */
+ Assert(isEnforced || constraintType == CONSTRAINT_CHECK ||
+ constraintType == CONSTRAINT_FOREIGN);
/* NOT ENFORCED constraint must be NOT VALID */
Assert(isEnforced || !isValidated);
diff --git a/src/backend/catalog/sql_features.txt b/src/backend/catalog/sql_features.txt
index 2f250d2c57b..ebe85337c28 100644
--- a/src/backend/catalog/sql_features.txt
+++ b/src/backend/catalog/sql_features.txt
@@ -281,7 +281,7 @@ F461 Named character sets NO
F471 Scalar subquery values YES
F481 Expanded NULL predicate YES
F491 Constraint management YES
-F492 Optional table constraint enforcement NO check constraints only
+F492 Optional table constraint enforcement YES except not-null constraints
F501 Features and conformance views YES
F501 Features and conformance views 01 SQL_FEATURES view YES
F501 Features and conformance views 02 SQL_SIZING view YES
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 81c7e230965..2f53acc8b7d 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -395,6 +395,14 @@ static ObjectAddress ATExecAlterConstraint(List **wqueue, Relation rel,
static bool ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon, Relation conrel,
Relation tgrel, Relation rel, HeapTuple contuple,
bool recurse, LOCKMODE lockmode);
+static bool ATExecAlterConstrEnforceability(List **wqueue, ATAlterConstraint *cmdcon,
+ Relation conrel, Relation tgrel,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, LOCKMODE lockmode,
+ Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger);
static bool ATExecAlterConstrDeferrability(List **wqueue, ATAlterConstraint *cmdcon,
Relation conrel, Relation tgrel, Relation rel,
HeapTuple contuple, bool recurse,
@@ -405,6 +413,14 @@ static bool ATExecAlterConstrInheritability(List **wqueue, ATAlterConstraint *cm
static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
bool deferrable, bool initdeferred,
List **otherrelids);
+static void AlterConstrEnforceabilityRecurse(List **wqueue, ATAlterConstraint *cmdcon,
+ Relation conrel, Relation tgrel,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, LOCKMODE lockmode,
+ Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger);
static void AlterConstrDeferrabilityRecurse(List **wqueue, ATAlterConstraint *cmdcon,
Relation conrel, Relation tgrel, Relation rel,
HeapTuple contuple, bool recurse,
@@ -10563,7 +10579,7 @@ addFkConstraint(addFkConstraintSides fkside,
CONSTRAINT_FOREIGN,
fkconstraint->deferrable,
fkconstraint->initdeferred,
- true, /* Is Enforced */
+ fkconstraint->is_enforced,
fkconstraint->initially_valid,
parentConstr,
RelationGetRelid(rel),
@@ -10681,21 +10697,23 @@ addFkRecurseReferenced(Constraint *fkconstraint, Relation rel,
Oid parentDelTrigger, Oid parentUpdTrigger,
bool with_period)
{
- Oid deleteTriggerOid,
- updateTriggerOid;
+ Oid deleteTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
Assert(CheckRelationLockedByMe(pkrel, ShareRowExclusiveLock, true));
Assert(CheckRelationLockedByMe(rel, ShareRowExclusiveLock, true));
/*
- * Create the action triggers that enforce the constraint.
+ * Create action triggers to enforce the constraint, or skip them if the
+ * constraint is NOT ENFORCED.
*/
- createForeignKeyActionTriggers(RelationGetRelid(rel),
- RelationGetRelid(pkrel),
- fkconstraint,
- parentConstr, indexOid,
- parentDelTrigger, parentUpdTrigger,
- &deleteTriggerOid, &updateTriggerOid);
+ if (fkconstraint->is_enforced)
+ createForeignKeyActionTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
+ fkconstraint,
+ parentConstr, indexOid,
+ parentDelTrigger, parentUpdTrigger,
+ &deleteTriggerOid, &updateTriggerOid);
/*
* If the referenced table is partitioned, recurse on ourselves to handle
@@ -10816,8 +10834,8 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
Oid parentInsTrigger, Oid parentUpdTrigger,
bool with_period)
{
- Oid insertTriggerOid,
- updateTriggerOid;
+ Oid insertTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
Assert(OidIsValid(parentConstr));
Assert(CheckRelationLockedByMe(rel, ShareRowExclusiveLock, true));
@@ -10829,29 +10847,32 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
errmsg("foreign key constraints are not supported on foreign tables")));
/*
- * Add the check triggers to it and, if necessary, schedule it to be
- * checked in Phase 3.
+ * Add check triggers if the constraint is ENFORCED, and if needed,
+ * schedule them to be checked in Phase 3.
*
* If the relation is partitioned, drill down to do it to its partitions.
*/
- createForeignKeyCheckTriggers(RelationGetRelid(rel),
- RelationGetRelid(pkrel),
- fkconstraint,
- parentConstr,
- indexOid,
- parentInsTrigger, parentUpdTrigger,
- &insertTriggerOid, &updateTriggerOid);
+ if (fkconstraint->is_enforced)
+ createForeignKeyCheckTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
+ fkconstraint,
+ parentConstr,
+ indexOid,
+ parentInsTrigger, parentUpdTrigger,
+ &insertTriggerOid, &updateTriggerOid);
if (rel->rd_rel->relkind == RELKIND_RELATION)
{
/*
* Tell Phase 3 to check that the constraint is satisfied by existing
- * rows. We can skip this during table creation, when requested
- * explicitly by specifying NOT VALID in an ADD FOREIGN KEY command,
- * and when we're recreating a constraint following a SET DATA TYPE
- * operation that did not impugn its validity.
+ * rows. We can skip this during table creation, when constraint is
+ * specified as NOT ENFORCED, or when requested explicitly by
+ * specifying NOT VALID in an ADD FOREIGN KEY command, and when we're
+ * recreating a constraint following a SET DATA TYPE operation that
+ * did not impugn its validity.
*/
- if (wqueue && !old_check_ok && !fkconstraint->skip_validation)
+ if (wqueue && !old_check_ok && !fkconstraint->skip_validation &&
+ fkconstraint->is_enforced)
{
NewConstraint *newcon;
AlteredTableInfo *tab;
@@ -11082,8 +11103,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
AttrNumber confdelsetcols[INDEX_MAX_KEYS];
Constraint *fkconstraint;
ObjectAddress address;
- Oid deleteTriggerOid,
- updateTriggerOid;
+ Oid deleteTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(constrOid));
if (!HeapTupleIsValid(tuple))
@@ -11143,8 +11164,9 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
fkconstraint->fk_del_set_cols = NIL;
fkconstraint->old_conpfeqop = NIL;
fkconstraint->old_pktable_oid = InvalidOid;
+ fkconstraint->is_enforced = constrForm->conenforced;
fkconstraint->skip_validation = false;
- fkconstraint->initially_valid = true;
+ fkconstraint->initially_valid = constrForm->convalidated;
/* set up colnames that are used to generate the constraint name */
for (int i = 0; i < numfks; i++)
@@ -11172,9 +11194,10 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
* parent OIDs for similar triggers that will be created on the
* partition in addFkRecurseReferenced().
*/
- GetForeignKeyActionTriggers(trigrel, constrOid,
- constrForm->confrelid, constrForm->conrelid,
- &deleteTriggerOid, &updateTriggerOid);
+ if (constrForm->conenforced)
+ GetForeignKeyActionTriggers(trigrel, constrOid,
+ constrForm->confrelid, constrForm->conrelid,
+ &deleteTriggerOid, &updateTriggerOid);
/* Add this constraint ... */
address = addFkConstraint(addFkReferencedSide,
@@ -11307,8 +11330,8 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
Oid indexOid;
ObjectAddress address;
ListCell *lc;
- Oid insertTriggerOid,
- updateTriggerOid;
+ Oid insertTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
bool with_period;
tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(parentConstrOid));
@@ -11340,17 +11363,18 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
mapped_conkey[i] = attmap->attnums[conkey[i] - 1];
/*
- * Get the "check" triggers belonging to the constraint to pass as
- * parent OIDs for similar triggers that will be created on the
- * partition in addFkRecurseReferencing(). They are also passed to
- * tryAttachPartitionForeignKey() below to simply assign as parents to
- * the partition's existing "check" triggers, that is, if the
- * corresponding constraints is deemed attachable to the parent
- * constraint.
+ * Get the "check" triggers belonging to the constraint, if it is
+ * ENFORCED, to pass as parent OIDs for similar triggers that will be
+ * created on the partition in addFkRecurseReferencing(). They are
+ * also passed to tryAttachPartitionForeignKey() below to simply
+ * assign as parents to the partition's existing "check" triggers,
+ * that is, if the corresponding constraints is deemed attachable to
+ * the parent constraint.
*/
- GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
- constrForm->confrelid, constrForm->conrelid,
- &insertTriggerOid, &updateTriggerOid);
+ if (constrForm->conenforced)
+ GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
+ constrForm->confrelid, constrForm->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
/*
* Before creating a new constraint, see whether any existing FKs are
@@ -11403,6 +11427,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
fkconstraint->fk_del_set_cols = NIL;
fkconstraint->old_conpfeqop = NIL;
fkconstraint->old_pktable_oid = InvalidOid;
+ fkconstraint->is_enforced = constrForm->conenforced;
fkconstraint->skip_validation = false;
fkconstraint->initially_valid = constrForm->convalidated;
for (int i = 0; i < numfks; i++)
@@ -11520,6 +11545,7 @@ tryAttachPartitionForeignKey(List **wqueue,
if (OidIsValid(partConstr->conparentid) ||
partConstr->condeferrable != parentConstr->condeferrable ||
partConstr->condeferred != parentConstr->condeferred ||
+ partConstr->conenforced != parentConstr->conenforced ||
partConstr->confupdtype != parentConstr->confupdtype ||
partConstr->confdeltype != parentConstr->confdeltype ||
partConstr->confmatchtype != parentConstr->confmatchtype)
@@ -11563,8 +11589,7 @@ AttachPartitionForeignKey(List **wqueue,
bool queueValidation;
Oid partConstrFrelid;
Oid partConstrRelid;
- Oid insertTriggerOid,
- updateTriggerOid;
+ bool parentConstrIsEnforced;
/* Fetch the parent constraint tuple */
parentConstrTup = SearchSysCache1(CONSTROID,
@@ -11572,6 +11597,7 @@ AttachPartitionForeignKey(List **wqueue,
if (!HeapTupleIsValid(parentConstrTup))
elog(ERROR, "cache lookup failed for constraint %u", parentConstrOid);
parentConstr = (Form_pg_constraint) GETSTRUCT(parentConstrTup);
+ parentConstrIsEnforced = parentConstr->conenforced;
/* Fetch the child constraint tuple */
partcontup = SearchSysCache1(CONSTROID,
@@ -11621,17 +11647,24 @@ AttachPartitionForeignKey(List **wqueue,
/*
* Like the constraint, attach partition's "check" triggers to the
- * corresponding parent triggers.
+ * corresponding parent triggers if the constraint is ENFORCED.
+ * NOT ENFORCED constraints do not have these triggers.
*/
- GetForeignKeyCheckTriggers(trigrel,
- partConstrOid, partConstrFrelid, partConstrRelid,
- &insertTriggerOid, &updateTriggerOid);
- Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
- TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
- RelationGetRelid(partition));
- Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
- TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
- RelationGetRelid(partition));
+ if (parentConstrIsEnforced)
+ {
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
+ GetForeignKeyCheckTriggers(trigrel,
+ partConstrOid, partConstrFrelid, partConstrRelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
+ RelationGetRelid(partition));
+ Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
+ RelationGetRelid(partition));
+ }
/*
* We updated this pg_constraint row above to set its parent; validating
@@ -11745,6 +11778,10 @@ RemoveInheritedConstraint(Relation conrel, Relation trigrel, Oid conoid,
*
* The subroutine for tryAttachPartitionForeignKey handles the deletion of
* action triggers for the foreign key constraint.
+ *
+ * If valid confrelid and conrelid values are not provided, the respective
+ * trigger check will be skipped, and the trigger will be considered for
+ * removal.
*/
static void
DropForeignKeyConstraintTriggers(Relation trigrel, Oid conoid, Oid confrelid,
@@ -11765,10 +11802,27 @@ DropForeignKeyConstraintTriggers(Relation trigrel, Oid conoid, Oid confrelid,
Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
ObjectAddress trigger;
- if (trgform->tgconstrrelid != conrelid)
+ /* Invalid if trigger is not for a referential integrity constraint */
+ if (!OidIsValid(trgform->tgconstrrelid))
continue;
- if (trgform->tgrelid != confrelid)
+ if (OidIsValid(conrelid) && trgform->tgconstrrelid != conrelid)
continue;
+ if (OidIsValid(confrelid) && trgform->tgrelid != confrelid)
+ continue;
+
+ /* We should be droping trigger related to foreign key constraint */
+ Assert(trgform->tgfoid == F_RI_FKEY_CHECK_INS ||
+ trgform->tgfoid == F_RI_FKEY_CHECK_UPD ||
+ trgform->tgfoid == F_RI_FKEY_CASCADE_DEL ||
+ trgform->tgfoid == F_RI_FKEY_CASCADE_UPD ||
+ trgform->tgfoid == F_RI_FKEY_RESTRICT_DEL ||
+ trgform->tgfoid == F_RI_FKEY_RESTRICT_UPD ||
+ trgform->tgfoid == F_RI_FKEY_SETNULL_DEL ||
+ trgform->tgfoid == F_RI_FKEY_SETNULL_UPD ||
+ trgform->tgfoid == F_RI_FKEY_SETDEFAULT_DEL ||
+ trgform->tgfoid == F_RI_FKEY_SETDEFAULT_UPD ||
+ trgform->tgfoid == F_RI_FKEY_NOACTION_DEL ||
+ trgform->tgfoid == F_RI_FKEY_NOACTION_UPD);
/*
* The constraint is originally set up to contain this trigger as an
@@ -11981,6 +12035,11 @@ ATExecAlterConstraint(List **wqueue, Relation rel, ATAlterConstraint *cmdcon,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("constraint \"%s\" of relation \"%s\" is not a foreign key constraint",
cmdcon->conname, RelationGetRelationName(rel))));
+ if (cmdcon->alterEnforceability && currcon->contype != CONSTRAINT_FOREIGN)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot alter enforceability of constraint \"%s\" of relation \"%s\"",
+ cmdcon->conname, RelationGetRelationName(rel))));
if (cmdcon->alterInheritability &&
currcon->contype != CONSTRAINT_NOTNULL)
ereport(ERROR,
@@ -12060,7 +12119,7 @@ ATExecAlterConstraint(List **wqueue, Relation rel, ATAlterConstraint *cmdcon,
/*
* A subroutine of ATExecAlterConstraint that calls the respective routines for
- * altering constraint attributes.
+ * altering constraint's enforceability, deferrability or inheritability.
*/
static bool
ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon,
@@ -12068,16 +12127,35 @@ ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon,
HeapTuple contuple, bool recurse,
LOCKMODE lockmode)
{
+ Form_pg_constraint currcon;
bool changed = false;
List *otherrelids = NIL;
+ currcon = (Form_pg_constraint) GETSTRUCT(contuple);
+
/*
- * Do the catalog work for the deferrability change, recurse if necessary.
+ * Do the catalog work for the enforceability or deferrability change,
+ * recurse if necessary.
+ *
+ * Note that even if deferrability is requested to be altered along with
+ * enforceability, we don't need to explicitly update multiple entries in
+ * pg_trigger related to deferrability.
+ *
+ * Modifying enforceability involves either creating or dropping the
+ * trigger, during which the deferrability setting will be adjusted
+ * automatically.
*/
- if (cmdcon->alterDeferrability &&
- ATExecAlterConstrDeferrability(wqueue, cmdcon, conrel, tgrel, rel,
- contuple, recurse, &otherrelids,
- lockmode))
+ if (cmdcon->alterEnforceability &&
+ ATExecAlterConstrEnforceability(wqueue, cmdcon, conrel, tgrel,
+ currcon->conrelid, currcon->confrelid,
+ contuple, lockmode, InvalidOid,
+ InvalidOid, InvalidOid, InvalidOid))
+ changed = true;
+
+ else if (cmdcon->alterDeferrability &&
+ ATExecAlterConstrDeferrability(wqueue, cmdcon, conrel, tgrel, rel,
+ contuple, recurse, &otherrelids,
+ lockmode))
{
/*
* UpdateConstraintEntry already invalidated relcache for the
@@ -12101,6 +12179,151 @@ ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon,
return changed;
}
+/*
+ * Returns true if the constraint's enforceability is altered.
+ *
+ * Depending on whether the constraint is being set to ENFORCED or NOT
+ * ENFORCED, it creates or drops the trigger accordingly.
+ *
+ * Note that we must recurse even when trying to change a 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. 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.
+ */
+static bool
+ATExecAlterConstrEnforceability(List **wqueue, ATAlterConstraint *cmdcon,
+ Relation conrel, Relation tgrel,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, LOCKMODE lockmode,
+ Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger)
+{
+ Form_pg_constraint currcon;
+ Oid conoid;
+ Relation rel;
+ bool changed = false;
+
+ /* Since this function recurses, it could be driven to stack overflow */
+ check_stack_depth();
+
+ Assert(cmdcon->alterEnforceability);
+
+ currcon = (Form_pg_constraint) GETSTRUCT(contuple);
+ conoid = currcon->oid;
+
+ /* Should be foreign key constraint */
+ Assert(currcon->contype == CONSTRAINT_FOREIGN);
+
+ rel = table_open(currcon->conrelid, lockmode);
+
+ if (currcon->conenforced != cmdcon->is_enforced)
+ {
+ UpdateConstraintEntry(cmdcon, conrel, contuple);
+ changed = true;
+ }
+
+ /* Drop triggers */
+ if (!cmdcon->is_enforced)
+ {
+ /*
+ * When setting a constraint to NOT ENFORCED, the constraint triggers
+ * need to be dropped. Therefore, we must process the child relations
+ * first, followed by the parent, to account for dependencies.
+ */
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE)
+ AlterConstrEnforceabilityRecurse(wqueue, cmdcon, conrel, tgrel,
+ fkrelid, pkrelid, contuple,
+ lockmode, InvalidOid, InvalidOid,
+ InvalidOid, InvalidOid);
+
+ /* Drop all the triggers */
+ DropForeignKeyConstraintTriggers(tgrel, conoid, InvalidOid, InvalidOid);
+ }
+ else if (changed) /* Create triggers */
+ {
+ Oid ReferencedDelTriggerOid = InvalidOid,
+ ReferencedUpdTriggerOid = InvalidOid,
+ ReferencingInsTriggerOid = InvalidOid,
+ ReferencingUpdTriggerOid = InvalidOid;
+
+ /* Prepare the minimal information required for trigger creation. */
+ Constraint *fkconstraint = makeNode(Constraint);
+
+ fkconstraint->conname = pstrdup(NameStr(currcon->conname));
+ fkconstraint->fk_matchtype = currcon->confmatchtype;
+ fkconstraint->fk_upd_action = currcon->confupdtype;
+ fkconstraint->fk_del_action = currcon->confdeltype;
+
+ /* Create referenced triggers */
+ if (currcon->conrelid == fkrelid)
+ createForeignKeyActionTriggers(currcon->conrelid,
+ currcon->confrelid,
+ fkconstraint,
+ conoid,
+ currcon->conindid,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ &ReferencedDelTriggerOid,
+ &ReferencedUpdTriggerOid);
+
+ /* Create referencing triggers */
+ if (currcon->confrelid == pkrelid)
+ createForeignKeyCheckTriggers(currcon->conrelid,
+ pkrelid,
+ fkconstraint,
+ conoid,
+ currcon->conindid,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger,
+ &ReferencingInsTriggerOid,
+ &ReferencingUpdTriggerOid);
+
+ /*
+ * Tell Phase 3 to check that the constraint is satisfied by existing
+ * rows.
+ */
+ if (rel->rd_rel->relkind == RELKIND_RELATION)
+ {
+ AlteredTableInfo *tab;
+ NewConstraint *newcon;
+
+ newcon = (NewConstraint *) palloc0(sizeof(NewConstraint));
+ newcon->name = fkconstraint->conname;
+ newcon->contype = CONSTR_FOREIGN;
+ newcon->refrelid = currcon->confrelid;
+ newcon->refindid = currcon->conindid;
+ newcon->conid = currcon->oid;
+ newcon->qual = (Node *) fkconstraint;
+
+ /* Find or create work queue entry for this table */
+ tab = ATGetQueueEntry(wqueue, rel);
+ tab->constraints = lappend(tab->constraints, newcon);
+ }
+
+ /*
+ * If the table at either end of the constraint is partitioned, we
+ * need to recurse and create triggers for each constraint that is a
+ * child of this one.
+ */
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE)
+ AlterConstrEnforceabilityRecurse(wqueue, cmdcon, conrel, tgrel,
+ fkrelid, pkrelid, contuple,
+ lockmode, ReferencedDelTriggerOid,
+ ReferencedUpdTriggerOid,
+ ReferencingInsTriggerOid,
+ ReferencingUpdTriggerOid);
+ }
+
+ table_close(rel, NoLock);
+
+ return changed;
+}
+
/*
* Returns true if the constraint's deferrability is altered.
*
@@ -12305,6 +12528,55 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
systable_endscan(tgscan);
}
+/*
+ * Invokes ATExecAlterConstrEnforceability for each constraint that is a child of
+ * the specified constraint.
+ *
+ * Note that this doesn't handle recursion the normal way, viz. by scanning the
+ * list of child relations and recursing; instead it uses the conparentid
+ * relationships. This may need to be reconsidered.
+ *
+ * The arguments to this function have the same meaning as the arguments to
+ * ATExecAlterConstrEnforceability.
+ */
+static void
+AlterConstrEnforceabilityRecurse(List **wqueue, ATAlterConstraint *cmdcon,
+ Relation conrel, Relation tgrel,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, LOCKMODE lockmode,
+ Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger)
+{
+ Form_pg_constraint currcon;
+ Oid conoid;
+ ScanKeyData pkey;
+ SysScanDesc pscan;
+ HeapTuple childtup;
+
+ currcon = (Form_pg_constraint) GETSTRUCT(contuple);
+ conoid = currcon->oid;
+
+ ScanKeyInit(&pkey,
+ Anum_pg_constraint_conparentid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+
+ pscan = systable_beginscan(conrel, ConstraintParentIndexId,
+ true, NULL, 1, &pkey);
+
+ while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
+ ATExecAlterConstrEnforceability(wqueue, cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, childtup, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
+
+ systable_endscan(pscan);
+}
+
/*
* Invokes ATExecAlterConstrDeferrability for each constraint that is a child of
* the specified constraint.
@@ -12365,13 +12637,27 @@ UpdateConstraintEntry(ATAlterConstraint *cmdcon, Relation conrel,
HeapTuple copyTuple;
Form_pg_constraint copy_con;
- Assert(cmdcon->alterDeferrability || cmdcon->alterInheritability);
+ Assert(cmdcon->alterEnforceability || cmdcon->alterDeferrability ||
+ cmdcon->alterInheritability);
currcon = (Form_pg_constraint) GETSTRUCT(contuple);
copyTuple = heap_copytuple(contuple);
copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
+ if (cmdcon->alterEnforceability)
+ {
+ copy_con->conenforced = cmdcon->is_enforced;
+
+ /*
+ * 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.
+ */
+ copy_con->convalidated = cmdcon->is_enforced;
+ }
if (cmdcon->alterDeferrability)
{
copy_con->condeferrable = cmdcon->deferrable;
@@ -17092,9 +17378,9 @@ MergeConstraintsIntoExisting(Relation child_rel, Relation parent_rel)
NameStr(child_con->conname), RelationGetRelationName(child_rel))));
/*
- * A non-enforced child constraint cannot be merged with an
- * enforced parent constraint. However, the reverse is allowed,
- * where the child constraint is enforced.
+ * A NOT ENFORCED child constraint cannot be merged with an
+ * ENFORCED parent constraint. However, the reverse is allowed,
+ * where the child constraint is ENFORCED.
*/
if (parent_con->conenforced && !child_con->conenforced)
ereport(ERROR,
@@ -20465,8 +20751,6 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
Form_pg_constraint conform;
- Oid insertTriggerOid,
- updateTriggerOid;
contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
if (!HeapTupleIsValid(contup))
@@ -20493,17 +20777,25 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
/*
* Also, look up the partition's "check" triggers corresponding to the
- * constraint being detached and detach them from the parent triggers.
+ * ENFORCED constraint being detached and detach them from the parent
+ * triggers. NOT ENFORCED constraints do not have these triggers;
+ * therefore, this step is not needed.
*/
- GetForeignKeyCheckTriggers(trigrel,
- fk->conoid, fk->confrelid, fk->conrelid,
- &insertTriggerOid, &updateTriggerOid);
- Assert(OidIsValid(insertTriggerOid));
- TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
- RelationGetRelid(partRel));
- Assert(OidIsValid(updateTriggerOid));
- TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
- RelationGetRelid(partRel));
+ if (fk->conenforced)
+ {
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ Assert(OidIsValid(updateTriggerOid));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ }
/*
* Lastly, create the action triggers on the referenced table, using
@@ -20543,8 +20835,9 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
fkconstraint->conname = pstrdup(NameStr(conform->conname));
fkconstraint->deferrable = conform->condeferrable;
fkconstraint->initdeferred = conform->condeferred;
+ fkconstraint->is_enforced = conform->conenforced;
fkconstraint->skip_validation = true;
- fkconstraint->initially_valid = true;
+ fkconstraint->initially_valid = conform->convalidated;
/* a few irrelevant fields omitted here */
fkconstraint->pktable = NULL;
fkconstraint->fk_attrs = NIL;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 271ae26cbaf..1bef387aad4 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -2662,11 +2662,15 @@ alter_table_cmd:
n->subtype = AT_AlterConstraint;
n->def = (Node *) c;
c->conname = $3;
- c->alterDeferrability = true;
+ c->alterEnforceability =
+ ($4 & (CAS_NOT_ENFORCED | CAS_ENFORCED)) != 0;
+ c->alterDeferrability =
+ ($4 & (CAS_NOT_DEFERRABLE | CAS_DEFERRABLE |
+ CAS_INITIALLY_IMMEDIATE | CAS_INITIALLY_IMMEDIATE)) != 0;
processCASbits($4, @4, "FOREIGN KEY",
&c->deferrable,
&c->initdeferred,
- NULL, NULL, NULL, yyscanner);
+ &c->is_enforced, NULL, NULL, yyscanner);
$$ = (Node *) n;
}
/* ALTER TABLE <name> ALTER CONSTRAINT SET INHERIT */
@@ -4344,8 +4348,8 @@ ConstraintElem:
n->fk_del_set_cols = ($11)->deleteAction->cols;
processCASbits($12, @12, "FOREIGN KEY",
&n->deferrable, &n->initdeferred,
- NULL, &n->skip_validation, NULL,
- yyscanner);
+ &n->is_enforced, &n->skip_validation,
+ NULL, yyscanner);
n->initially_valid = !n->skip_validation;
$$ = (Node *) n;
}
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 896a7f2c59b..50f430a4e1a 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -2976,8 +2976,10 @@ transformFKConstraints(CreateStmtContext *cxt,
/*
* If CREATE TABLE or adding a column with NULL default, we can safely
- * skip validation of FK constraints, and nonetheless mark them valid.
- * (This will override any user-supplied NOT VALID flag.)
+ * skip validation of FK constraints, and mark them as valid based on the
+ * constraint enforcement flag, since NOT ENFORCED constraints must always
+ * be marked as NOT VALID. (This will override any user-supplied NOT VALID
+ * flag.)
*/
if (skipValidation)
{
@@ -2986,7 +2988,7 @@ transformFKConstraints(CreateStmtContext *cxt,
Constraint *constraint = (Constraint *) lfirst(fkclist);
constraint->skip_validation = true;
- constraint->initially_valid = true;
+ constraint->initially_valid = constraint->is_enforced;
}
}
@@ -3981,7 +3983,8 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
case CONSTR_ATTR_ENFORCED:
if (lastprimarycon == NULL ||
- lastprimarycon->contype != CONSTR_CHECK)
+ (lastprimarycon->contype != CONSTR_CHECK &&
+ lastprimarycon->contype != CONSTR_FOREIGN))
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("misplaced ENFORCED clause"),
@@ -3997,7 +4000,8 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
case CONSTR_ATTR_NOT_ENFORCED:
if (lastprimarycon == NULL ||
- lastprimarycon->contype != CONSTR_CHECK)
+ (lastprimarycon->contype != CONSTR_CHECK &&
+ lastprimarycon->contype != CONSTR_FOREIGN))
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("misplaced NOT ENFORCED clause"),
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index e6721056536..18a14ae186e 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4697,6 +4697,7 @@ RelationGetFKeyList(Relation relation)
info->conoid = constraint->oid;
info->conrelid = constraint->conrelid;
info->confrelid = constraint->confrelid;
+ info->conenforced = constraint->conenforced;
DeconstructFkConstraintRow(htup, &info->nkeys,
info->conkey,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 23c9e3c5abf..9e64e18b501 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2490,6 +2490,8 @@ typedef struct ATAlterConstraint
{
NodeTag type;
char *conname; /* Constraint name */
+ bool alterEnforceability; /* changing enforceability properties? */
+ bool is_enforced; /* ENFORCED? */
bool alterDeferrability; /* changing deferrability properties? */
bool deferrable; /* DEFERRABLE? */
bool initdeferred; /* INITIALLY DEFERRED? */
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index d94fddd7cef..b552359915f 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -284,6 +284,9 @@ typedef struct ForeignKeyCacheInfo
/* number of columns in the foreign key */
int nkeys;
+ /* Is enforced ? */
+ bool conenforced;
+
/*
* these arrays each have nkeys valid entries:
*/
diff --git a/src/test/regress/expected/constraints.out b/src/test/regress/expected/constraints.out
index 4f39100fcdf..a719d2f74e9 100644
--- a/src/test/regress/expected/constraints.out
+++ b/src/test/regress/expected/constraints.out
@@ -745,13 +745,9 @@ ERROR: misplaced NOT ENFORCED clause
LINE 1: CREATE TABLE UNIQUE_NOTEN_TBL(i int UNIQUE NOT ENFORCED);
^
ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
-ERROR: FOREIGN KEY constraints cannot be marked ENFORCED
-LINE 1: ...TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
- ^
+ERROR: cannot alter enforceability of constraint "unique_tbl_i_key" of relation "unique_tbl"
ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORCED;
-ERROR: FOREIGN KEY constraints cannot be marked NOT ENFORCED
-LINE 1: ...ABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORC...
- ^
+ERROR: cannot alter enforceability of constraint "unique_tbl_i_key" of relation "unique_tbl"
DROP TABLE unique_tbl;
--
-- EXCLUDE constraints
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 6a3374d5152..39744da7fd6 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -1,12 +1,43 @@
--
-- FOREIGN KEY
--
--- MATCH FULL
+-- NOT ENFORCED
--
-- First test, check and cascade
--
CREATE TABLE PKTABLE ( ptest1 int PRIMARY KEY, ptest2 text );
-CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE, ftest2 int );
+CREATE TABLE FKTABLE ( ftest1 int CONSTRAINT fktable_ftest1_fkey REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE NOT ENFORCED,
+ ftest2 int );
+-- Inserting into the foreign key table will not result in an error, even if
+-- there is no matching key in the referenced table.
+INSERT INTO FKTABLE VALUES (1, 2);
+INSERT INTO FKTABLE VALUES (2, 3);
+-- Check FKTABLE
+SELECT * FROM FKTABLE;
+ ftest1 | ftest2
+--------+--------
+ 1 | 2
+ 2 | 3
+(2 rows)
+
+-- Reverting it back to ENFORCED will result in failure because constraint validation will be triggered,
+-- as it was previously in a valid state.
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_fkey"
+DETAIL: Key (ftest1)=(1) is not present in table "pktable".
+-- Remove any existing rows that violate the constraint, then attempt to change
+-- it.
+TRUNCATE FKTABLE;
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+-- Any further inserts will fail due to the enforcement.
+INSERT INTO FKTABLE VALUES (3, 4);
+ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_fkey"
+DETAIL: Key (ftest1)=(3) is not present in table "pktable".
+--
+-- MATCH FULL
+--
+-- First test, check and cascade
+--
-- Insert test data into PKTABLE
INSERT INTO PKTABLE VALUES (1, 'Test1');
INSERT INTO PKTABLE VALUES (2, 'Test2');
@@ -351,6 +382,43 @@ INSERT INTO FKTABLE VALUES (1, NULL);
ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL;
ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_ftest2_fkey"
DETAIL: MATCH FULL does not allow mixing of null and nonnull key values.
+-- Modifying other attributes of a constraint should not affect its enforceability, and vice versa
+ALTER TABLE FKTABLE ADD CONSTRAINT fk_con FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE NOT VALID NOT ENFORCED;
+ALTER TABLE FKTABLE ALTER CONSTRAINT fk_con DEFERRABLE INITIALLY DEFERRED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fk_con';
+ condeferrable | condeferred | conenforced | convalidated
+---------------+-------------+-------------+--------------
+ t | t | f | f
+(1 row)
+
+ALTER TABLE FKTABLE ALTER CONSTRAINT fk_con NOT ENFORCED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fk_con';
+ condeferrable | condeferred | conenforced | convalidated
+---------------+-------------+-------------+--------------
+ t | t | f | f
+(1 row)
+
+-- Enforceability also changes the validate state, as data validation will be
+-- performed during this transformation.
+ALTER TABLE FKTABLE ALTER CONSTRAINT fk_con ENFORCED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fk_con';
+ condeferrable | condeferred | conenforced | convalidated
+---------------+-------------+-------------+--------------
+ t | t | t | t
+(1 row)
+
+-- Can change enforceability and deferrability together
+ALTER TABLE FKTABLE ALTER CONSTRAINT fk_con NOT ENFORCED NOT DEFERRABLE;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fk_con';
+ condeferrable | condeferred | conenforced | convalidated
+---------------+-------------+-------------+--------------
+ f | f | f | f
+(1 row)
+
DROP TABLE FKTABLE;
DROP TABLE PKTABLE;
-- MATCH SIMPLE
@@ -1276,6 +1344,13 @@ INSERT INTO fktable VALUES (0, 20);
ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_fk_fkey"
DETAIL: Key (fk)=(20) is not present in table "pktable".
COMMIT;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT ENFORCED;
+BEGIN;
+-- doesn't match FK, no error.
+UPDATE pktable SET id = 10 WHERE id = 5;
+-- doesn't match PK, no error.
+INSERT INTO fktable VALUES (0, 20);
+ROLLBACK;
-- try additional syntax
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE;
-- illegal options
@@ -1291,6 +1366,14 @@ ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT VALID;
ERROR: FOREIGN KEY constraints cannot be marked NOT VALID
LINE 1: ...ER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT VALID;
^
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORCED;
+ERROR: conflicting constraint properties
+LINE 1: ...fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORC...
+ ^
+CREATE TEMP TABLE fktable2 (fk int references pktable ENFORCED NOT ENFORCED);
+ERROR: multiple ENFORCED/NOT ENFORCED clauses not allowed
+LINE 1: ...ABLE fktable2 (fk int references pktable ENFORCED NOT ENFORC...
+ ^
-- test order of firing of FK triggers when several RI-induced changes need to
-- be made to the same row. This was broken by subtransaction-related
-- changes in 8.0.
@@ -1588,10 +1671,13 @@ ALTER TABLE fk_partitioned_fk DROP COLUMN fdrop1;
CREATE TABLE fk_partitioned_fk_1 (fdrop1 int, fdrop2 int, a int, fdrop3 int, b int);
ALTER TABLE fk_partitioned_fk_1 DROP COLUMN fdrop1, DROP COLUMN fdrop2, DROP COLUMN fdrop3;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FROM (0,0) TO (1000,1000);
-ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk;
+ALTER TABLE fk_partitioned_fk ADD CONSTRAINT fk_partitioned_fk_a_b_fkey FOREIGN KEY (a, b)
+ REFERENCES fk_notpartitioned_pk NOT ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
+ALTER TABLE fk_partitioned_fk_2 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
CREATE TABLE fk_partitioned_fk_3 (fdrop1 int, fdrop2 int, fdrop3 int, fdrop4 int, b int, a int)
PARTITION BY HASH (a);
ALTER TABLE fk_partitioned_fk_3 DROP COLUMN fdrop1, DROP COLUMN fdrop2,
@@ -1614,10 +1700,10 @@ INSERT INTO fk_partitioned_fk_1 (a,b) VALUES (500, 501);
ERROR: insert or update on table "fk_partitioned_fk_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
DETAIL: Key (a, b)=(500, 501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (1500, 1501);
-ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_2_a_b_fkey"
DETAIL: Key (a, b)=(1500, 1501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk_2 (a,b) VALUES (1500, 1501);
-ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_2_a_b_fkey"
DETAIL: Key (a, b)=(1500, 1501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (2500, 2502);
ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
@@ -1667,6 +1753,37 @@ Indexes:
Referenced by:
TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b)
+-- Check the exsting FK trigger
+CREATE TEMP TABLE tmp_trg_info AS SELECT tgtype, tgrelid::regclass, tgconstraint
+FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+SELECT count(1) FROM tmp_trg_info;
+ count
+-------
+ 14
+(1 row)
+
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+-- No triggers
+SELECT count(1) FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+ count
+-------
+ 0
+(1 row)
+
+-- Changing it back to ENFORCED will recreate the necessary triggers.
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+-- Should be exactly the same number of triggers found as before
+SELECT COUNT(1) FROM pg_trigger pgt JOIN tmp_trg_info tt
+ON (pgt.tgtype = tt.tgtype AND pgt.tgrelid = tt.tgrelid AND pgt.tgconstraint = tt.tgconstraint);
+ count
+-------
+ 14
+(1 row)
+
ALTER TABLE fk_partitioned_fk DROP CONSTRAINT fk_partitioned_fk_a_b_fkey;
-- done.
DROP TABLE fk_notpartitioned_pk, fk_partitioned_fk;
@@ -1964,6 +2081,40 @@ Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
Foreign-key constraints:
TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
+DROP TABLE fk_partitioned_fk_2;
+CREATE TABLE fk_partitioned_fk_2 (b int, a int,
+ FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
+BEGIN;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should be having two constraints
+\d fk_partitioned_fk_2
+ Table "public.fk_partitioned_fk_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ b | integer | | |
+ a | integer | | |
+Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
+Foreign-key constraints:
+ "fk_partitioned_fk_2_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED
+ TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
+
+DROP TABLE fk_partitioned_fk_2;
+ROLLBACK;
+BEGIN;
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have only one constraint
+\d fk_partitioned_fk_2
+ Table "public.fk_partitioned_fk_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ b | integer | | |
+ a | integer | | |
+Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
+Foreign-key constraints:
+ TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED
+
+ROLLBACK;
DROP TABLE fk_partitioned_fk_2;
CREATE TABLE fk_partitioned_fk_4 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE) PARTITION BY RANGE (b, a);
CREATE TABLE fk_partitioned_fk_4_1 PARTITION OF fk_partitioned_fk_4 FOR VALUES FROM (1,1) TO (100,100);
diff --git a/src/test/regress/expected/inherit.out b/src/test/regress/expected/inherit.out
index e671975a281..910668ecc4a 100644
--- a/src/test/regress/expected/inherit.out
+++ b/src/test/regress/expected/inherit.out
@@ -1332,6 +1332,13 @@ NOTICE: merging constraint "inh_check_constraint5" with inherited definition
alter table p1 add constraint inh_check_constraint6 check (f1 < 10) not enforced;
alter table p1_c1 add constraint inh_check_constraint6 check (f1 < 10) enforced;
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 invalid state of the child constraint will be ignored here.
+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
create table p1_c2(f1 int constraint inh_check_constraint4 check (f1 < 10)) inherits(p1);
NOTICE: merging column "f1" with inherited definition
NOTICE: merging constraint "inh_check_constraint4" with inherited definition
@@ -1353,42 +1360,50 @@ create table p1_fail(f1 int constraint inh_check_constraint6 check (f1 < 10) not
NOTICE: merging multiple inherited definitions of column "f1"
NOTICE: merging column "f1" with inherited definition
ERROR: constraint "inh_check_constraint6" conflicts with NOT ENFORCED constraint on relation "p1_fail"
-select conrelid::regclass::text as relname, conname, conislocal, coninhcount, conenforced
+select conrelid::regclass::text as relname, conname, conislocal, coninhcount, conenforced, convalidated
from pg_constraint where conname like 'inh\_check\_constraint%'
order by 1, 2;
- relname | conname | conislocal | coninhcount | conenforced
----------+-----------------------+------------+-------------+-------------
- p1 | inh_check_constraint1 | t | 0 | t
- p1 | inh_check_constraint2 | t | 0 | t
- p1 | inh_check_constraint3 | t | 0 | f
- p1 | inh_check_constraint4 | t | 0 | f
- p1 | inh_check_constraint5 | t | 0 | f
- p1 | inh_check_constraint6 | t | 0 | f
- p1 | inh_check_constraint8 | t | 0 | t
- p1_c1 | inh_check_constraint1 | t | 1 | t
- p1_c1 | inh_check_constraint2 | t | 1 | t
- p1_c1 | inh_check_constraint3 | t | 1 | f
- p1_c1 | inh_check_constraint4 | t | 1 | f
- p1_c1 | inh_check_constraint5 | t | 1 | t
- p1_c1 | inh_check_constraint6 | t | 1 | t
- p1_c1 | inh_check_constraint7 | t | 0 | f
- p1_c1 | inh_check_constraint8 | f | 1 | t
- p1_c2 | inh_check_constraint1 | f | 1 | t
- p1_c2 | inh_check_constraint2 | f | 1 | t
- p1_c2 | inh_check_constraint3 | f | 1 | f
- p1_c2 | inh_check_constraint4 | t | 1 | t
- p1_c2 | inh_check_constraint5 | f | 1 | f
- p1_c2 | inh_check_constraint6 | f | 1 | f
- p1_c2 | inh_check_constraint8 | f | 1 | t
- p1_c3 | inh_check_constraint1 | f | 2 | t
- p1_c3 | inh_check_constraint2 | f | 2 | t
- p1_c3 | inh_check_constraint3 | f | 2 | f
- p1_c3 | inh_check_constraint4 | f | 2 | f
- p1_c3 | inh_check_constraint5 | f | 2 | t
- p1_c3 | inh_check_constraint6 | f | 2 | t
- p1_c3 | inh_check_constraint7 | f | 1 | f
- p1_c3 | inh_check_constraint8 | f | 2 | t
-(30 rows)
+ relname | conname | conislocal | coninhcount | conenforced | convalidated
+---------+------------------------+------------+-------------+-------------+--------------
+ p1 | inh_check_constraint1 | t | 0 | t | t
+ p1 | inh_check_constraint10 | t | 0 | f | f
+ p1 | inh_check_constraint2 | t | 0 | t | t
+ p1 | inh_check_constraint3 | t | 0 | f | f
+ p1 | inh_check_constraint4 | t | 0 | f | f
+ p1 | inh_check_constraint5 | t | 0 | f | f
+ p1 | inh_check_constraint6 | t | 0 | f | f
+ 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_constraint2 | t | 1 | t | t
+ p1_c1 | inh_check_constraint3 | t | 1 | f | f
+ p1_c1 | inh_check_constraint4 | t | 1 | f | f
+ p1_c1 | inh_check_constraint5 | t | 1 | t | t
+ p1_c1 | inh_check_constraint6 | t | 1 | t | t
+ p1_c1 | inh_check_constraint7 | t | 0 | f | f
+ p1_c1 | inh_check_constraint8 | f | 1 | t | t
+ p1_c1 | inh_check_constraint9 | t | 1 | t | f
+ p1_c2 | inh_check_constraint1 | f | 1 | t | t
+ p1_c2 | inh_check_constraint10 | f | 1 | f | f
+ p1_c2 | inh_check_constraint2 | f | 1 | t | t
+ p1_c2 | inh_check_constraint3 | f | 1 | f | f
+ p1_c2 | inh_check_constraint4 | t | 1 | t | t
+ p1_c2 | inh_check_constraint5 | f | 1 | f | f
+ p1_c2 | inh_check_constraint6 | f | 1 | f | f
+ p1_c2 | inh_check_constraint8 | f | 1 | t | t
+ p1_c2 | inh_check_constraint9 | f | 1 | f | f
+ p1_c3 | inh_check_constraint1 | f | 2 | t | t
+ p1_c3 | inh_check_constraint10 | f | 2 | t | t
+ p1_c3 | inh_check_constraint2 | f | 2 | t | t
+ p1_c3 | inh_check_constraint3 | f | 2 | f | f
+ p1_c3 | inh_check_constraint4 | f | 2 | f | f
+ p1_c3 | inh_check_constraint5 | f | 2 | t | t
+ p1_c3 | inh_check_constraint6 | f | 2 | t | t
+ p1_c3 | inh_check_constraint7 | f | 1 | f | f
+ p1_c3 | inh_check_constraint8 | f | 2 | t | t
+ p1_c3 | inh_check_constraint9 | f | 2 | t | t
+(38 rows)
drop table p1 cascade;
NOTICE: drop cascades to 3 other objects
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index 44945b0453a..7e5be0702da 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -2,13 +2,39 @@
-- FOREIGN KEY
--
--- MATCH FULL
+-- NOT ENFORCED
--
-- First test, check and cascade
--
CREATE TABLE PKTABLE ( ptest1 int PRIMARY KEY, ptest2 text );
-CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE, ftest2 int );
+CREATE TABLE FKTABLE ( ftest1 int CONSTRAINT fktable_ftest1_fkey REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE NOT ENFORCED,
+ ftest2 int );
+-- Inserting into the foreign key table will not result in an error, even if
+-- there is no matching key in the referenced table.
+INSERT INTO FKTABLE VALUES (1, 2);
+INSERT INTO FKTABLE VALUES (2, 3);
+
+-- Check FKTABLE
+SELECT * FROM FKTABLE;
+
+-- Reverting it back to ENFORCED will result in failure because constraint validation will be triggered,
+-- as it was previously in a valid state.
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+
+-- Remove any existing rows that violate the constraint, then attempt to change
+-- it.
+TRUNCATE FKTABLE;
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+
+-- Any further inserts will fail due to the enforcement.
+INSERT INTO FKTABLE VALUES (3, 4);
+
+--
+-- MATCH FULL
+--
+-- First test, check and cascade
+--
-- Insert test data into PKTABLE
INSERT INTO PKTABLE VALUES (1, 'Test1');
INSERT INTO PKTABLE VALUES (2, 'Test2');
@@ -230,6 +256,27 @@ INSERT INTO FKTABLE VALUES (1, NULL);
ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL;
+-- Modifying other attributes of a constraint should not affect its enforceability, and vice versa
+ALTER TABLE FKTABLE ADD CONSTRAINT fk_con FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE NOT VALID NOT ENFORCED;
+ALTER TABLE FKTABLE ALTER CONSTRAINT fk_con DEFERRABLE INITIALLY DEFERRED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fk_con';
+
+ALTER TABLE FKTABLE ALTER CONSTRAINT fk_con NOT ENFORCED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fk_con';
+
+-- Enforceability also changes the validate state, as data validation will be
+-- performed during this transformation.
+ALTER TABLE FKTABLE ALTER CONSTRAINT fk_con ENFORCED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fk_con';
+
+-- Can change enforceability and deferrability together
+ALTER TABLE FKTABLE ALTER CONSTRAINT fk_con NOT ENFORCED NOT DEFERRABLE;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fk_con';
+
DROP TABLE FKTABLE;
DROP TABLE PKTABLE;
@@ -968,12 +1015,25 @@ INSERT INTO fktable VALUES (0, 20);
COMMIT;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT ENFORCED;
+
+BEGIN;
+
+-- doesn't match FK, no error.
+UPDATE pktable SET id = 10 WHERE id = 5;
+-- doesn't match PK, no error.
+INSERT INTO fktable VALUES (0, 20);
+
+ROLLBACK;
+
-- try additional syntax
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE;
-- illegal options
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE INITIALLY DEFERRED;
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NO INHERIT;
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT VALID;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORCED;
+CREATE TEMP TABLE fktable2 (fk int references pktable ENFORCED NOT ENFORCED);
-- test order of firing of FK triggers when several RI-induced changes need to
-- be made to the same row. This was broken by subtransaction-related
@@ -1184,11 +1244,13 @@ ALTER TABLE fk_partitioned_fk DROP COLUMN fdrop1;
CREATE TABLE fk_partitioned_fk_1 (fdrop1 int, fdrop2 int, a int, fdrop3 int, b int);
ALTER TABLE fk_partitioned_fk_1 DROP COLUMN fdrop1, DROP COLUMN fdrop2, DROP COLUMN fdrop3;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FROM (0,0) TO (1000,1000);
-ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk;
+ALTER TABLE fk_partitioned_fk ADD CONSTRAINT fk_partitioned_fk_a_b_fkey FOREIGN KEY (a, b)
+ REFERENCES fk_notpartitioned_pk NOT ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
+ALTER TABLE fk_partitioned_fk_2 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
-
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
CREATE TABLE fk_partitioned_fk_3 (fdrop1 int, fdrop2 int, fdrop3 int, fdrop4 int, b int, a int)
PARTITION BY HASH (a);
ALTER TABLE fk_partitioned_fk_3 DROP COLUMN fdrop1, DROP COLUMN fdrop2,
@@ -1234,6 +1296,27 @@ UPDATE fk_notpartitioned_pk SET b = 1502 WHERE a = 1500;
UPDATE fk_notpartitioned_pk SET b = 2504 WHERE a = 2500;
-- check psql behavior
\d fk_notpartitioned_pk
+
+-- Check the exsting FK trigger
+CREATE TEMP TABLE tmp_trg_info AS SELECT tgtype, tgrelid::regclass, tgconstraint
+FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+SELECT count(1) FROM tmp_trg_info;
+
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+-- No triggers
+SELECT count(1) FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+
+-- Changing it back to ENFORCED will recreate the necessary triggers.
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+
+-- Should be exactly the same number of triggers found as before
+SELECT COUNT(1) FROM pg_trigger pgt JOIN tmp_trg_info tt
+ON (pgt.tgtype = tt.tgtype AND pgt.tgrelid = tt.tgrelid AND pgt.tgconstraint = tt.tgconstraint);
+
ALTER TABLE fk_partitioned_fk DROP CONSTRAINT fk_partitioned_fk_a_b_fkey;
-- done.
DROP TABLE fk_notpartitioned_pk, fk_partitioned_fk;
@@ -1441,6 +1524,22 @@ ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN
\d fk_partitioned_fk_2
DROP TABLE fk_partitioned_fk_2;
+CREATE TABLE fk_partitioned_fk_2 (b int, a int,
+ FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
+BEGIN;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should be having two constraints
+\d fk_partitioned_fk_2
+DROP TABLE fk_partitioned_fk_2;
+ROLLBACK;
+BEGIN;
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have only one constraint
+\d fk_partitioned_fk_2
+ROLLBACK;
+DROP TABLE fk_partitioned_fk_2;
+
CREATE TABLE fk_partitioned_fk_4 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE) PARTITION BY RANGE (b, a);
CREATE TABLE fk_partitioned_fk_4_1 PARTITION OF fk_partitioned_fk_4 FOR VALUES FROM (1,1) TO (100,100);
CREATE TABLE fk_partitioned_fk_4_2 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE SET NULL);
diff --git a/src/test/regress/sql/inherit.sql b/src/test/regress/sql/inherit.sql
index 4e73c70495c..bc548199d1e 100644
--- a/src/test/regress/sql/inherit.sql
+++ b/src/test/regress/sql/inherit.sql
@@ -481,6 +481,13 @@ alter table p1 add constraint inh_check_constraint5 check (f1 < 10) not enforced
alter table p1 add constraint inh_check_constraint6 check (f1 < 10) not enforced;
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 invalid state of the child constraint will be ignored here.
+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;
+
create table p1_c2(f1 int constraint inh_check_constraint4 check (f1 < 10)) inherits(p1);
-- but reverse is not allowed
@@ -498,7 +505,7 @@ create table p1_c3() inherits(p1, p1_c1);
-- but not allowed if the child constraint is explicitly asked to be NOT ENFORCED
create table p1_fail(f1 int constraint inh_check_constraint6 check (f1 < 10) not enforced) inherits(p1, p1_c1);
-select conrelid::regclass::text as relname, conname, conislocal, coninhcount, conenforced
+select conrelid::regclass::text as relname, conname, conislocal, coninhcount, conenforced, convalidated
from pg_constraint where conname like 'inh\_check\_constraint%'
order by 1, 2;
--
2.43.5
v18-0006-Merge-the-parent-and-child-constraints-with-diff.patchapplication/octet-stream; name=v18-0006-Merge-the-parent-and-child-constraints-with-diff.patchDownload
From cf394da75b2992f50aff552e3e6ff4b93bc19cf6 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 10 Feb 2025 10:59:28 +0530
Subject: [PATCH v18 6/6] Merge the parent and child constraints with differing
enforcibility.
If an ENFORCED parent constraint is attached to a NOT ENFORCED child
constraint, the child constraint will be made ENFORCED, with
validation applied if the parent constraint is validated as well.
Otherwise, a new ENFORCED constraint (with validation, if the parent
constraint is validated) would need to be created on the child table,
which would be unnecessary if a similar constraint already exists and
can be attached.
On the other hand, having a NOT ENFORCED parent constraint with an
ENFORCED child constraint does not cause any issues, and no changes
are required.
----
NOTE: This patch is intended to reduce the diff noise from the main
patch and is not meant to be committed separately. It should be
squashed with the main patch that adds ENFORCED/NOT ENFORCED.
----
---
src/backend/commands/tablecmds.c | 164 ++++++++++++++++++++--
src/test/regress/expected/foreign_key.out | 77 ++++++++--
src/test/regress/sql/foreign_key.sql | 37 ++++-
3 files changed, 253 insertions(+), 25 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 2f53acc8b7d..4bb94bd1782 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -11545,7 +11545,6 @@ tryAttachPartitionForeignKey(List **wqueue,
if (OidIsValid(partConstr->conparentid) ||
partConstr->condeferrable != parentConstr->condeferrable ||
partConstr->condeferred != parentConstr->condeferred ||
- partConstr->conenforced != parentConstr->conenforced ||
partConstr->confupdtype != parentConstr->confupdtype ||
partConstr->confdeltype != parentConstr->confdeltype ||
partConstr->confmatchtype != parentConstr->confmatchtype)
@@ -11590,6 +11589,8 @@ AttachPartitionForeignKey(List **wqueue,
Oid partConstrFrelid;
Oid partConstrRelid;
bool parentConstrIsEnforced;
+ bool partConstrIsEnforced;
+ bool partConstrParentIsSet;
/* Fetch the parent constraint tuple */
parentConstrTup = SearchSysCache1(CONSTROID,
@@ -11605,13 +11606,47 @@ AttachPartitionForeignKey(List **wqueue,
if (!HeapTupleIsValid(partcontup))
elog(ERROR, "cache lookup failed for constraint %u", partConstrOid);
partConstr = (Form_pg_constraint) GETSTRUCT(partcontup);
+ partConstrIsEnforced = partConstr->conenforced;
partConstrFrelid = partConstr->confrelid;
partConstrRelid = partConstr->conrelid;
+ /*
+ * The case where the parent constraint is NOT ENFORCED and the child
+ * constraint is ENFORCED is acceptable because the not enforced parent
+ * constraint lacks triggers, eliminating any redundancy issues with the
+ * enforced child constraint. In this scenario, the child constraint
+ * remains enforced, and its trigger is retained, ensuring that
+ * referential integrity checks for the child continue as before, even
+ * with the parent constraint not enforced. The relationship between the
+ * two constraints is preserved by setting the parent constraint, which
+ * allows us to locate the child constraint. This becomes important if the
+ * parent constraint is later changed to enforced, at which point the
+ * necessary trigger will be created for the parent, and any redundancy
+ * from these triggers will be appropriately handled.
+ */
+ if (!parentConstrIsEnforced && partConstrIsEnforced)
+ {
+ ReleaseSysCache(partcontup);
+ ReleaseSysCache(parentConstrTup);
+
+ ConstraintSetParentConstraint(partConstrOid, parentConstrOid,
+ RelationGetRelid(partition));
+ CommandCounterIncrement();
+
+ return;
+ }
+
/*
* If the referenced table is partitioned, then the partition we're
* attaching now has extra pg_constraint rows and action triggers that are
* no longer needed. Remove those.
+ *
+ * Note that this must be done beforehand, particularly in situations
+ * where we might decide to change the constraint to an ENFORCED state
+ * which will create the required triggers and add the child constraint to
+ * the validation queue. To avoid generating unnecessary triggers and
+ * adding them to the validation queue, it is crucial to eliminate any
+ * redundant constraints beforehand.
*/
if (get_rel_relkind(partConstrFrelid) == RELKIND_PARTITIONED_TABLE)
{
@@ -11630,6 +11665,53 @@ AttachPartitionForeignKey(List **wqueue,
*/
queueValidation = parentConstr->convalidated && !partConstr->convalidated;
+ /*
+ * The case where the parent constraint is ENFORCED and the child
+ * constraint is NOT ENFORCED is not acceptable, as it would violate
+ * referential integrity. In such cases, the child constraint will first
+ * be enforced before merging it with the enforced parent constraint.
+ * Subsequently, removing action triggers, setting up constraint triggers,
+ * and handling check triggers for the parent will be managed in the usual
+ * manner, similar to how two enforced constraints are merged.
+ */
+ if (parentConstrIsEnforced && !partConstrIsEnforced)
+ {
+ ATAlterConstraint *cmdcon = makeNode(ATAlterConstraint);
+ Relation conrel;
+
+ cmdcon->conname = NameStr(partConstr->conname);
+ cmdcon->deferrable = partConstr->condeferrable;
+ cmdcon->initdeferred = partConstr->condeferred;
+ cmdcon->alterEnforceability = true;
+ cmdcon->is_enforced = true;
+
+ conrel = table_open(ConstraintRelationId, RowExclusiveLock);
+
+ ATExecAlterConstrEnforceability(wqueue, cmdcon, conrel, trigrel,
+ partConstr->conrelid,
+ partConstr->confrelid,
+ partcontup, AccessExclusiveLock,
+ InvalidOid, InvalidOid, InvalidOid,
+ InvalidOid);
+
+ table_close(conrel, RowExclusiveLock);
+
+ CommandCounterIncrement();
+
+ /*
+ * No further validation is needed, as changing the constraint to
+ * enforced will implicitly trigger the same validation.
+ */
+ queueValidation = false;
+ }
+
+ /*
+ * The constraint parent shouldn't be set beforehand, or if it's already
+ * set, it should be the specified parent.
+ */
+ partConstrParentIsSet = OidIsValid(partConstr->conparentid);
+ Assert(!partConstrParentIsSet || partConstr->conparentid == parentConstrOid);
+
ReleaseSysCache(partcontup);
ReleaseSysCache(parentConstrTup);
@@ -11642,8 +11724,10 @@ AttachPartitionForeignKey(List **wqueue,
DropForeignKeyConstraintTriggers(trigrel, partConstrOid, partConstrFrelid,
partConstrRelid);
- ConstraintSetParentConstraint(partConstrOid, parentConstrOid,
- RelationGetRelid(partition));
+ /* Skip if the parent is already set */
+ if (!partConstrParentIsSet)
+ ConstraintSetParentConstraint(partConstrOid, parentConstrOid,
+ RelationGetRelid(partition));
/*
* Like the constraint, attach partition's "check" triggers to the
@@ -12242,6 +12326,17 @@ ATExecAlterConstrEnforceability(List **wqueue, ATAlterConstraint *cmdcon,
/* Drop all the triggers */
DropForeignKeyConstraintTriggers(tgrel, conoid, InvalidOid, InvalidOid);
+
+ /*
+ * If the referenced table is partitioned, the child constraint we're
+ * changing to NOT ENFORCED may have additional pg_constraint rows and
+ * action triggers that remain untouched while this child constraint
+ * is attached to the NOT ENFORCED parent. These must now be removed.
+ * For more details, see AttachPartitionForeignKey().
+ */
+ if (OidIsValid(currcon->conparentid) &&
+ get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE)
+ RemoveInheritedConstraint(conrel, tgrel, currcon->conrelid, conoid);
}
else if (changed) /* Create triggers */
{
@@ -12567,13 +12662,40 @@ AlterConstrEnforceabilityRecurse(List **wqueue, ATAlterConstraint *cmdcon,
true, NULL, 1, &pkey);
while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
- ATExecAlterConstrEnforceability(wqueue, cmdcon, conrel, tgrel, fkrelid,
- pkrelid, childtup, lockmode,
- ReferencedParentDelTrigger,
- ReferencedParentUpdTrigger,
- ReferencingParentInsTrigger,
- ReferencingParentUpdTrigger);
+ {
+ Form_pg_constraint childcon;
+ childcon = (Form_pg_constraint) GETSTRUCT(childtup);
+
+ /*
+ * When the parent constraint is modified to be ENFORCED, and the
+ * child constraint is attached to the parent constraint (which is
+ * already ENFORCED), some constraints and action triggers on the
+ * child table may become redundant and need to be removed.
+ */
+ if (cmdcon->is_enforced && childcon->conenforced)
+ {
+ if (currcon->confrelid == pkrelid)
+ {
+ Relation rel = table_open(childcon->conrelid, lockmode);
+
+ AttachPartitionForeignKey(wqueue, rel, childcon->oid,
+ conoid,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger,
+ tgrel);
+
+ table_close(rel, NoLock);
+ }
+ }
+ else
+ ATExecAlterConstrEnforceability(wqueue, cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, childtup, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
+ }
systable_endscan(pscan);
}
@@ -20750,7 +20872,9 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
{
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
+ HeapTuple parentContup;
Form_pg_constraint conform;
+ Oid parentConstrIsEnforced;
contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
if (!HeapTupleIsValid(contup))
@@ -20769,12 +20893,34 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
continue;
}
+ /* Get the enforcibility of the parent constraint */
+ parentContup = SearchSysCache1(CONSTROID,
+ ObjectIdGetDatum(conform->conparentid));
+ if (!HeapTupleIsValid(parentContup))
+ elog(ERROR, "cache lookup failed for constraint %u",
+ conform->conparentid);
+ parentConstrIsEnforced =
+ ((Form_pg_constraint) GETSTRUCT(parentContup))->conenforced;
+ ReleaseSysCache(parentContup);
+
/*
* The constraint on this table must be marked no longer a child of
* the parent's constraint, as do its check triggers.
*/
ConstraintSetParentConstraint(fk->conoid, InvalidOid, InvalidOid);
+ /*
+ * Unsetting the parent is sufficient when the parent constraint is
+ * NOT ENFORCED and the child constraint is ENFORCED, as we link them
+ * by setting the constraint parent, while leaving the rest unchanged.
+ * For more details, see AttachPartitionForeignKey().
+ */
+ if (!parentConstrIsEnforced && fk->conenforced)
+ {
+ ReleaseSysCache(contup);
+ continue;
+ }
+
/*
* Also, look up the partition's "check" triggers corresponding to the
* ENFORCED constraint being detached and detach them from the parent
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 39744da7fd6..0d6d7838c79 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -1675,7 +1675,6 @@ ALTER TABLE fk_partitioned_fk ADD CONSTRAINT fk_partitioned_fk_a_b_fkey FOREIGN
REFERENCES fk_notpartitioned_pk NOT ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
-ALTER TABLE fk_partitioned_fk_2 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
CREATE TABLE fk_partitioned_fk_3 (fdrop1 int, fdrop2 int, fdrop3 int, fdrop4 int, b int, a int)
@@ -1684,8 +1683,65 @@ ALTER TABLE fk_partitioned_fk_3 DROP COLUMN fdrop1, DROP COLUMN fdrop2,
DROP COLUMN fdrop3, DROP COLUMN fdrop4;
CREATE TABLE fk_partitioned_fk_3_0 PARTITION OF fk_partitioned_fk_3 FOR VALUES WITH (MODULUS 5, REMAINDER 0);
CREATE TABLE fk_partitioned_fk_3_1 PARTITION OF fk_partitioned_fk_3 FOR VALUES WITH (MODULUS 5, REMAINDER 1);
+-- Merge the not-enforced parent constraint with the enforced and not-enforced child constraints
+ALTER TABLE fk_partitioned_fk_3_0 ADD CONSTRAINT fk_partitioned_fk_3_0_a_b_fkey FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ENFORCED;
+ALTER TABLE fk_partitioned_fk_3_1 ADD CONSTRAINT fk_partitioned_fk_3_1_a_b_fkey FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk_3 ADD CONSTRAINT fk_partitioned_fk_3_a_b_fkey FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+-- Merge the non-enforced parent constraint with both the enforced and
+-- non-enforced child constraints, where the referenced table is partitioned.
+CREATE TABLE fk_partitioned_pk (a int, b int, PRIMARY KEY (a, b)) PARTITION BY RANGE (a, b);
+CREATE TABLE fk_partitioned_pk_1 PARTITION OF fk_partitioned_pk FOR VALUES FROM (0,0) TO (1000,1000);
+-- Merge the enforced parent constraint with the enforced and not-enforced child constraints.
+ALTER TABLE fk_partitioned_fk_3_0 ADD CONSTRAINT fk_partitioned_fk_3_0_a_b_fkey1 FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk ENFORCED;
+ALTER TABLE fk_partitioned_fk_3_1 ADD CONSTRAINT fk_partitioned_fk_3_1_a_b_fkey1 FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk_3 ADD CONSTRAINT fk_partitioned_fk_3_a_b_fkey1 FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk ENFORCED;
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+ conname | conenforced | convalidated | conrelid | confrelid
+---------------------------------+-------------+--------------+-----------------------+----------------------
+ fk_partitioned_fk_3_0_a_b_fkey | t | t | fk_partitioned_fk_3_0 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey | f | f | fk_partitioned_fk_3_1 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_a_b_fkey | f | f | fk_partitioned_fk_3 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_0_a_b_fkey1 | t | t | fk_partitioned_fk_3_0 | fk_partitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey1 | t | t | fk_partitioned_fk_3_1 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey1 | t | t | fk_partitioned_fk_3 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey2 | t | t | fk_partitioned_fk_3 | fk_partitioned_pk_1
+(7 rows)
+
+ALTER TABLE fk_partitioned_fk_3 DETACH PARTITION fk_partitioned_fk_3_0;
+ALTER TABLE fk_partitioned_fk_3 ATTACH PARTITION fk_partitioned_fk_3_0 FOR VALUES WITH (MODULUS 5, REMAINDER 0);
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+ conname | conenforced | convalidated | conrelid | confrelid
+---------------------------------+-------------+--------------+-----------------------+----------------------
+ fk_partitioned_fk_3_0_a_b_fkey | t | t | fk_partitioned_fk_3_0 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey | f | f | fk_partitioned_fk_3_1 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_a_b_fkey | f | f | fk_partitioned_fk_3 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_0_a_b_fkey1 | t | t | fk_partitioned_fk_3_0 | fk_partitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey1 | t | t | fk_partitioned_fk_3_1 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey1 | t | t | fk_partitioned_fk_3 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey2 | t | t | fk_partitioned_fk_3 | fk_partitioned_pk_1
+(7 rows)
+
+ALTER TABLE fk_partitioned_fk_3 ALTER CONSTRAINT fk_partitioned_fk_3_a_b_fkey1 NOT ENFORCED;
+-- Merging an enforced parent constraint (validated) with a not-enforced child
+-- constraint will implicitly change the child constraint to enforced and apply
+-- the validation as well.
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_3
FOR VALUES FROM (2000,2000) TO (3000,3000);
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+ conname | conenforced | convalidated | conrelid | confrelid
+---------------------------------+-------------+--------------+-----------------------+----------------------
+ fk_partitioned_fk_3_0_a_b_fkey | t | t | fk_partitioned_fk_3_0 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey | t | t | fk_partitioned_fk_3_1 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_a_b_fkey | t | t | fk_partitioned_fk_3 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_0_a_b_fkey1 | f | f | fk_partitioned_fk_3_0 | fk_partitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey1 | f | f | fk_partitioned_fk_3_1 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey1 | f | f | fk_partitioned_fk_3 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey2 | f | f | fk_partitioned_fk_3 | fk_partitioned_pk_1
+(7 rows)
+
-- Creating a foreign key with ONLY on a partitioned table referencing
-- a non-partitioned table fails.
ALTER TABLE ONLY fk_partitioned_fk ADD FOREIGN KEY (a, b)
@@ -1700,22 +1756,22 @@ INSERT INTO fk_partitioned_fk_1 (a,b) VALUES (500, 501);
ERROR: insert or update on table "fk_partitioned_fk_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
DETAIL: Key (a, b)=(500, 501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (1500, 1501);
-ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_2_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
DETAIL: Key (a, b)=(1500, 1501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk_2 (a,b) VALUES (1500, 1501);
-ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_2_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
DETAIL: Key (a, b)=(1500, 1501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (2500, 2502);
-ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_3_1_a_b_fkey"
DETAIL: Key (a, b)=(2500, 2502) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk_3 (a,b) VALUES (2500, 2502);
-ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_3_1_a_b_fkey"
DETAIL: Key (a, b)=(2500, 2502) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (2501, 2503);
-ERROR: insert or update on table "fk_partitioned_fk_3_0" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_0" violates foreign key constraint "fk_partitioned_fk_3_0_a_b_fkey"
DETAIL: Key (a, b)=(2501, 2503) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk_3 (a,b) VALUES (2501, 2503);
-ERROR: insert or update on table "fk_partitioned_fk_3_0" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_0" violates foreign key constraint "fk_partitioned_fk_3_0_a_b_fkey"
DETAIL: Key (a, b)=(2501, 2503) is not present in table "fk_notpartitioned_pk".
-- but if we insert the values that make them valid, then they work
INSERT INTO fk_notpartitioned_pk VALUES (500, 501), (1500, 1501),
@@ -1726,7 +1782,7 @@ INSERT INTO fk_partitioned_fk (a,b) VALUES (2500, 2502);
INSERT INTO fk_partitioned_fk (a,b) VALUES (2501, 2503);
-- this update fails because there is no referenced row
UPDATE fk_partitioned_fk SET a = a + 1 WHERE a = 2501;
-ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_3_1_a_b_fkey"
DETAIL: Key (a, b)=(2502, 2503) is not present in table "fk_notpartitioned_pk".
-- but we can fix it thusly:
INSERT INTO fk_notpartitioned_pk (a,b) VALUES (2502, 2503);
@@ -1869,8 +1925,6 @@ WHERE conrelid::regclass::text like 'fk_partitioned_fk%' ORDER BY oid::regclass:
DROP TABLE fk_partitioned_fk, fk_notpartitioned_pk;
-- NOT VALID foreign key on a non-partitioned table referencing a partitioned table
-CREATE TABLE fk_partitioned_pk (a int, b int, PRIMARY KEY (a, b)) PARTITION BY RANGE (a, b);
-CREATE TABLE fk_partitioned_pk_1 PARTITION OF fk_partitioned_pk FOR VALUES FROM (0,0) TO (1000,1000);
CREATE TABLE fk_notpartitioned_fk (b int, a int);
ALTER TABLE fk_notpartitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk NOT VALID;
-- Constraint will be invalid.
@@ -2086,7 +2140,7 @@ CREATE TABLE fk_partitioned_fk_2 (b int, a int,
FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
BEGIN;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
--- should be having two constraints
+-- should only have one constraint
\d fk_partitioned_fk_2
Table "public.fk_partitioned_fk_2"
Column | Type | Collation | Nullable | Default
@@ -2095,7 +2149,6 @@ ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN
a | integer | | |
Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
Foreign-key constraints:
- "fk_partitioned_fk_2_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED
TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
DROP TABLE fk_partitioned_fk_2;
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index 7e5be0702da..fa51c7110ef 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -1248,18 +1248,49 @@ ALTER TABLE fk_partitioned_fk ADD CONSTRAINT fk_partitioned_fk_a_b_fkey FOREIGN
REFERENCES fk_notpartitioned_pk NOT ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
-ALTER TABLE fk_partitioned_fk_2 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+
CREATE TABLE fk_partitioned_fk_3 (fdrop1 int, fdrop2 int, fdrop3 int, fdrop4 int, b int, a int)
PARTITION BY HASH (a);
ALTER TABLE fk_partitioned_fk_3 DROP COLUMN fdrop1, DROP COLUMN fdrop2,
DROP COLUMN fdrop3, DROP COLUMN fdrop4;
CREATE TABLE fk_partitioned_fk_3_0 PARTITION OF fk_partitioned_fk_3 FOR VALUES WITH (MODULUS 5, REMAINDER 0);
CREATE TABLE fk_partitioned_fk_3_1 PARTITION OF fk_partitioned_fk_3 FOR VALUES WITH (MODULUS 5, REMAINDER 1);
+-- Merge the not-enforced parent constraint with the enforced and not-enforced child constraints
+ALTER TABLE fk_partitioned_fk_3_0 ADD CONSTRAINT fk_partitioned_fk_3_0_a_b_fkey FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ENFORCED;
+ALTER TABLE fk_partitioned_fk_3_1 ADD CONSTRAINT fk_partitioned_fk_3_1_a_b_fkey FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk_3 ADD CONSTRAINT fk_partitioned_fk_3_a_b_fkey FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+
+-- Merge the non-enforced parent constraint with both the enforced and
+-- non-enforced child constraints, where the referenced table is partitioned.
+CREATE TABLE fk_partitioned_pk (a int, b int, PRIMARY KEY (a, b)) PARTITION BY RANGE (a, b);
+CREATE TABLE fk_partitioned_pk_1 PARTITION OF fk_partitioned_pk FOR VALUES FROM (0,0) TO (1000,1000);
+-- Merge the enforced parent constraint with the enforced and not-enforced child constraints.
+ALTER TABLE fk_partitioned_fk_3_0 ADD CONSTRAINT fk_partitioned_fk_3_0_a_b_fkey1 FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk ENFORCED;
+ALTER TABLE fk_partitioned_fk_3_1 ADD CONSTRAINT fk_partitioned_fk_3_1_a_b_fkey1 FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk_3 ADD CONSTRAINT fk_partitioned_fk_3_a_b_fkey1 FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk ENFORCED;
+
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+
+ALTER TABLE fk_partitioned_fk_3 DETACH PARTITION fk_partitioned_fk_3_0;
+ALTER TABLE fk_partitioned_fk_3 ATTACH PARTITION fk_partitioned_fk_3_0 FOR VALUES WITH (MODULUS 5, REMAINDER 0);
+
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+
+ALTER TABLE fk_partitioned_fk_3 ALTER CONSTRAINT fk_partitioned_fk_3_a_b_fkey1 NOT ENFORCED;
+
+-- Merging an enforced parent constraint (validated) with a not-enforced child
+-- constraint will implicitly change the child constraint to enforced and apply
+-- the validation as well.
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_3
FOR VALUES FROM (2000,2000) TO (3000,3000);
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+
-- Creating a foreign key with ONLY on a partitioned table referencing
-- a non-partitioned table fails.
ALTER TABLE ONLY fk_partitioned_fk ADD FOREIGN KEY (a, b)
@@ -1386,8 +1417,6 @@ WHERE conrelid::regclass::text like 'fk_partitioned_fk%' ORDER BY oid::regclass:
DROP TABLE fk_partitioned_fk, fk_notpartitioned_pk;
-- NOT VALID foreign key on a non-partitioned table referencing a partitioned table
-CREATE TABLE fk_partitioned_pk (a int, b int, PRIMARY KEY (a, b)) PARTITION BY RANGE (a, b);
-CREATE TABLE fk_partitioned_pk_1 PARTITION OF fk_partitioned_pk FOR VALUES FROM (0,0) TO (1000,1000);
CREATE TABLE fk_notpartitioned_fk (b int, a int);
ALTER TABLE fk_notpartitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk NOT VALID;
@@ -1528,7 +1557,7 @@ CREATE TABLE fk_partitioned_fk_2 (b int, a int,
FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
BEGIN;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
--- should be having two constraints
+-- should only have one constraint
\d fk_partitioned_fk_2
DROP TABLE fk_partitioned_fk_2;
ROLLBACK;
--
2.43.5
On 21.03.25 06:58, Amul Sul wrote:
I think the next step here is that you work to fix Álvaro's concerns
about the recursion structure.Yes, I worked on that in the attached version. I refactored
ATExecAlterConstraintInternal() and moved the code that updates the
pg_constraint entry into a separate function (see 0001), so it can be
called from the places where the entry needs to be updated, rather
than revisiting ATExecAlterConstraintInternal(). In 0002,
ATExecAlterConstraintInternal() is split into two functions:
ATExecAlterConstrDeferrability() and
ATExecAlterConstrInheritability(), which handle altering deferrability
and inheritability, respectively. These functions are expected to
recurse on themselves, rather than revisiting
ATExecAlterConstraintInternal() as before. This approach simplifies
things. Similarly can add ATExecAlterConstrEnforceability() which
recurses itself.Yeah, I gave this a look and I think this code layout is good. There
are more functions now, but the code flow is simpler.Thank you !
Attached is the updated version, where the commit messages for patch
0005 and 0006 have been slightly corrected. Additionally, a few code
comments have been updated to consistently use the ENFORCED/NOT
ENFORCED keywords. The rest of the patches and all the code are
unchanged.
I have committed patches 0001 through 0003. I made some small changes:
In 0001, I renamed the function UpdateConstraintEntry() to
AlterConstrUpdateConstraintEntry() so the context is clearer.
In 0002, you had this change:
@@ -12113,75 +12154,89 @@ ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon,
* If the table at either end of the constraint is partitioned, we need to
* handle every constraint that is a child of this one.
*/
- if (recurse && changed &&
+ if (recurse &&
(rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
- (OidIsValid(refrelid) &&
- get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)))
- ATExecAlterChildConstr(wqueue, cmdcon, conrel, tgrel, rel, contuple,
- recurse, otherrelids, lockmode);
+ get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE))
+ AlterConstrDeferrabilityRecurse(wqueue, cmdcon, conrel, tgrel, rel,
+ contuple, recurse, otherrelids,
+ lockmode);
AFAICT, dropping the "changed" from the conditional was not correct. Or at
least, it would do redundant work if nothing was "changed". So I put that
back. Let me know if that change was intentional or there is something else
going on.
I will work through the remaining patches. It looks good to me so far.
On Tue, Mar 25, 2025 at 10:18 PM Peter Eisentraut <peter@eisentraut.org> wrote:
On 21.03.25 06:58, Amul Sul wrote:
[....]
Attached is the updated version, where the commit messages for patch
0005 and 0006 have been slightly corrected. Additionally, a few code
comments have been updated to consistently use the ENFORCED/NOT
ENFORCED keywords. The rest of the patches and all the code are
unchanged.I have committed patches 0001 through 0003. I made some small changes:
Thank you very much !
In 0001, I renamed the function UpdateConstraintEntry() to
AlterConstrUpdateConstraintEntry() so the context is clearer.In 0002, you had this change:
@@ -12113,75 +12154,89 @@ ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon, * If the table at either end of the constraint is partitioned, we need to * handle every constraint that is a child of this one. */ - if (recurse && changed && + if (recurse && (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE || - (OidIsValid(refrelid) && - get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE))) - ATExecAlterChildConstr(wqueue, cmdcon, conrel, tgrel, rel, contuple, - recurse, otherrelids, lockmode); + get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)) + AlterConstrDeferrabilityRecurse(wqueue, cmdcon, conrel, tgrel, rel, + contuple, recurse, otherrelids, + lockmode);AFAICT, dropping the "changed" from the conditional was not correct. Or at
least, it would do redundant work if nothing was "changed". So I put that
back. Let me know if that change was intentional or there is something else
going on.
Makes sense. This is intentional, but I must confess that this change
isn't part of the scope of this patch. I should have mentioned it when
posting, as it was something I intended to discuss with Álvaro, but it
slipped my mind.
The reason for the change is to revert to the behavior before commit
#80d7f990496b1c, where recursion occurred regardless of the
changed flags. This is also described in the header comment for
ATExecAlterConstrDeferrability() (earlier it was for
ATExecAlterConstraintInternal):
*
* Note that we must recurse even when the values are correct, in case
* indirect descendants have had their constraints altered locally.
* (This could be avoided if we forbade altering constraints in partitions
* but existing releases don't do that.)
*
Regards,
Amul
On 2025-Mar-26, Amul Sul wrote:
The reason for the change is to revert to the behavior before commit
#80d7f990496b1c, where recursion occurred regardless of the
changed flags. This is also described in the header comment for
ATExecAlterConstrDeferrability() (earlier it was for
ATExecAlterConstraintInternal):* Note that we must recurse even when the values are correct, in case
* indirect descendants have had their constraints altered locally.
* (This could be avoided if we forbade altering constraints in partitions
* but existing releases don't do that.)
Umm, why? Surely we should not allow a partition tree to become
inconsistent.
--
Álvaro Herrera PostgreSQL Developer — https://www.EnterpriseDB.com/
#error "Operator lives in the wrong universe"
("Use of cookies in real-time system development", M. Gleixner, M. Mc Guire)
On Wed, Mar 26, 2025 at 12:29 PM Álvaro Herrera <alvherre@alvh.no-ip.org> wrote:
On 2025-Mar-26, Amul Sul wrote:
The reason for the change is to revert to the behavior before commit
#80d7f990496b1c, where recursion occurred regardless of the
changed flags. This is also described in the header comment for
ATExecAlterConstrDeferrability() (earlier it was for
ATExecAlterConstraintInternal):* Note that we must recurse even when the values are correct, in case
* indirect descendants have had their constraints altered locally.
* (This could be avoided if we forbade altering constraints in partitions
* but existing releases don't do that.)Umm, why? Surely we should not allow a partition tree to become
inconsistent.
I just checked, and we are not allowed to alter a constraint on the
child table alone, nor can we merge it when attaching to the parent
constraint if the deferrability is different. Therefore, I think we
should remove this comment as it seems outdated now.
Regards,
Amul
On Wed, Mar 26, 2025 at 9:33 AM Amul Sul <sulamul@gmail.com> wrote:
On Tue, Mar 25, 2025 at 10:18 PM Peter Eisentraut <peter@eisentraut.org>
wrote:On 21.03.25 06:58, Amul Sul wrote:
[....]
Attached is the updated version, where the commit messages for patch
0005 and 0006 have been slightly corrected. Additionally, a few code
comments have been updated to consistently use the ENFORCED/NOT
ENFORCED keywords. The rest of the patches and all the code are
unchanged.I have committed patches 0001 through 0003. I made some small changes:
Thank you very much !
I wanted to run my dump and restore test on this patch set but it doesn't
apply cleanly. I tried applying 0004-0006. 0004 applies but not 0005.
I reviewed the tests to see if they leave objects in enough varied states
for 002_pg_upgrade to test it well. The test frequently drops and recreates
same partitioned and non-partitioned table inducing different states. So I
have a feeling that we are just leaving one state combination behind and
not all. It's an existing problem but probably with enforced and valid
states it becomes more serious. I ended up reviewing the tests. Here are
some comments.
+-- Reverting it back to ENFORCED will result in failure because constraint
validation will be triggered,
+-- as it was previously in a valid state.
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+ERROR: insert or update on table "fktable" violates foreign key
constraint "fktable_ftest1_fkey"
+DETAIL: Key (ftest1)=(1) is not present in table "pktable".
The text of error is misleading. There's no INSERT or UPDATE happening it's
ALTER. That's what VALIDATE CONSTRAINT also reports, so not fault of this
patch. But thought of writing it here since I noticed this now.
+-- Remove any existing rows that violate the constraint, then attempt to
change
+-- it.
+TRUNCATE FKTABLE;
I think, it will be good if we don't remove all the data; we wouldn't know
whether constraint was considered validated because there's no data or it
actually scanned the table and found no rows violating the constraint.
Let's leave/insert one row which obeys the constraint.
+-- Modifying other attributes of a constraint should not affect its
enforceability, and vice versa
+ALTER TABLE FKTABLE ADD CONSTRAINT fk_con FOREIGN KEY(ftest1, ftest2)
REFERENCES PKTABLE NOT VALID NOT ENFORCED;
+ALTER TABLE FKTABLE ALTER CONSTRAINT fk_con DEFERRABLE INITIALLY DEFERRED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fk_con';
+ condeferrable | condeferred | conenforced | convalidated
+---------------+-------------+-------------+--------------
+ t | t | f | f
+(1 row)
+
+ALTER TABLE FKTABLE ALTER CONSTRAINT fk_con NOT ENFORCED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fk_con';
+ condeferrable | condeferred | conenforced | convalidated
+---------------+-------------+-------------+--------------
+ t | t | f | f
+(1 row)
The statement is marking a NOT ENFORCED constraint as NOT ENFORCED. What is
it trying to test?
+
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT ENFORCED;
+BEGIN;
+-- doesn't match FK, no error.
A "but" before no error would make the comment clearer.
+UPDATE pktable SET id = 10 WHERE id = 5;
+-- doesn't match PK, no error.
A "but" before no error would make the comment clearer.
CREATE TABLE fk_partitioned_fk_1 (fdrop1 int, fdrop2 int, a int, fdrop3
int, b int);
ALTER TABLE fk_partitioned_fk_1 DROP COLUMN fdrop1, DROP COLUMN fdrop2,
DROP COLUMN fdrop3;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR
VALUES FROM (0,0) TO (1000,1000);
-ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES
fk_notpartitioned_pk;
+ALTER TABLE fk_partitioned_fk ADD CONSTRAINT fk_partitioned_fk_a_b_fkey
FOREIGN KEY (a, b)
+ REFERENCES fk_notpartitioned_pk NOT ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
+ALTER TABLE fk_partitioned_fk_2 ADD FOREIGN KEY (a, b) REFERENCES
fk_notpartitioned_pk NOT ENFORCED;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR
VALUES FROM (1000,1000) TO (2000,2000);
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey
ENFORCED;
I don't understand what value this change is bringing? Maybe a comment
explaining it. Why did we change name of one constraint and not the other?
ERROR: insert or update on table "fk_partitioned_fk_1" violates foreign
key constraint "fk_partitioned_fk_a_b_fkey"
DETAIL: Key (a, b)=(500, 501) is not present in table
"fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (1500, 1501);
-ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign
key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign
key constraint "fk_partitioned_fk_2_a_b_fkey"
DETAIL: Key (a, b)=(1500, 1501) is not present in table
"fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk_2 (a,b) VALUES (1500, 1501);
-ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign
key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign
key constraint "fk_partitioned_fk_2_a_b_fkey"
DETAIL: Key (a, b)=(1500, 1501) is not present in table
"fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (2500, 2502);
ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign
key constraint "fk_partitioned_fk_a_b_fkey"
These diffs would go away if we didn't rename the constraint.
@@ -1667,6 +1753,37 @@ Indexes:
Referenced by:
TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey"
FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b)
+-- Check the exsting FK trigger
+CREATE TEMP TABLE tmp_trg_info AS SELECT tgtype, tgrelid::regclass,
tgconstraint
+FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM
pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+SELECT count(1) FROM tmp_trg_info;
+ count
+-------
+ 14
+(1 row)
+
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey
NOT ENFORCED;
+-- No triggers
+SELECT count(1) FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM
pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+ count
+-------
+ 0
+(1 row)
+
+-- Changing it back to ENFORCED will recreate the necessary triggers.
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey
ENFORCED;
+-- Should be exactly the same number of triggers found as before
+SELECT COUNT(1) FROM pg_trigger pgt JOIN tmp_trg_info tt
+ON (pgt.tgtype = tt.tgtype AND pgt.tgrelid = tt.tgrelid AND
pgt.tgconstraint = tt.tgconstraint);
+ count
+-------
+ 14
+(1 row)
Why don't we just print the relevant triggers. Just matching counts could
be misleading - we may have added one and dropped another keeping the count
same.
+BEGIN;
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey
NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR
VALUES IN (1500,1502);
+-- should have only one constraint
+\d fk_partitioned_fk_2
+ Table "public.fk_partitioned_fk_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ b | integer | | |
+ a | integer | | |
+Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
+Foreign-key constraints:
+ TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey"
FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE
ON DELETE CASCADE NOT ENFORCED
Did it just rename an existing constraint on fk_partitioned_fk_2? Is that
ok? I thought we just mark the constraint as inherited.
--
Best Wishes,
Ashutosh Bapat
On Thu, Mar 27, 2025 at 11:05 AM Ashutosh Bapat
<ashutosh.bapat.oss@gmail.com> wrote:
On Wed, Mar 26, 2025 at 9:33 AM Amul Sul <sulamul@gmail.com> wrote:
On Tue, Mar 25, 2025 at 10:18 PM Peter Eisentraut <peter@eisentraut.org> wrote:
On 21.03.25 06:58, Amul Sul wrote:
[....]
Attached is the updated version, where the commit messages for patch
0005 and 0006 have been slightly corrected. Additionally, a few code
comments have been updated to consistently use the ENFORCED/NOT
ENFORCED keywords. The rest of the patches and all the code are
unchanged.I have committed patches 0001 through 0003. I made some small changes:
Thank you very much !
I wanted to run my dump and restore test on this patch set but it doesn't apply cleanly. I tried applying 0004-0006. 0004 applies but not 0005.
Yes, I failed to notice that due to the previous patch commit with
Peter's improvements, this has started failing to apply. I've attached
the rebased version.
I reviewed the tests to see if they leave objects in enough varied states for 002_pg_upgrade to test it well. The test frequently drops and recreates same partitioned and non-partitioned table inducing different states. So I have a feeling that we are just leaving one state combination behind and not all. It's an existing problem but probably with enforced and valid states it becomes more serious. I ended up reviewing the tests. Here are some comments.
Thanks for the review.
+-- Reverting it back to ENFORCED will result in failure because constraint validation will be triggered, +-- as it was previously in a valid state. +ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED; +ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_fkey" +DETAIL: Key (ftest1)=(1) is not present in table "pktable".The text of error is misleading. There's no INSERT or UPDATE happening it's ALTER. That's what VALIDATE CONSTRAINT also reports, so not fault of this patch. But thought of writing it here since I noticed this now.
Yes, this is an existing issue, and Peter too mentioned the same in his
previous review[1].
+-- Remove any existing rows that violate the constraint, then attempt to change +-- it. +TRUNCATE FKTABLE;I think, it will be good if we don't remove all the data; we wouldn't know whether constraint was considered validated because there's no data or it actually scanned the table and found no rows violating the constraint. Let's leave/insert one row which obeys the constraint.
Makes sense, did it that way.
+-- Modifying other attributes of a constraint should not affect its enforceability, and vice versa +ALTER TABLE FKTABLE ADD CONSTRAINT fk_con FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE NOT VALID NOT ENFORCED; +ALTER TABLE FKTABLE ALTER CONSTRAINT fk_con DEFERRABLE INITIALLY DEFERRED; +SELECT condeferrable, condeferred, conenforced, convalidated +FROM pg_constraint WHERE conname = 'fk_con'; + condeferrable | condeferred | conenforced | convalidated +---------------+-------------+-------------+-------------- + t | t | f | f +(1 row) + +ALTER TABLE FKTABLE ALTER CONSTRAINT fk_con NOT ENFORCED; +SELECT condeferrable, condeferred, conenforced, convalidated +FROM pg_constraint WHERE conname = 'fk_con'; + condeferrable | condeferred | conenforced | convalidated +---------------+-------------+-------------+-------------- + t | t | f | f +(1 row)The statement is marking a NOT ENFORCED constraint as NOT ENFORCED. What is it trying to test?
There was previously a bug that caused changes to other attributes.
Here, I wanted to verify that nothing changes if the constraint is
already in the desired state.
+ +ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT ENFORCED; +BEGIN; +-- doesn't match FK, no error.A "but" before no error would make the comment clearer.
Done.
+UPDATE pktable SET id = 10 WHERE id = 5; +-- doesn't match PK, no error.A "but" before no error would make the comment clearer.
Done.
CREATE TABLE fk_partitioned_fk_1 (fdrop1 int, fdrop2 int, a int, fdrop3 int, b int); ALTER TABLE fk_partitioned_fk_1 DROP COLUMN fdrop1, DROP COLUMN fdrop2, DROP COLUMN fdrop3; ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FROM (0,0) TO (1000,1000); -ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk; +ALTER TABLE fk_partitioned_fk ADD CONSTRAINT fk_partitioned_fk_a_b_fkey FOREIGN KEY (a, b) + REFERENCES fk_notpartitioned_pk NOT ENFORCED; CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int); ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2; +ALTER TABLE fk_partitioned_fk_2 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED; ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000); +ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;I don't understand what value this change is bringing? Maybe a comment explaining it. Why did we change name of one constraint and not the other?
Earlier, I used the system-generated constraint name in the ALTER
TABLE command, but I was advised[1] to use an explicit name instead.
As a result, I started using an explicit name when creating the
constraint on the fk_partitioned_fk table, which I plan to modify
later. However, I didn't take into account the other table that wasn't
being modified, such as the constraint for fk_partitioned_fk_2.
ERROR: insert or update on table "fk_partitioned_fk_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey" DETAIL: Key (a, b)=(500, 501) is not present in table "fk_notpartitioned_pk". INSERT INTO fk_partitioned_fk (a,b) VALUES (1500, 1501); -ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_a_b_fkey" +ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_2_a_b_fkey" DETAIL: Key (a, b)=(1500, 1501) is not present in table "fk_notpartitioned_pk". INSERT INTO fk_partitioned_fk_2 (a,b) VALUES (1500, 1501); -ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_a_b_fkey" +ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_2_a_b_fkey" DETAIL: Key (a, b)=(1500, 1501) is not present in table "fk_notpartitioned_pk". INSERT INTO fk_partitioned_fk (a,b) VALUES (2500, 2502); ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"These diffs would go away if we didn't rename the constraint.
The next patch, 0006, will revert this diff where it removes the ALTER
command that adds the constraint to fk_partitioned_fk_2. Previously,
the fk_partitioned_fk_2 table inherited its constraint from the parent
via the ATTACH operation, resulting in the same name as the parent
constraint. However, this patch explicitly created the constraint,
which led to a different name than when it was inherited.
Nevertheless, I fix this by explicitly specifying the constraint
name to match the parent constraint in the ALTER command to minimize
the diff in the 0005 patch.
@@ -1667,6 +1753,37 @@ Indexes:
Referenced by:
TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b)+-- Check the exsting FK trigger +CREATE TEMP TABLE tmp_trg_info AS SELECT tgtype, tgrelid::regclass, tgconstraint +FROM pg_trigger +WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass) + UNION ALL SELECT 'fk_notpartitioned_pk'::regclass); +SELECT count(1) FROM tmp_trg_info; + count +------- + 14 +(1 row) + +ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED; +-- No triggers +SELECT count(1) FROM pg_trigger +WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass) + UNION ALL SELECT 'fk_notpartitioned_pk'::regclass); + count +------- + 0 +(1 row) + +-- Changing it back to ENFORCED will recreate the necessary triggers. +ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED; +-- Should be exactly the same number of triggers found as before +SELECT COUNT(1) FROM pg_trigger pgt JOIN tmp_trg_info tt +ON (pgt.tgtype = tt.tgtype AND pgt.tgrelid = tt.tgrelid AND pgt.tgconstraint = tt.tgconstraint); + count +------- + 14 +(1 row)Why don't we just print the relevant triggers. Just matching counts could be misleading - we may have added one and dropped another keeping the count same.
I am not sure how to make such tests stable enough since the trigger
name involves OIDs. In count check, I tried adjusting the join
condition to ensure that I get the exact same type of constraint
w.r.t. trigger relation and the constraint.
+BEGIN; +ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED; +ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502); +-- should have only one constraint +\d fk_partitioned_fk_2 + Table "public.fk_partitioned_fk_2" + Column | Type | Collation | Nullable | Default +--------+---------+-----------+----------+--------- + b | integer | | | + a | integer | | | +Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502) +Foreign-key constraints: + TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCEDDid it just rename an existing constraint on fk_partitioned_fk_2? Is that ok? I thought we just mark the constraint as inherited.
This is the existing behavior of the psql meta command, which hides
the child constraint name when it inherits the parent constraint.
Regards,
Amul
1] /messages/by-id/CAAJ_b94PTcHzp2BqOxQZ7S-Zxp2hGV192a=4V8dTifjypDPpEw@mail.gmail.com
Attachments:
v19-0004-Remove-hastriggers-flag-check-before-fetching-FK.patchapplication/x-patch; name=v19-0004-Remove-hastriggers-flag-check-before-fetching-FK.patchDownload
From 4d88157a2f98aa31ea3e752d3228ba866d032d2b Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Thu, 27 Mar 2025 16:15:05 +0530
Subject: [PATCH v19 4/6] Remove hastriggers flag check before fetching FK
constraints.
With NOT ENFORCED, foreign key constraints will be created without
triggers. Therefore, the criteria for fetching foreign keys based on
the presence of triggers no longer apply.
----
NOTE: This patch is intended to reduce the diff noise from the main
patch and is not meant to be committed separately. It should be
squashed with the main patch that adds ENFORCED/NOT ENFORCED.
----
---
src/backend/utils/cache/relcache.c | 5 -
src/bin/pg_dump/pg_dump.c | 8 +-
src/bin/psql/describe.c | 226 ++++++++++++++---------------
3 files changed, 108 insertions(+), 131 deletions(-)
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 9f54a9e72b7..e6721056536 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4666,11 +4666,6 @@ RelationGetFKeyList(Relation relation)
if (relation->rd_fkeyvalid)
return relation->rd_fkeylist;
- /* Fast path: non-partitioned tables without triggers can't have FKs */
- if (!relation->rd_rel->relhastriggers &&
- relation->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
- return NIL;
-
/*
* We build the list we intend to return (in the caller's context) while
* doing the scan. After successfully completing the scan, we copy that
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index e41e645f649..dc9797aa251 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -8016,13 +8016,7 @@ getConstraints(Archive *fout, TableInfo tblinfo[], int numTables)
{
TableInfo *tinfo = &tblinfo[i];
- /*
- * For partitioned tables, foreign keys have no triggers so they must
- * be included anyway in case some foreign keys are defined.
- */
- if ((!tinfo->hastriggers &&
- tinfo->relkind != RELKIND_PARTITIONED_TABLE) ||
- !(tinfo->dobj.dump & DUMP_COMPONENT_DEFINITION))
+ if (!(tinfo->dobj.dump & DUMP_COMPONENT_DEFINITION))
continue;
/* OK, we need info for this table */
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index bf565afcc4e..e4a4c5071dd 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2550,136 +2550,124 @@ describeOneTableDetails(const char *schemaname,
PQclear(result);
}
- /*
- * Print foreign-key constraints (there are none if no triggers,
- * except if the table is partitioned, in which case the triggers
- * appear in the partitions)
- */
- if (tableinfo.hastriggers ||
- tableinfo.relkind == RELKIND_PARTITIONED_TABLE)
+ /* Print foreign-key constraints */
+ if (pset.sversion >= 120000 &&
+ (tableinfo.ispartition || tableinfo.relkind == RELKIND_PARTITIONED_TABLE))
{
- if (pset.sversion >= 120000 &&
- (tableinfo.ispartition || tableinfo.relkind == RELKIND_PARTITIONED_TABLE))
+ /*
+ * Put the constraints defined in this table first, followed
+ * by the constraints defined in ancestor partitioned tables.
+ */
+ printfPQExpBuffer(&buf,
+ "SELECT conrelid = '%s'::pg_catalog.regclass AS sametable,\n"
+ " conname,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef,\n"
+ " conrelid::pg_catalog.regclass AS ontable\n"
+ " FROM pg_catalog.pg_constraint,\n"
+ " pg_catalog.pg_partition_ancestors('%s')\n"
+ " WHERE conrelid = relid AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
+ "ORDER BY sametable DESC, conname;",
+ oid, oid);
+ }
+ else
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT true as sametable, conname,\n"
+ " pg_catalog.pg_get_constraintdef(r.oid, true) as condef,\n"
+ " conrelid::pg_catalog.regclass AS ontable\n"
+ "FROM pg_catalog.pg_constraint r\n"
+ "WHERE r.conrelid = '%s' AND r.contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n",
+ oid);
+
+ if (pset.sversion >= 120000)
+ appendPQExpBufferStr(&buf, " AND conparentid = 0\n");
+ appendPQExpBufferStr(&buf, "ORDER BY conname");
+ }
+
+ result = PSQLexec(buf.data);
+ if (!result)
+ goto error_return;
+ else
+ tuples = PQntuples(result);
+
+ if (tuples > 0)
+ {
+ int i_sametable = PQfnumber(result, "sametable"),
+ i_conname = PQfnumber(result, "conname"),
+ i_condef = PQfnumber(result, "condef"),
+ i_ontable = PQfnumber(result, "ontable");
+
+ printTableAddFooter(&cont, _("Foreign-key constraints:"));
+ for (i = 0; i < tuples; i++)
{
/*
- * Put the constraints defined in this table first, followed
- * by the constraints defined in ancestor partitioned tables.
+ * Print untranslated constraint name and definition. Use
+ * a "TABLE tab" prefix when the constraint is defined in
+ * a parent partitioned table.
*/
- printfPQExpBuffer(&buf,
- "SELECT conrelid = '%s'::pg_catalog.regclass AS sametable,\n"
- " conname,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef,\n"
- " conrelid::pg_catalog.regclass AS ontable\n"
- " FROM pg_catalog.pg_constraint,\n"
- " pg_catalog.pg_partition_ancestors('%s')\n"
- " WHERE conrelid = relid AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
- "ORDER BY sametable DESC, conname;",
- oid, oid);
- }
- else
- {
- printfPQExpBuffer(&buf,
- "SELECT true as sametable, conname,\n"
- " pg_catalog.pg_get_constraintdef(r.oid, true) as condef,\n"
- " conrelid::pg_catalog.regclass AS ontable\n"
- "FROM pg_catalog.pg_constraint r\n"
- "WHERE r.conrelid = '%s' AND r.contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n",
- oid);
-
- if (pset.sversion >= 120000)
- appendPQExpBufferStr(&buf, " AND conparentid = 0\n");
- appendPQExpBufferStr(&buf, "ORDER BY conname");
- }
-
- result = PSQLexec(buf.data);
- if (!result)
- goto error_return;
- else
- tuples = PQntuples(result);
-
- if (tuples > 0)
- {
- int i_sametable = PQfnumber(result, "sametable"),
- i_conname = PQfnumber(result, "conname"),
- i_condef = PQfnumber(result, "condef"),
- i_ontable = PQfnumber(result, "ontable");
-
- printTableAddFooter(&cont, _("Foreign-key constraints:"));
- for (i = 0; i < tuples; i++)
- {
- /*
- * Print untranslated constraint name and definition. Use
- * a "TABLE tab" prefix when the constraint is defined in
- * a parent partitioned table.
- */
- if (strcmp(PQgetvalue(result, i, i_sametable), "f") == 0)
- printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
- PQgetvalue(result, i, i_ontable),
- PQgetvalue(result, i, i_conname),
- PQgetvalue(result, i, i_condef));
- else
- printfPQExpBuffer(&buf, " \"%s\" %s",
- PQgetvalue(result, i, i_conname),
- PQgetvalue(result, i, i_condef));
-
- printTableAddFooter(&cont, buf.data);
- }
- }
- PQclear(result);
- }
-
- /* print incoming foreign-key references */
- if (tableinfo.hastriggers ||
- tableinfo.relkind == RELKIND_PARTITIONED_TABLE)
- {
- if (pset.sversion >= 120000)
- {
- printfPQExpBuffer(&buf,
- "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
- " FROM pg_catalog.pg_constraint c\n"
- " WHERE confrelid IN (SELECT pg_catalog.pg_partition_ancestors('%s')\n"
- " UNION ALL VALUES ('%s'::pg_catalog.regclass))\n"
- " AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
- "ORDER BY conname;",
- oid, oid);
- }
- else
- {
- printfPQExpBuffer(&buf,
- "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
- " FROM pg_catalog.pg_constraint\n"
- " WHERE confrelid = %s AND contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n"
- "ORDER BY conname;",
- oid);
- }
-
- result = PSQLexec(buf.data);
- if (!result)
- goto error_return;
- else
- tuples = PQntuples(result);
-
- if (tuples > 0)
- {
- int i_conname = PQfnumber(result, "conname"),
- i_ontable = PQfnumber(result, "ontable"),
- i_condef = PQfnumber(result, "condef");
-
- printTableAddFooter(&cont, _("Referenced by:"));
- for (i = 0; i < tuples; i++)
- {
+ if (strcmp(PQgetvalue(result, i, i_sametable), "f") == 0)
printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
PQgetvalue(result, i, i_ontable),
PQgetvalue(result, i, i_conname),
PQgetvalue(result, i, i_condef));
+ else
+ printfPQExpBuffer(&buf, " \"%s\" %s",
+ PQgetvalue(result, i, i_conname),
+ PQgetvalue(result, i, i_condef));
- printTableAddFooter(&cont, buf.data);
- }
+ printTableAddFooter(&cont, buf.data);
}
- PQclear(result);
}
+ PQclear(result);
+
+ /* print incoming foreign-key references */
+ if (pset.sversion >= 120000)
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
+ " FROM pg_catalog.pg_constraint c\n"
+ " WHERE confrelid IN (SELECT pg_catalog.pg_partition_ancestors('%s')\n"
+ " UNION ALL VALUES ('%s'::pg_catalog.regclass))\n"
+ " AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
+ "ORDER BY conname;",
+ oid, oid);
+ }
+ else
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
+ " FROM pg_catalog.pg_constraint\n"
+ " WHERE confrelid = %s AND contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n"
+ "ORDER BY conname;",
+ oid);
+ }
+
+ result = PSQLexec(buf.data);
+ if (!result)
+ goto error_return;
+ else
+ tuples = PQntuples(result);
+
+ if (tuples > 0)
+ {
+ int i_conname = PQfnumber(result, "conname"),
+ i_ontable = PQfnumber(result, "ontable"),
+ i_condef = PQfnumber(result, "condef");
+
+ printTableAddFooter(&cont, _("Referenced by:"));
+ for (i = 0; i < tuples; i++)
+ {
+ printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
+ PQgetvalue(result, i, i_ontable),
+ PQgetvalue(result, i, i_conname),
+ PQgetvalue(result, i, i_condef));
+
+ printTableAddFooter(&cont, buf.data);
+ }
+ }
+ PQclear(result);
/* print any row-level policies */
if (pset.sversion >= 90500)
--
2.43.5
v19-0005-Add-support-for-NOT-ENFORCED-in-foreign-key-cons.patchapplication/x-patch; name=v19-0005-Add-support-for-NOT-ENFORCED-in-foreign-key-cons.patchDownload
From b7d808f5d213233a3afaad3b96bcfde827823a35 Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Thu, 27 Mar 2025 17:30:10 +0530
Subject: [PATCH v19 5/6] Add support for NOT ENFORCED in foreign key
constraints.
Typically, when a foreign key (FK) constraint is created on a table,
action and check triggers are added to maintain data integrity. With
this patch, if a constraint is marked as NOT ENFORCED, integrity
checks are no longer required, making these triggers unnecessary.
Consequently, when creating a NOT ENFORCED FK constraint, triggers
will not be created, and the constraint will be marked as NOT VALID.
Similarly, if an existing FK constraint is changed to NOT ENFORCED,
the associated triggers will be dropped, and the constraint will also
be marked as NOT VALID. Conversely, if a NOT ENFORCED FK constraint is
changed to ENFORCED, the necessary triggers will be created, and the
will be changed to VALID by performing necessary validation.
----
NOTE: In this patch, the tryAttachPartitionForeignKey() function will
not merge the constraint if the enforcibility differs. This will be
addressed in the next patch.
----
---
doc/src/sgml/catalogs.sgml | 2 +-
doc/src/sgml/ref/alter_table.sgml | 5 +-
doc/src/sgml/ref/create_table.sgml | 2 +-
src/backend/catalog/pg_constraint.c | 5 +-
src/backend/catalog/sql_features.txt | 2 +-
src/backend/commands/tablecmds.c | 455 ++++++++++++++++++----
src/backend/parser/gram.y | 9 +-
src/backend/parser/parse_utilcmd.c | 14 +-
src/backend/utils/cache/relcache.c | 1 +
src/include/nodes/parsenodes.h | 2 +
src/include/utils/rel.h | 3 +
src/test/regress/expected/constraints.out | 8 +-
src/test/regress/expected/foreign_key.out | 161 +++++++-
src/test/regress/expected/inherit.out | 81 ++--
src/test/regress/sql/foreign_key.sql | 111 +++++-
src/test/regress/sql/inherit.sql | 7 +
16 files changed, 720 insertions(+), 148 deletions(-)
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index fb050635551..014bb815665 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2620,7 +2620,7 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l
</para>
<para>
Is the constraint enforced?
- Currently, can be false only for CHECK constraints
+ Currently, can be false only for foreign keys and CHECK constraints
</para></entry>
</row>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 11d1bc7dbe1..ece438f0075 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -58,7 +58,7 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable>
ALTER [ COLUMN ] <replaceable class="parameter">column_name</replaceable> SET COMPRESSION <replaceable class="parameter">compression_method</replaceable>
ADD <replaceable class="parameter">table_constraint</replaceable> [ NOT VALID ]
ADD <replaceable class="parameter">table_constraint_using_index</replaceable>
- ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+ ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]
ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> [ INHERIT | NO INHERIT ]
VALIDATE CONSTRAINT <replaceable class="parameter">constraint_name</replaceable>
DROP CONSTRAINT [ IF EXISTS ] <replaceable class="parameter">constraint_name</replaceable> [ RESTRICT | CASCADE ]
@@ -589,7 +589,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
This form validates a foreign key or check constraint that was
previously created as <literal>NOT VALID</literal>, by scanning the
table to ensure there are no rows for which the constraint is not
- satisfied. Nothing happens if the constraint is already marked valid.
+ satisfied. If the constraint is not enforced, an error is thrown.
+ Nothing happens if the constraint is already marked valid.
(See <xref linkend="sql-altertable-notes"/> below for an explanation
of the usefulness of this command.)
</para>
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index e5c034d724e..4a41b2f5530 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -1409,7 +1409,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</para>
<para>
- This is currently only supported for <literal>CHECK</literal>
+ This is currently only supported for foreign key and <literal>CHECK</literal>
constraints.
</para>
</listitem>
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index ac80652baf2..0467e7442ff 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -100,8 +100,9 @@ CreateConstraintEntry(const char *constraintName,
ObjectAddresses *addrs_auto;
ObjectAddresses *addrs_normal;
- /* Only CHECK constraint can be not enforced */
- Assert(isEnforced || constraintType == CONSTRAINT_CHECK);
+ /* Only CHECK or FOREIGN KEY constraint can be not enforced */
+ Assert(isEnforced || constraintType == CONSTRAINT_CHECK ||
+ constraintType == CONSTRAINT_FOREIGN);
/* NOT ENFORCED constraint must be NOT VALID */
Assert(isEnforced || !isValidated);
diff --git a/src/backend/catalog/sql_features.txt b/src/backend/catalog/sql_features.txt
index 2f250d2c57b..ebe85337c28 100644
--- a/src/backend/catalog/sql_features.txt
+++ b/src/backend/catalog/sql_features.txt
@@ -281,7 +281,7 @@ F461 Named character sets NO
F471 Scalar subquery values YES
F481 Expanded NULL predicate YES
F491 Constraint management YES
-F492 Optional table constraint enforcement NO check constraints only
+F492 Optional table constraint enforcement YES except not-null constraints
F501 Features and conformance views YES
F501 Features and conformance views 01 SQL_FEATURES view YES
F501 Features and conformance views 02 SQL_SIZING view YES
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index afb25007613..0e126300878 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -395,6 +395,14 @@ static ObjectAddress ATExecAlterConstraint(List **wqueue, Relation rel,
static bool ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon, Relation conrel,
Relation tgrel, Relation rel, HeapTuple contuple,
bool recurse, LOCKMODE lockmode);
+static bool ATExecAlterConstrEnforceability(List **wqueue, ATAlterConstraint *cmdcon,
+ Relation conrel, Relation tgrel,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, LOCKMODE lockmode,
+ Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger);
static bool ATExecAlterConstrDeferrability(List **wqueue, ATAlterConstraint *cmdcon,
Relation conrel, Relation tgrel, Relation rel,
HeapTuple contuple, bool recurse,
@@ -405,6 +413,14 @@ static bool ATExecAlterConstrInheritability(List **wqueue, ATAlterConstraint *cm
static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
bool deferrable, bool initdeferred,
List **otherrelids);
+static void AlterConstrEnforceabilityRecurse(List **wqueue, ATAlterConstraint *cmdcon,
+ Relation conrel, Relation tgrel,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, LOCKMODE lockmode,
+ Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger);
static void AlterConstrDeferrabilityRecurse(List **wqueue, ATAlterConstraint *cmdcon,
Relation conrel, Relation tgrel, Relation rel,
HeapTuple contuple, bool recurse,
@@ -10561,7 +10577,7 @@ addFkConstraint(addFkConstraintSides fkside,
CONSTRAINT_FOREIGN,
fkconstraint->deferrable,
fkconstraint->initdeferred,
- true, /* Is Enforced */
+ fkconstraint->is_enforced,
fkconstraint->initially_valid,
parentConstr,
RelationGetRelid(rel),
@@ -10679,21 +10695,23 @@ addFkRecurseReferenced(Constraint *fkconstraint, Relation rel,
Oid parentDelTrigger, Oid parentUpdTrigger,
bool with_period)
{
- Oid deleteTriggerOid,
- updateTriggerOid;
+ Oid deleteTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
Assert(CheckRelationLockedByMe(pkrel, ShareRowExclusiveLock, true));
Assert(CheckRelationLockedByMe(rel, ShareRowExclusiveLock, true));
/*
- * Create the action triggers that enforce the constraint.
+ * Create action triggers to enforce the constraint, or skip them if the
+ * constraint is NOT ENFORCED.
*/
- createForeignKeyActionTriggers(RelationGetRelid(rel),
- RelationGetRelid(pkrel),
- fkconstraint,
- parentConstr, indexOid,
- parentDelTrigger, parentUpdTrigger,
- &deleteTriggerOid, &updateTriggerOid);
+ if (fkconstraint->is_enforced)
+ createForeignKeyActionTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
+ fkconstraint,
+ parentConstr, indexOid,
+ parentDelTrigger, parentUpdTrigger,
+ &deleteTriggerOid, &updateTriggerOid);
/*
* If the referenced table is partitioned, recurse on ourselves to handle
@@ -10814,8 +10832,8 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
Oid parentInsTrigger, Oid parentUpdTrigger,
bool with_period)
{
- Oid insertTriggerOid,
- updateTriggerOid;
+ Oid insertTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
Assert(OidIsValid(parentConstr));
Assert(CheckRelationLockedByMe(rel, ShareRowExclusiveLock, true));
@@ -10827,29 +10845,32 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
errmsg("foreign key constraints are not supported on foreign tables")));
/*
- * Add the check triggers to it and, if necessary, schedule it to be
- * checked in Phase 3.
+ * Add check triggers if the constraint is ENFORCED, and if needed,
+ * schedule them to be checked in Phase 3.
*
* If the relation is partitioned, drill down to do it to its partitions.
*/
- createForeignKeyCheckTriggers(RelationGetRelid(rel),
- RelationGetRelid(pkrel),
- fkconstraint,
- parentConstr,
- indexOid,
- parentInsTrigger, parentUpdTrigger,
- &insertTriggerOid, &updateTriggerOid);
+ if (fkconstraint->is_enforced)
+ createForeignKeyCheckTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
+ fkconstraint,
+ parentConstr,
+ indexOid,
+ parentInsTrigger, parentUpdTrigger,
+ &insertTriggerOid, &updateTriggerOid);
if (rel->rd_rel->relkind == RELKIND_RELATION)
{
/*
* Tell Phase 3 to check that the constraint is satisfied by existing
- * rows. We can skip this during table creation, when requested
- * explicitly by specifying NOT VALID in an ADD FOREIGN KEY command,
- * and when we're recreating a constraint following a SET DATA TYPE
- * operation that did not impugn its validity.
+ * rows. We can skip this during table creation, when constraint is
+ * specified as NOT ENFORCED, or when requested explicitly by
+ * specifying NOT VALID in an ADD FOREIGN KEY command, and when we're
+ * recreating a constraint following a SET DATA TYPE operation that
+ * did not impugn its validity.
*/
- if (wqueue && !old_check_ok && !fkconstraint->skip_validation)
+ if (wqueue && !old_check_ok && !fkconstraint->skip_validation &&
+ fkconstraint->is_enforced)
{
NewConstraint *newcon;
AlteredTableInfo *tab;
@@ -11080,8 +11101,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
AttrNumber confdelsetcols[INDEX_MAX_KEYS];
Constraint *fkconstraint;
ObjectAddress address;
- Oid deleteTriggerOid,
- updateTriggerOid;
+ Oid deleteTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(constrOid));
if (!HeapTupleIsValid(tuple))
@@ -11141,8 +11162,9 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
fkconstraint->fk_del_set_cols = NIL;
fkconstraint->old_conpfeqop = NIL;
fkconstraint->old_pktable_oid = InvalidOid;
+ fkconstraint->is_enforced = constrForm->conenforced;
fkconstraint->skip_validation = false;
- fkconstraint->initially_valid = true;
+ fkconstraint->initially_valid = constrForm->convalidated;
/* set up colnames that are used to generate the constraint name */
for (int i = 0; i < numfks; i++)
@@ -11170,9 +11192,10 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
* parent OIDs for similar triggers that will be created on the
* partition in addFkRecurseReferenced().
*/
- GetForeignKeyActionTriggers(trigrel, constrOid,
- constrForm->confrelid, constrForm->conrelid,
- &deleteTriggerOid, &updateTriggerOid);
+ if (constrForm->conenforced)
+ GetForeignKeyActionTriggers(trigrel, constrOid,
+ constrForm->confrelid, constrForm->conrelid,
+ &deleteTriggerOid, &updateTriggerOid);
/* Add this constraint ... */
address = addFkConstraint(addFkReferencedSide,
@@ -11305,8 +11328,8 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
Oid indexOid;
ObjectAddress address;
ListCell *lc;
- Oid insertTriggerOid,
- updateTriggerOid;
+ Oid insertTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
bool with_period;
tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(parentConstrOid));
@@ -11338,17 +11361,18 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
mapped_conkey[i] = attmap->attnums[conkey[i] - 1];
/*
- * Get the "check" triggers belonging to the constraint to pass as
- * parent OIDs for similar triggers that will be created on the
- * partition in addFkRecurseReferencing(). They are also passed to
- * tryAttachPartitionForeignKey() below to simply assign as parents to
- * the partition's existing "check" triggers, that is, if the
- * corresponding constraints is deemed attachable to the parent
- * constraint.
+ * Get the "check" triggers belonging to the constraint, if it is
+ * ENFORCED, to pass as parent OIDs for similar triggers that will be
+ * created on the partition in addFkRecurseReferencing(). They are
+ * also passed to tryAttachPartitionForeignKey() below to simply
+ * assign as parents to the partition's existing "check" triggers,
+ * that is, if the corresponding constraints is deemed attachable to
+ * the parent constraint.
*/
- GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
- constrForm->confrelid, constrForm->conrelid,
- &insertTriggerOid, &updateTriggerOid);
+ if (constrForm->conenforced)
+ GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
+ constrForm->confrelid, constrForm->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
/*
* Before creating a new constraint, see whether any existing FKs are
@@ -11401,6 +11425,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
fkconstraint->fk_del_set_cols = NIL;
fkconstraint->old_conpfeqop = NIL;
fkconstraint->old_pktable_oid = InvalidOid;
+ fkconstraint->is_enforced = constrForm->conenforced;
fkconstraint->skip_validation = false;
fkconstraint->initially_valid = constrForm->convalidated;
for (int i = 0; i < numfks; i++)
@@ -11518,6 +11543,7 @@ tryAttachPartitionForeignKey(List **wqueue,
if (OidIsValid(partConstr->conparentid) ||
partConstr->condeferrable != parentConstr->condeferrable ||
partConstr->condeferred != parentConstr->condeferred ||
+ partConstr->conenforced != parentConstr->conenforced ||
partConstr->confupdtype != parentConstr->confupdtype ||
partConstr->confdeltype != parentConstr->confdeltype ||
partConstr->confmatchtype != parentConstr->confmatchtype)
@@ -11561,8 +11587,7 @@ AttachPartitionForeignKey(List **wqueue,
bool queueValidation;
Oid partConstrFrelid;
Oid partConstrRelid;
- Oid insertTriggerOid,
- updateTriggerOid;
+ bool parentConstrIsEnforced;
/* Fetch the parent constraint tuple */
parentConstrTup = SearchSysCache1(CONSTROID,
@@ -11570,6 +11595,7 @@ AttachPartitionForeignKey(List **wqueue,
if (!HeapTupleIsValid(parentConstrTup))
elog(ERROR, "cache lookup failed for constraint %u", parentConstrOid);
parentConstr = (Form_pg_constraint) GETSTRUCT(parentConstrTup);
+ parentConstrIsEnforced = parentConstr->conenforced;
/* Fetch the child constraint tuple */
partcontup = SearchSysCache1(CONSTROID,
@@ -11619,17 +11645,24 @@ AttachPartitionForeignKey(List **wqueue,
/*
* Like the constraint, attach partition's "check" triggers to the
- * corresponding parent triggers.
+ * corresponding parent triggers if the constraint is ENFORCED. NOT
+ * ENFORCED constraints do not have these triggers.
*/
- GetForeignKeyCheckTriggers(trigrel,
- partConstrOid, partConstrFrelid, partConstrRelid,
- &insertTriggerOid, &updateTriggerOid);
- Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
- TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
- RelationGetRelid(partition));
- Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
- TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
- RelationGetRelid(partition));
+ if (parentConstrIsEnforced)
+ {
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
+ GetForeignKeyCheckTriggers(trigrel,
+ partConstrOid, partConstrFrelid, partConstrRelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
+ RelationGetRelid(partition));
+ Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
+ RelationGetRelid(partition));
+ }
/*
* We updated this pg_constraint row above to set its parent; validating
@@ -11743,6 +11776,10 @@ RemoveInheritedConstraint(Relation conrel, Relation trigrel, Oid conoid,
*
* The subroutine for tryAttachPartitionForeignKey handles the deletion of
* action triggers for the foreign key constraint.
+ *
+ * If valid confrelid and conrelid values are not provided, the respective
+ * trigger check will be skipped, and the trigger will be considered for
+ * removal.
*/
static void
DropForeignKeyConstraintTriggers(Relation trigrel, Oid conoid, Oid confrelid,
@@ -11763,10 +11800,27 @@ DropForeignKeyConstraintTriggers(Relation trigrel, Oid conoid, Oid confrelid,
Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
ObjectAddress trigger;
- if (trgform->tgconstrrelid != conrelid)
+ /* Invalid if trigger is not for a referential integrity constraint */
+ if (!OidIsValid(trgform->tgconstrrelid))
continue;
- if (trgform->tgrelid != confrelid)
+ if (OidIsValid(conrelid) && trgform->tgconstrrelid != conrelid)
continue;
+ if (OidIsValid(confrelid) && trgform->tgrelid != confrelid)
+ continue;
+
+ /* We should be droping trigger related to foreign key constraint */
+ Assert(trgform->tgfoid == F_RI_FKEY_CHECK_INS ||
+ trgform->tgfoid == F_RI_FKEY_CHECK_UPD ||
+ trgform->tgfoid == F_RI_FKEY_CASCADE_DEL ||
+ trgform->tgfoid == F_RI_FKEY_CASCADE_UPD ||
+ trgform->tgfoid == F_RI_FKEY_RESTRICT_DEL ||
+ trgform->tgfoid == F_RI_FKEY_RESTRICT_UPD ||
+ trgform->tgfoid == F_RI_FKEY_SETNULL_DEL ||
+ trgform->tgfoid == F_RI_FKEY_SETNULL_UPD ||
+ trgform->tgfoid == F_RI_FKEY_SETDEFAULT_DEL ||
+ trgform->tgfoid == F_RI_FKEY_SETDEFAULT_UPD ||
+ trgform->tgfoid == F_RI_FKEY_NOACTION_DEL ||
+ trgform->tgfoid == F_RI_FKEY_NOACTION_UPD);
/*
* The constraint is originally set up to contain this trigger as an
@@ -11979,6 +12033,11 @@ ATExecAlterConstraint(List **wqueue, Relation rel, ATAlterConstraint *cmdcon,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("constraint \"%s\" of relation \"%s\" is not a foreign key constraint",
cmdcon->conname, RelationGetRelationName(rel))));
+ if (cmdcon->alterEnforceability && currcon->contype != CONSTRAINT_FOREIGN)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot alter enforceability of constraint \"%s\" of relation \"%s\"",
+ cmdcon->conname, RelationGetRelationName(rel))));
if (cmdcon->alterInheritability &&
currcon->contype != CONSTRAINT_NOTNULL)
ereport(ERROR,
@@ -12058,7 +12117,7 @@ ATExecAlterConstraint(List **wqueue, Relation rel, ATAlterConstraint *cmdcon,
/*
* A subroutine of ATExecAlterConstraint that calls the respective routines for
- * altering constraint attributes.
+ * altering constraint's enforceability, deferrability or inheritability.
*/
static bool
ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon,
@@ -12066,16 +12125,35 @@ ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon,
HeapTuple contuple, bool recurse,
LOCKMODE lockmode)
{
+ Form_pg_constraint currcon;
bool changed = false;
List *otherrelids = NIL;
+ currcon = (Form_pg_constraint) GETSTRUCT(contuple);
+
/*
- * Do the catalog work for the deferrability change, recurse if necessary.
+ * Do the catalog work for the enforceability or deferrability change,
+ * recurse if necessary.
+ *
+ * Note that even if deferrability is requested to be altered along with
+ * enforceability, we don't need to explicitly update multiple entries in
+ * pg_trigger related to deferrability.
+ *
+ * Modifying enforceability involves either creating or dropping the
+ * trigger, during which the deferrability setting will be adjusted
+ * automatically.
*/
- if (cmdcon->alterDeferrability &&
- ATExecAlterConstrDeferrability(wqueue, cmdcon, conrel, tgrel, rel,
- contuple, recurse, &otherrelids,
- lockmode))
+ if (cmdcon->alterEnforceability &&
+ ATExecAlterConstrEnforceability(wqueue, cmdcon, conrel, tgrel,
+ currcon->conrelid, currcon->confrelid,
+ contuple, lockmode, InvalidOid,
+ InvalidOid, InvalidOid, InvalidOid))
+ changed = true;
+
+ else if (cmdcon->alterDeferrability &&
+ ATExecAlterConstrDeferrability(wqueue, cmdcon, conrel, tgrel, rel,
+ contuple, recurse, &otherrelids,
+ lockmode))
{
/*
* AlterConstrUpdateConstraintEntry already invalidated relcache for
@@ -12100,6 +12178,151 @@ ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon,
return changed;
}
+/*
+ * Returns true if the constraint's enforceability is altered.
+ *
+ * Depending on whether the constraint is being set to ENFORCED or NOT
+ * ENFORCED, it creates or drops the trigger accordingly.
+ *
+ * Note that we must recurse even when trying to change a 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. 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.
+ */
+static bool
+ATExecAlterConstrEnforceability(List **wqueue, ATAlterConstraint *cmdcon,
+ Relation conrel, Relation tgrel,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, LOCKMODE lockmode,
+ Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger)
+{
+ Form_pg_constraint currcon;
+ Oid conoid;
+ Relation rel;
+ bool changed = false;
+
+ /* Since this function recurses, it could be driven to stack overflow */
+ check_stack_depth();
+
+ Assert(cmdcon->alterEnforceability);
+
+ currcon = (Form_pg_constraint) GETSTRUCT(contuple);
+ conoid = currcon->oid;
+
+ /* Should be foreign key constraint */
+ Assert(currcon->contype == CONSTRAINT_FOREIGN);
+
+ rel = table_open(currcon->conrelid, lockmode);
+
+ if (currcon->conenforced != cmdcon->is_enforced)
+ {
+ AlterConstrUpdateConstraintEntry(cmdcon, conrel, contuple);
+ changed = true;
+ }
+
+ /* Drop triggers */
+ if (!cmdcon->is_enforced)
+ {
+ /*
+ * When setting a constraint to NOT ENFORCED, the constraint triggers
+ * need to be dropped. Therefore, we must process the child relations
+ * first, followed by the parent, to account for dependencies.
+ */
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE)
+ AlterConstrEnforceabilityRecurse(wqueue, cmdcon, conrel, tgrel,
+ fkrelid, pkrelid, contuple,
+ lockmode, InvalidOid, InvalidOid,
+ InvalidOid, InvalidOid);
+
+ /* Drop all the triggers */
+ DropForeignKeyConstraintTriggers(tgrel, conoid, InvalidOid, InvalidOid);
+ }
+ else if (changed) /* Create triggers */
+ {
+ Oid ReferencedDelTriggerOid = InvalidOid,
+ ReferencedUpdTriggerOid = InvalidOid,
+ ReferencingInsTriggerOid = InvalidOid,
+ ReferencingUpdTriggerOid = InvalidOid;
+
+ /* Prepare the minimal information required for trigger creation. */
+ Constraint *fkconstraint = makeNode(Constraint);
+
+ fkconstraint->conname = pstrdup(NameStr(currcon->conname));
+ fkconstraint->fk_matchtype = currcon->confmatchtype;
+ fkconstraint->fk_upd_action = currcon->confupdtype;
+ fkconstraint->fk_del_action = currcon->confdeltype;
+
+ /* Create referenced triggers */
+ if (currcon->conrelid == fkrelid)
+ createForeignKeyActionTriggers(currcon->conrelid,
+ currcon->confrelid,
+ fkconstraint,
+ conoid,
+ currcon->conindid,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ &ReferencedDelTriggerOid,
+ &ReferencedUpdTriggerOid);
+
+ /* Create referencing triggers */
+ if (currcon->confrelid == pkrelid)
+ createForeignKeyCheckTriggers(currcon->conrelid,
+ pkrelid,
+ fkconstraint,
+ conoid,
+ currcon->conindid,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger,
+ &ReferencingInsTriggerOid,
+ &ReferencingUpdTriggerOid);
+
+ /*
+ * Tell Phase 3 to check that the constraint is satisfied by existing
+ * rows.
+ */
+ if (rel->rd_rel->relkind == RELKIND_RELATION)
+ {
+ AlteredTableInfo *tab;
+ NewConstraint *newcon;
+
+ newcon = (NewConstraint *) palloc0(sizeof(NewConstraint));
+ newcon->name = fkconstraint->conname;
+ newcon->contype = CONSTR_FOREIGN;
+ newcon->refrelid = currcon->confrelid;
+ newcon->refindid = currcon->conindid;
+ newcon->conid = currcon->oid;
+ newcon->qual = (Node *) fkconstraint;
+
+ /* Find or create work queue entry for this table */
+ tab = ATGetQueueEntry(wqueue, rel);
+ tab->constraints = lappend(tab->constraints, newcon);
+ }
+
+ /*
+ * If the table at either end of the constraint is partitioned, we
+ * need to recurse and create triggers for each constraint that is a
+ * child of this one.
+ */
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE)
+ AlterConstrEnforceabilityRecurse(wqueue, cmdcon, conrel, tgrel,
+ fkrelid, pkrelid, contuple,
+ lockmode, ReferencedDelTriggerOid,
+ ReferencedUpdTriggerOid,
+ ReferencingInsTriggerOid,
+ ReferencingUpdTriggerOid);
+ }
+
+ table_close(rel, NoLock);
+
+ return changed;
+}
+
/*
* Returns true if the constraint's deferrability is altered.
*
@@ -12304,6 +12527,55 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
systable_endscan(tgscan);
}
+/*
+ * Invokes ATExecAlterConstrEnforceability for each constraint that is a child of
+ * the specified constraint.
+ *
+ * Note that this doesn't handle recursion the normal way, viz. by scanning the
+ * list of child relations and recursing; instead it uses the conparentid
+ * relationships. This may need to be reconsidered.
+ *
+ * The arguments to this function have the same meaning as the arguments to
+ * ATExecAlterConstrEnforceability.
+ */
+static void
+AlterConstrEnforceabilityRecurse(List **wqueue, ATAlterConstraint *cmdcon,
+ Relation conrel, Relation tgrel,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, LOCKMODE lockmode,
+ Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger)
+{
+ Form_pg_constraint currcon;
+ Oid conoid;
+ ScanKeyData pkey;
+ SysScanDesc pscan;
+ HeapTuple childtup;
+
+ currcon = (Form_pg_constraint) GETSTRUCT(contuple);
+ conoid = currcon->oid;
+
+ ScanKeyInit(&pkey,
+ Anum_pg_constraint_conparentid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+
+ pscan = systable_beginscan(conrel, ConstraintParentIndexId,
+ true, NULL, 1, &pkey);
+
+ while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
+ ATExecAlterConstrEnforceability(wqueue, cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, childtup, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
+
+ systable_endscan(pscan);
+}
+
/*
* Invokes ATExecAlterConstrDeferrability for each constraint that is a child of
* the specified constraint.
@@ -12364,11 +12636,25 @@ AlterConstrUpdateConstraintEntry(ATAlterConstraint *cmdcon, Relation conrel,
HeapTuple copyTuple;
Form_pg_constraint copy_con;
- Assert(cmdcon->alterDeferrability || cmdcon->alterInheritability);
+ Assert(cmdcon->alterEnforceability || cmdcon->alterDeferrability ||
+ cmdcon->alterInheritability);
copyTuple = heap_copytuple(contuple);
copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
+ if (cmdcon->alterEnforceability)
+ {
+ copy_con->conenforced = cmdcon->is_enforced;
+
+ /*
+ * 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.
+ */
+ copy_con->convalidated = cmdcon->is_enforced;
+ }
if (cmdcon->alterDeferrability)
{
copy_con->condeferrable = cmdcon->deferrable;
@@ -17088,9 +17374,9 @@ MergeConstraintsIntoExisting(Relation child_rel, Relation parent_rel)
NameStr(child_con->conname), RelationGetRelationName(child_rel))));
/*
- * A non-enforced child constraint cannot be merged with an
- * enforced parent constraint. However, the reverse is allowed,
- * where the child constraint is enforced.
+ * A NOT ENFORCED child constraint cannot be merged with an
+ * ENFORCED parent constraint. However, the reverse is allowed,
+ * where the child constraint is ENFORCED.
*/
if (parent_con->conenforced && !child_con->conenforced)
ereport(ERROR,
@@ -20461,8 +20747,6 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
Form_pg_constraint conform;
- Oid insertTriggerOid,
- updateTriggerOid;
contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
if (!HeapTupleIsValid(contup))
@@ -20489,17 +20773,25 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
/*
* Also, look up the partition's "check" triggers corresponding to the
- * constraint being detached and detach them from the parent triggers.
+ * ENFORCED constraint being detached and detach them from the parent
+ * triggers. NOT ENFORCED constraints do not have these triggers;
+ * therefore, this step is not needed.
*/
- GetForeignKeyCheckTriggers(trigrel,
- fk->conoid, fk->confrelid, fk->conrelid,
- &insertTriggerOid, &updateTriggerOid);
- Assert(OidIsValid(insertTriggerOid));
- TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
- RelationGetRelid(partRel));
- Assert(OidIsValid(updateTriggerOid));
- TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
- RelationGetRelid(partRel));
+ if (fk->conenforced)
+ {
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ Assert(OidIsValid(updateTriggerOid));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ }
/*
* Lastly, create the action triggers on the referenced table, using
@@ -20539,8 +20831,9 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
fkconstraint->conname = pstrdup(NameStr(conform->conname));
fkconstraint->deferrable = conform->condeferrable;
fkconstraint->initdeferred = conform->condeferred;
+ fkconstraint->is_enforced = conform->conenforced;
fkconstraint->skip_validation = true;
- fkconstraint->initially_valid = true;
+ fkconstraint->initially_valid = conform->convalidated;
/* a few irrelevant fields omitted here */
fkconstraint->pktable = NULL;
fkconstraint->fk_attrs = NIL;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 0fc502a3a40..cc96d2aa022 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -2662,6 +2662,8 @@ alter_table_cmd:
n->subtype = AT_AlterConstraint;
n->def = (Node *) c;
c->conname = $3;
+ if ($4 & (CAS_NOT_ENFORCED | CAS_ENFORCED))
+ c->alterEnforceability = true;
if ($4 & (CAS_DEFERRABLE | CAS_NOT_DEFERRABLE |
CAS_INITIALLY_DEFERRED | CAS_INITIALLY_IMMEDIATE))
c->alterDeferrability = true;
@@ -2670,7 +2672,8 @@ alter_table_cmd:
processCASbits($4, @4, "FOREIGN KEY",
&c->deferrable,
&c->initdeferred,
- NULL, NULL, &c->noinherit, yyscanner);
+ &c->is_enforced, NULL,
+ &c->noinherit, yyscanner);
$$ = (Node *) n;
}
/* ALTER TABLE <name> ALTER CONSTRAINT INHERIT */
@@ -4334,8 +4337,8 @@ ConstraintElem:
n->fk_del_set_cols = ($11)->deleteAction->cols;
processCASbits($12, @12, "FOREIGN KEY",
&n->deferrable, &n->initdeferred,
- NULL, &n->skip_validation, NULL,
- yyscanner);
+ &n->is_enforced, &n->skip_validation,
+ NULL, yyscanner);
n->initially_valid = !n->skip_validation;
$$ = (Node *) n;
}
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 896a7f2c59b..50f430a4e1a 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -2976,8 +2976,10 @@ transformFKConstraints(CreateStmtContext *cxt,
/*
* If CREATE TABLE or adding a column with NULL default, we can safely
- * skip validation of FK constraints, and nonetheless mark them valid.
- * (This will override any user-supplied NOT VALID flag.)
+ * skip validation of FK constraints, and mark them as valid based on the
+ * constraint enforcement flag, since NOT ENFORCED constraints must always
+ * be marked as NOT VALID. (This will override any user-supplied NOT VALID
+ * flag.)
*/
if (skipValidation)
{
@@ -2986,7 +2988,7 @@ transformFKConstraints(CreateStmtContext *cxt,
Constraint *constraint = (Constraint *) lfirst(fkclist);
constraint->skip_validation = true;
- constraint->initially_valid = true;
+ constraint->initially_valid = constraint->is_enforced;
}
}
@@ -3981,7 +3983,8 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
case CONSTR_ATTR_ENFORCED:
if (lastprimarycon == NULL ||
- lastprimarycon->contype != CONSTR_CHECK)
+ (lastprimarycon->contype != CONSTR_CHECK &&
+ lastprimarycon->contype != CONSTR_FOREIGN))
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("misplaced ENFORCED clause"),
@@ -3997,7 +4000,8 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
case CONSTR_ATTR_NOT_ENFORCED:
if (lastprimarycon == NULL ||
- lastprimarycon->contype != CONSTR_CHECK)
+ (lastprimarycon->contype != CONSTR_CHECK &&
+ lastprimarycon->contype != CONSTR_FOREIGN))
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("misplaced NOT ENFORCED clause"),
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index e6721056536..18a14ae186e 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4697,6 +4697,7 @@ RelationGetFKeyList(Relation relation)
info->conoid = constraint->oid;
info->conrelid = constraint->conrelid;
info->confrelid = constraint->confrelid;
+ info->conenforced = constraint->conenforced;
DeconstructFkConstraintRow(htup, &info->nkeys,
info->conkey,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index df331b1c0d9..00fefa9483a 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2495,6 +2495,8 @@ typedef struct ATAlterConstraint
{
NodeTag type;
char *conname; /* Constraint name */
+ bool alterEnforceability; /* changing enforceability properties? */
+ bool is_enforced; /* ENFORCED? */
bool alterDeferrability; /* changing deferrability properties? */
bool deferrable; /* DEFERRABLE? */
bool initdeferred; /* INITIALLY DEFERRED? */
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index d94fddd7cef..b552359915f 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -284,6 +284,9 @@ typedef struct ForeignKeyCacheInfo
/* number of columns in the foreign key */
int nkeys;
+ /* Is enforced ? */
+ bool conenforced;
+
/*
* these arrays each have nkeys valid entries:
*/
diff --git a/src/test/regress/expected/constraints.out b/src/test/regress/expected/constraints.out
index 4f39100fcdf..a719d2f74e9 100644
--- a/src/test/regress/expected/constraints.out
+++ b/src/test/regress/expected/constraints.out
@@ -745,13 +745,9 @@ ERROR: misplaced NOT ENFORCED clause
LINE 1: CREATE TABLE UNIQUE_NOTEN_TBL(i int UNIQUE NOT ENFORCED);
^
ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
-ERROR: FOREIGN KEY constraints cannot be marked ENFORCED
-LINE 1: ...TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
- ^
+ERROR: cannot alter enforceability of constraint "unique_tbl_i_key" of relation "unique_tbl"
ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORCED;
-ERROR: FOREIGN KEY constraints cannot be marked NOT ENFORCED
-LINE 1: ...ABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORC...
- ^
+ERROR: cannot alter enforceability of constraint "unique_tbl_i_key" of relation "unique_tbl"
DROP TABLE unique_tbl;
--
-- EXCLUDE constraints
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 7f678349a8e..78ccf940647 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -1,21 +1,49 @@
--
-- FOREIGN KEY
--
--- MATCH FULL
+-- NOT ENFORCED
--
-- First test, check and cascade
--
CREATE TABLE PKTABLE ( ptest1 int PRIMARY KEY, ptest2 text );
-CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE, ftest2 int );
--- Insert test data into PKTABLE
+CREATE TABLE FKTABLE ( ftest1 int CONSTRAINT fktable_ftest1_fkey REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE NOT ENFORCED,
+ ftest2 int );
+-- Inserting into the foreign key table will not result in an error, even if
+-- there is no matching key in the referenced table.
+INSERT INTO FKTABLE VALUES (1, 2);
+INSERT INTO FKTABLE VALUES (2, 3);
+-- Check FKTABLE
+SELECT * FROM FKTABLE;
+ ftest1 | ftest2
+--------+--------
+ 1 | 2
+ 2 | 3
+(2 rows)
+
+-- Reverting it back to ENFORCED will result in failure because constraint validation will be triggered,
+-- as it was previously in a valid state.
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_fkey"
+DETAIL: Key (ftest1)=(1) is not present in table "pktable".
+-- Insert referenced data that satisfies the constraint, then attempted to
+-- change it.
INSERT INTO PKTABLE VALUES (1, 'Test1');
INSERT INTO PKTABLE VALUES (2, 'Test2');
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+-- Any further inserts will fail due to the enforcement.
+INSERT INTO FKTABLE VALUES (3, 4);
+ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_fkey"
+DETAIL: Key (ftest1)=(3) is not present in table "pktable".
+--
+-- MATCH FULL
+--
+-- First test, check and cascade
+--
+-- Insert test data into PKTABLE
INSERT INTO PKTABLE VALUES (3, 'Test3');
INSERT INTO PKTABLE VALUES (4, 'Test4');
INSERT INTO PKTABLE VALUES (5, 'Test5');
-- Insert successful rows into FK TABLE
-INSERT INTO FKTABLE VALUES (1, 2);
-INSERT INTO FKTABLE VALUES (2, 3);
INSERT INTO FKTABLE VALUES (3, 4);
INSERT INTO FKTABLE VALUES (NULL, 1);
-- Insert a failed row into FK TABLE
@@ -351,6 +379,43 @@ INSERT INTO FKTABLE VALUES (1, NULL);
ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL;
ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_ftest2_fkey"
DETAIL: MATCH FULL does not allow mixing of null and nonnull key values.
+-- Modifying other attributes of a constraint should not affect its enforceability, and vice versa
+ALTER TABLE FKTABLE ADD CONSTRAINT fk_con FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE NOT VALID NOT ENFORCED;
+ALTER TABLE FKTABLE ALTER CONSTRAINT fk_con DEFERRABLE INITIALLY DEFERRED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fk_con';
+ condeferrable | condeferred | conenforced | convalidated
+---------------+-------------+-------------+--------------
+ t | t | f | f
+(1 row)
+
+ALTER TABLE FKTABLE ALTER CONSTRAINT fk_con NOT ENFORCED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fk_con';
+ condeferrable | condeferred | conenforced | convalidated
+---------------+-------------+-------------+--------------
+ t | t | f | f
+(1 row)
+
+-- Enforceability also changes the validate state, as data validation will be
+-- performed during this transformation.
+ALTER TABLE FKTABLE ALTER CONSTRAINT fk_con ENFORCED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fk_con';
+ condeferrable | condeferred | conenforced | convalidated
+---------------+-------------+-------------+--------------
+ t | t | t | t
+(1 row)
+
+-- Can change enforceability and deferrability together
+ALTER TABLE FKTABLE ALTER CONSTRAINT fk_con NOT ENFORCED NOT DEFERRABLE;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fk_con';
+ condeferrable | condeferred | conenforced | convalidated
+---------------+-------------+-------------+--------------
+ f | f | f | f
+(1 row)
+
DROP TABLE FKTABLE;
DROP TABLE PKTABLE;
-- MATCH SIMPLE
@@ -1276,6 +1341,13 @@ INSERT INTO fktable VALUES (0, 20);
ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_fk_fkey"
DETAIL: Key (fk)=(20) is not present in table "pktable".
COMMIT;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT ENFORCED;
+BEGIN;
+-- doesn't match FK, but no error.
+UPDATE pktable SET id = 10 WHERE id = 5;
+-- doesn't match PK, but no error.
+INSERT INTO fktable VALUES (0, 20);
+ROLLBACK;
-- try additional syntax
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE;
-- illegal options
@@ -1289,6 +1361,14 @@ ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT VALID;
ERROR: FOREIGN KEY constraints cannot be marked NOT VALID
LINE 1: ...ER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT VALID;
^
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORCED;
+ERROR: conflicting constraint properties
+LINE 1: ...fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORC...
+ ^
+CREATE TEMP TABLE fktable2 (fk int references pktable ENFORCED NOT ENFORCED);
+ERROR: multiple ENFORCED/NOT ENFORCED clauses not allowed
+LINE 1: ...ABLE fktable2 (fk int references pktable ENFORCED NOT ENFORC...
+ ^
-- test order of firing of FK triggers when several RI-induced changes need to
-- be made to the same row. This was broken by subtransaction-related
-- changes in 8.0.
@@ -1586,10 +1666,14 @@ ALTER TABLE fk_partitioned_fk DROP COLUMN fdrop1;
CREATE TABLE fk_partitioned_fk_1 (fdrop1 int, fdrop2 int, a int, fdrop3 int, b int);
ALTER TABLE fk_partitioned_fk_1 DROP COLUMN fdrop1, DROP COLUMN fdrop2, DROP COLUMN fdrop3;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FROM (0,0) TO (1000,1000);
-ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk;
+ALTER TABLE fk_partitioned_fk ADD CONSTRAINT fk_partitioned_fk_a_b_fkey FOREIGN KEY (a, b)
+ REFERENCES fk_notpartitioned_pk NOT ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
+ALTER TABLE fk_partitioned_fk_2 ADD CONSTRAINT fk_partitioned_fk_a_b_fkey FOREIGN KEY (a, b)
+ REFERENCES fk_notpartitioned_pk NOT ENFORCED;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
CREATE TABLE fk_partitioned_fk_3 (fdrop1 int, fdrop2 int, fdrop3 int, fdrop4 int, b int, a int)
PARTITION BY HASH (a);
ALTER TABLE fk_partitioned_fk_3 DROP COLUMN fdrop1, DROP COLUMN fdrop2,
@@ -1665,6 +1749,37 @@ Indexes:
Referenced by:
TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b)
+-- Check the exsting FK trigger
+CREATE TEMP TABLE tmp_trg_info AS SELECT tgtype, tgrelid::regclass, tgconstraint
+FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+SELECT count(1) FROM tmp_trg_info;
+ count
+-------
+ 14
+(1 row)
+
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+-- No triggers
+SELECT count(1) FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+ count
+-------
+ 0
+(1 row)
+
+-- Changing it back to ENFORCED will recreate the necessary triggers.
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+-- Should be exactly the same number of triggers found as before
+SELECT COUNT(1) FROM pg_trigger pgt JOIN tmp_trg_info tt
+ON (pgt.tgtype = tt.tgtype AND pgt.tgrelid = tt.tgrelid AND pgt.tgconstraint = tt.tgconstraint);
+ count
+-------
+ 14
+(1 row)
+
ALTER TABLE fk_partitioned_fk DROP CONSTRAINT fk_partitioned_fk_a_b_fkey;
-- done.
DROP TABLE fk_notpartitioned_pk, fk_partitioned_fk;
@@ -1962,6 +2077,40 @@ Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
Foreign-key constraints:
TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
+DROP TABLE fk_partitioned_fk_2;
+CREATE TABLE fk_partitioned_fk_2 (b int, a int,
+ FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
+BEGIN;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should be having two constraints
+\d fk_partitioned_fk_2
+ Table "public.fk_partitioned_fk_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ b | integer | | |
+ a | integer | | |
+Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
+Foreign-key constraints:
+ "fk_partitioned_fk_2_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED
+ TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
+
+DROP TABLE fk_partitioned_fk_2;
+ROLLBACK;
+BEGIN;
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have only one constraint
+\d fk_partitioned_fk_2
+ Table "public.fk_partitioned_fk_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ b | integer | | |
+ a | integer | | |
+Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
+Foreign-key constraints:
+ TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED
+
+ROLLBACK;
DROP TABLE fk_partitioned_fk_2;
CREATE TABLE fk_partitioned_fk_4 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE) PARTITION BY RANGE (b, a);
CREATE TABLE fk_partitioned_fk_4_1 PARTITION OF fk_partitioned_fk_4 FOR VALUES FROM (1,1) TO (100,100);
diff --git a/src/test/regress/expected/inherit.out b/src/test/regress/expected/inherit.out
index 4d07d0bd79b..caab1164fdb 100644
--- a/src/test/regress/expected/inherit.out
+++ b/src/test/regress/expected/inherit.out
@@ -1332,6 +1332,13 @@ NOTICE: merging constraint "inh_check_constraint5" with inherited definition
alter table p1 add constraint inh_check_constraint6 check (f1 < 10) not enforced;
alter table p1_c1 add constraint inh_check_constraint6 check (f1 < 10) enforced;
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 invalid state of the child constraint will be ignored here.
+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
create table p1_c2(f1 int constraint inh_check_constraint4 check (f1 < 10)) inherits(p1);
NOTICE: merging column "f1" with inherited definition
NOTICE: merging constraint "inh_check_constraint4" with inherited definition
@@ -1356,39 +1363,47 @@ ERROR: constraint "inh_check_constraint6" conflicts with NOT ENFORCED constrain
select conrelid::regclass::text as relname, conname, conislocal, coninhcount, conenforced, convalidated
from pg_constraint where conname like 'inh\_check\_constraint%'
order by 1, 2;
- relname | conname | conislocal | coninhcount | conenforced | convalidated
----------+-----------------------+------------+-------------+-------------+--------------
- p1 | inh_check_constraint1 | t | 0 | t | t
- p1 | inh_check_constraint2 | t | 0 | t | t
- p1 | inh_check_constraint3 | t | 0 | f | f
- p1 | inh_check_constraint4 | t | 0 | f | f
- p1 | inh_check_constraint5 | t | 0 | f | f
- p1 | inh_check_constraint6 | t | 0 | f | f
- p1 | inh_check_constraint8 | t | 0 | t | t
- p1_c1 | inh_check_constraint1 | t | 1 | t | t
- 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
- p1_c1 | inh_check_constraint5 | t | 1 | t | t
- p1_c1 | inh_check_constraint6 | t | 1 | t | t
- p1_c1 | inh_check_constraint7 | t | 0 | f | f
- p1_c1 | inh_check_constraint8 | f | 1 | t | t
- p1_c2 | inh_check_constraint1 | f | 1 | t | t
- p1_c2 | inh_check_constraint2 | f | 1 | t | t
- p1_c2 | inh_check_constraint3 | f | 1 | f | f
- p1_c2 | inh_check_constraint4 | t | 1 | t | t
- p1_c2 | inh_check_constraint5 | f | 1 | f | f
- p1_c2 | inh_check_constraint6 | f | 1 | f | f
- p1_c2 | inh_check_constraint8 | f | 1 | t | t
- p1_c3 | inh_check_constraint1 | f | 2 | t | t
- p1_c3 | inh_check_constraint2 | f | 2 | t | t
- p1_c3 | inh_check_constraint3 | f | 2 | f | f
- p1_c3 | inh_check_constraint4 | f | 2 | f | f
- p1_c3 | inh_check_constraint5 | f | 2 | t | t
- p1_c3 | inh_check_constraint6 | f | 2 | t | t
- p1_c3 | inh_check_constraint7 | f | 1 | f | f
- p1_c3 | inh_check_constraint8 | f | 2 | t | t
-(30 rows)
+ relname | conname | conislocal | coninhcount | conenforced | convalidated
+---------+------------------------+------------+-------------+-------------+--------------
+ p1 | inh_check_constraint1 | t | 0 | t | t
+ p1 | inh_check_constraint10 | t | 0 | f | f
+ p1 | inh_check_constraint2 | t | 0 | t | t
+ p1 | inh_check_constraint3 | t | 0 | f | f
+ p1 | inh_check_constraint4 | t | 0 | f | f
+ p1 | inh_check_constraint5 | t | 0 | f | f
+ p1 | inh_check_constraint6 | t | 0 | f | f
+ 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_constraint2 | t | 1 | t | t
+ p1_c1 | inh_check_constraint3 | t | 1 | f | f
+ p1_c1 | inh_check_constraint4 | t | 1 | f | f
+ p1_c1 | inh_check_constraint5 | t | 1 | t | t
+ p1_c1 | inh_check_constraint6 | t | 1 | t | t
+ p1_c1 | inh_check_constraint7 | t | 0 | f | f
+ p1_c1 | inh_check_constraint8 | f | 1 | t | t
+ p1_c1 | inh_check_constraint9 | t | 1 | t | f
+ p1_c2 | inh_check_constraint1 | f | 1 | t | t
+ p1_c2 | inh_check_constraint10 | f | 1 | f | f
+ p1_c2 | inh_check_constraint2 | f | 1 | t | t
+ p1_c2 | inh_check_constraint3 | f | 1 | f | f
+ p1_c2 | inh_check_constraint4 | t | 1 | t | t
+ p1_c2 | inh_check_constraint5 | f | 1 | f | f
+ p1_c2 | inh_check_constraint6 | f | 1 | f | f
+ p1_c2 | inh_check_constraint8 | f | 1 | t | t
+ p1_c2 | inh_check_constraint9 | f | 1 | f | f
+ p1_c3 | inh_check_constraint1 | f | 2 | t | t
+ p1_c3 | inh_check_constraint10 | f | 2 | t | t
+ p1_c3 | inh_check_constraint2 | f | 2 | t | t
+ p1_c3 | inh_check_constraint3 | f | 2 | f | f
+ p1_c3 | inh_check_constraint4 | f | 2 | f | f
+ p1_c3 | inh_check_constraint5 | f | 2 | t | t
+ p1_c3 | inh_check_constraint6 | f | 2 | t | t
+ p1_c3 | inh_check_constraint7 | f | 1 | f | f
+ p1_c3 | inh_check_constraint8 | f | 2 | t | t
+ p1_c3 | inh_check_constraint9 | f | 2 | t | t
+(38 rows)
drop table p1 cascade;
NOTICE: drop cascades to 3 other objects
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index 44945b0453a..83de3338292 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -2,23 +2,46 @@
-- FOREIGN KEY
--
--- MATCH FULL
+-- NOT ENFORCED
--
-- First test, check and cascade
--
CREATE TABLE PKTABLE ( ptest1 int PRIMARY KEY, ptest2 text );
-CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE, ftest2 int );
+CREATE TABLE FKTABLE ( ftest1 int CONSTRAINT fktable_ftest1_fkey REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE NOT ENFORCED,
+ ftest2 int );
--- Insert test data into PKTABLE
+-- Inserting into the foreign key table will not result in an error, even if
+-- there is no matching key in the referenced table.
+INSERT INTO FKTABLE VALUES (1, 2);
+INSERT INTO FKTABLE VALUES (2, 3);
+
+-- Check FKTABLE
+SELECT * FROM FKTABLE;
+
+-- Reverting it back to ENFORCED will result in failure because constraint validation will be triggered,
+-- as it was previously in a valid state.
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+
+-- Insert referenced data that satisfies the constraint, then attempted to
+-- change it.
INSERT INTO PKTABLE VALUES (1, 'Test1');
INSERT INTO PKTABLE VALUES (2, 'Test2');
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+
+-- Any further inserts will fail due to the enforcement.
+INSERT INTO FKTABLE VALUES (3, 4);
+
+--
+-- MATCH FULL
+--
+-- First test, check and cascade
+--
+-- Insert test data into PKTABLE
INSERT INTO PKTABLE VALUES (3, 'Test3');
INSERT INTO PKTABLE VALUES (4, 'Test4');
INSERT INTO PKTABLE VALUES (5, 'Test5');
-- Insert successful rows into FK TABLE
-INSERT INTO FKTABLE VALUES (1, 2);
-INSERT INTO FKTABLE VALUES (2, 3);
INSERT INTO FKTABLE VALUES (3, 4);
INSERT INTO FKTABLE VALUES (NULL, 1);
@@ -230,6 +253,27 @@ INSERT INTO FKTABLE VALUES (1, NULL);
ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL;
+-- Modifying other attributes of a constraint should not affect its enforceability, and vice versa
+ALTER TABLE FKTABLE ADD CONSTRAINT fk_con FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE NOT VALID NOT ENFORCED;
+ALTER TABLE FKTABLE ALTER CONSTRAINT fk_con DEFERRABLE INITIALLY DEFERRED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fk_con';
+
+ALTER TABLE FKTABLE ALTER CONSTRAINT fk_con NOT ENFORCED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fk_con';
+
+-- Enforceability also changes the validate state, as data validation will be
+-- performed during this transformation.
+ALTER TABLE FKTABLE ALTER CONSTRAINT fk_con ENFORCED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fk_con';
+
+-- Can change enforceability and deferrability together
+ALTER TABLE FKTABLE ALTER CONSTRAINT fk_con NOT ENFORCED NOT DEFERRABLE;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fk_con';
+
DROP TABLE FKTABLE;
DROP TABLE PKTABLE;
@@ -968,12 +1012,25 @@ INSERT INTO fktable VALUES (0, 20);
COMMIT;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT ENFORCED;
+
+BEGIN;
+
+-- doesn't match FK, but no error.
+UPDATE pktable SET id = 10 WHERE id = 5;
+-- doesn't match PK, but no error.
+INSERT INTO fktable VALUES (0, 20);
+
+ROLLBACK;
+
-- try additional syntax
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE;
-- illegal options
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE INITIALLY DEFERRED;
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NO INHERIT;
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT VALID;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORCED;
+CREATE TEMP TABLE fktable2 (fk int references pktable ENFORCED NOT ENFORCED);
-- test order of firing of FK triggers when several RI-induced changes need to
-- be made to the same row. This was broken by subtransaction-related
@@ -1184,11 +1241,14 @@ ALTER TABLE fk_partitioned_fk DROP COLUMN fdrop1;
CREATE TABLE fk_partitioned_fk_1 (fdrop1 int, fdrop2 int, a int, fdrop3 int, b int);
ALTER TABLE fk_partitioned_fk_1 DROP COLUMN fdrop1, DROP COLUMN fdrop2, DROP COLUMN fdrop3;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FROM (0,0) TO (1000,1000);
-ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk;
+ALTER TABLE fk_partitioned_fk ADD CONSTRAINT fk_partitioned_fk_a_b_fkey FOREIGN KEY (a, b)
+ REFERENCES fk_notpartitioned_pk NOT ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
+ALTER TABLE fk_partitioned_fk_2 ADD CONSTRAINT fk_partitioned_fk_a_b_fkey FOREIGN KEY (a, b)
+ REFERENCES fk_notpartitioned_pk NOT ENFORCED;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
-
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
CREATE TABLE fk_partitioned_fk_3 (fdrop1 int, fdrop2 int, fdrop3 int, fdrop4 int, b int, a int)
PARTITION BY HASH (a);
ALTER TABLE fk_partitioned_fk_3 DROP COLUMN fdrop1, DROP COLUMN fdrop2,
@@ -1234,6 +1294,27 @@ UPDATE fk_notpartitioned_pk SET b = 1502 WHERE a = 1500;
UPDATE fk_notpartitioned_pk SET b = 2504 WHERE a = 2500;
-- check psql behavior
\d fk_notpartitioned_pk
+
+-- Check the exsting FK trigger
+CREATE TEMP TABLE tmp_trg_info AS SELECT tgtype, tgrelid::regclass, tgconstraint
+FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+SELECT count(1) FROM tmp_trg_info;
+
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+-- No triggers
+SELECT count(1) FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+
+-- Changing it back to ENFORCED will recreate the necessary triggers.
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+
+-- Should be exactly the same number of triggers found as before
+SELECT COUNT(1) FROM pg_trigger pgt JOIN tmp_trg_info tt
+ON (pgt.tgtype = tt.tgtype AND pgt.tgrelid = tt.tgrelid AND pgt.tgconstraint = tt.tgconstraint);
+
ALTER TABLE fk_partitioned_fk DROP CONSTRAINT fk_partitioned_fk_a_b_fkey;
-- done.
DROP TABLE fk_notpartitioned_pk, fk_partitioned_fk;
@@ -1441,6 +1522,22 @@ ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN
\d fk_partitioned_fk_2
DROP TABLE fk_partitioned_fk_2;
+CREATE TABLE fk_partitioned_fk_2 (b int, a int,
+ FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
+BEGIN;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should be having two constraints
+\d fk_partitioned_fk_2
+DROP TABLE fk_partitioned_fk_2;
+ROLLBACK;
+BEGIN;
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have only one constraint
+\d fk_partitioned_fk_2
+ROLLBACK;
+DROP TABLE fk_partitioned_fk_2;
+
CREATE TABLE fk_partitioned_fk_4 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE) PARTITION BY RANGE (b, a);
CREATE TABLE fk_partitioned_fk_4_1 PARTITION OF fk_partitioned_fk_4 FOR VALUES FROM (1,1) TO (100,100);
CREATE TABLE fk_partitioned_fk_4_2 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE SET NULL);
diff --git a/src/test/regress/sql/inherit.sql b/src/test/regress/sql/inherit.sql
index 941189761fd..5f0b2617464 100644
--- a/src/test/regress/sql/inherit.sql
+++ b/src/test/regress/sql/inherit.sql
@@ -481,6 +481,13 @@ alter table p1 add constraint inh_check_constraint5 check (f1 < 10) not enforced
alter table p1 add constraint inh_check_constraint6 check (f1 < 10) not enforced;
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 invalid state of the child constraint will be ignored here.
+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;
+
create table p1_c2(f1 int constraint inh_check_constraint4 check (f1 < 10)) inherits(p1);
-- but reverse is not allowed
--
2.43.5
v19-0006-Merge-the-parent-and-child-constraints-with-diff.patchapplication/x-patch; name=v19-0006-Merge-the-parent-and-child-constraints-with-diff.patchDownload
From 65c76bdb041e60baabe632349833b97faf619a91 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 10 Feb 2025 10:59:28 +0530
Subject: [PATCH v19 6/6] Merge the parent and child constraints with differing
enforcibility.
If an ENFORCED parent constraint is attached to a NOT ENFORCED child
constraint, the child constraint will be made ENFORCED, with
validation applied if the parent constraint is validated as well.
Otherwise, a new ENFORCED constraint (with validation, if the parent
constraint is validated) would need to be created on the child table,
which would be unnecessary if a similar constraint already exists and
can be attached.
On the other hand, having a NOT ENFORCED parent constraint with an
ENFORCED child constraint does not cause any issues, and no changes
are required.
----
NOTE: This patch is intended to reduce the diff noise from the main
patch and is not meant to be committed separately. It should be
squashed with the main patch that adds ENFORCED/NOT ENFORCED.
----
---
src/backend/commands/tablecmds.c | 164 ++++++++++++++++++++--
src/test/regress/expected/foreign_key.out | 74 ++++++++--
src/test/regress/sql/foreign_key.sql | 37 ++++-
3 files changed, 250 insertions(+), 25 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 0e126300878..ba6ce9f79b2 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -11543,7 +11543,6 @@ tryAttachPartitionForeignKey(List **wqueue,
if (OidIsValid(partConstr->conparentid) ||
partConstr->condeferrable != parentConstr->condeferrable ||
partConstr->condeferred != parentConstr->condeferred ||
- partConstr->conenforced != parentConstr->conenforced ||
partConstr->confupdtype != parentConstr->confupdtype ||
partConstr->confdeltype != parentConstr->confdeltype ||
partConstr->confmatchtype != parentConstr->confmatchtype)
@@ -11588,6 +11587,8 @@ AttachPartitionForeignKey(List **wqueue,
Oid partConstrFrelid;
Oid partConstrRelid;
bool parentConstrIsEnforced;
+ bool partConstrIsEnforced;
+ bool partConstrParentIsSet;
/* Fetch the parent constraint tuple */
parentConstrTup = SearchSysCache1(CONSTROID,
@@ -11603,13 +11604,47 @@ AttachPartitionForeignKey(List **wqueue,
if (!HeapTupleIsValid(partcontup))
elog(ERROR, "cache lookup failed for constraint %u", partConstrOid);
partConstr = (Form_pg_constraint) GETSTRUCT(partcontup);
+ partConstrIsEnforced = partConstr->conenforced;
partConstrFrelid = partConstr->confrelid;
partConstrRelid = partConstr->conrelid;
+ /*
+ * The case where the parent constraint is NOT ENFORCED and the child
+ * constraint is ENFORCED is acceptable because the not enforced parent
+ * constraint lacks triggers, eliminating any redundancy issues with the
+ * enforced child constraint. In this scenario, the child constraint
+ * remains enforced, and its trigger is retained, ensuring that
+ * referential integrity checks for the child continue as before, even
+ * with the parent constraint not enforced. The relationship between the
+ * two constraints is preserved by setting the parent constraint, which
+ * allows us to locate the child constraint. This becomes important if the
+ * parent constraint is later changed to enforced, at which point the
+ * necessary trigger will be created for the parent, and any redundancy
+ * from these triggers will be appropriately handled.
+ */
+ if (!parentConstrIsEnforced && partConstrIsEnforced)
+ {
+ ReleaseSysCache(partcontup);
+ ReleaseSysCache(parentConstrTup);
+
+ ConstraintSetParentConstraint(partConstrOid, parentConstrOid,
+ RelationGetRelid(partition));
+ CommandCounterIncrement();
+
+ return;
+ }
+
/*
* If the referenced table is partitioned, then the partition we're
* attaching now has extra pg_constraint rows and action triggers that are
* no longer needed. Remove those.
+ *
+ * Note that this must be done beforehand, particularly in situations
+ * where we might decide to change the constraint to an ENFORCED state
+ * which will create the required triggers and add the child constraint to
+ * the validation queue. To avoid generating unnecessary triggers and
+ * adding them to the validation queue, it is crucial to eliminate any
+ * redundant constraints beforehand.
*/
if (get_rel_relkind(partConstrFrelid) == RELKIND_PARTITIONED_TABLE)
{
@@ -11628,6 +11663,53 @@ AttachPartitionForeignKey(List **wqueue,
*/
queueValidation = parentConstr->convalidated && !partConstr->convalidated;
+ /*
+ * The case where the parent constraint is ENFORCED and the child
+ * constraint is NOT ENFORCED is not acceptable, as it would violate
+ * referential integrity. In such cases, the child constraint will first
+ * be enforced before merging it with the enforced parent constraint.
+ * Subsequently, removing action triggers, setting up constraint triggers,
+ * and handling check triggers for the parent will be managed in the usual
+ * manner, similar to how two enforced constraints are merged.
+ */
+ if (parentConstrIsEnforced && !partConstrIsEnforced)
+ {
+ ATAlterConstraint *cmdcon = makeNode(ATAlterConstraint);
+ Relation conrel;
+
+ cmdcon->conname = NameStr(partConstr->conname);
+ cmdcon->deferrable = partConstr->condeferrable;
+ cmdcon->initdeferred = partConstr->condeferred;
+ cmdcon->alterEnforceability = true;
+ cmdcon->is_enforced = true;
+
+ conrel = table_open(ConstraintRelationId, RowExclusiveLock);
+
+ ATExecAlterConstrEnforceability(wqueue, cmdcon, conrel, trigrel,
+ partConstr->conrelid,
+ partConstr->confrelid,
+ partcontup, AccessExclusiveLock,
+ InvalidOid, InvalidOid, InvalidOid,
+ InvalidOid);
+
+ table_close(conrel, RowExclusiveLock);
+
+ CommandCounterIncrement();
+
+ /*
+ * No further validation is needed, as changing the constraint to
+ * enforced will implicitly trigger the same validation.
+ */
+ queueValidation = false;
+ }
+
+ /*
+ * The constraint parent shouldn't be set beforehand, or if it's already
+ * set, it should be the specified parent.
+ */
+ partConstrParentIsSet = OidIsValid(partConstr->conparentid);
+ Assert(!partConstrParentIsSet || partConstr->conparentid == parentConstrOid);
+
ReleaseSysCache(partcontup);
ReleaseSysCache(parentConstrTup);
@@ -11640,8 +11722,10 @@ AttachPartitionForeignKey(List **wqueue,
DropForeignKeyConstraintTriggers(trigrel, partConstrOid, partConstrFrelid,
partConstrRelid);
- ConstraintSetParentConstraint(partConstrOid, parentConstrOid,
- RelationGetRelid(partition));
+ /* Skip if the parent is already set */
+ if (!partConstrParentIsSet)
+ ConstraintSetParentConstraint(partConstrOid, parentConstrOid,
+ RelationGetRelid(partition));
/*
* Like the constraint, attach partition's "check" triggers to the
@@ -12241,6 +12325,17 @@ ATExecAlterConstrEnforceability(List **wqueue, ATAlterConstraint *cmdcon,
/* Drop all the triggers */
DropForeignKeyConstraintTriggers(tgrel, conoid, InvalidOid, InvalidOid);
+
+ /*
+ * If the referenced table is partitioned, the child constraint we're
+ * changing to NOT ENFORCED may have additional pg_constraint rows and
+ * action triggers that remain untouched while this child constraint
+ * is attached to the NOT ENFORCED parent. These must now be removed.
+ * For more details, see AttachPartitionForeignKey().
+ */
+ if (OidIsValid(currcon->conparentid) &&
+ get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE)
+ RemoveInheritedConstraint(conrel, tgrel, currcon->conrelid, conoid);
}
else if (changed) /* Create triggers */
{
@@ -12566,13 +12661,40 @@ AlterConstrEnforceabilityRecurse(List **wqueue, ATAlterConstraint *cmdcon,
true, NULL, 1, &pkey);
while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
- ATExecAlterConstrEnforceability(wqueue, cmdcon, conrel, tgrel, fkrelid,
- pkrelid, childtup, lockmode,
- ReferencedParentDelTrigger,
- ReferencedParentUpdTrigger,
- ReferencingParentInsTrigger,
- ReferencingParentUpdTrigger);
+ {
+ Form_pg_constraint childcon;
+ childcon = (Form_pg_constraint) GETSTRUCT(childtup);
+
+ /*
+ * When the parent constraint is modified to be ENFORCED, and the
+ * child constraint is attached to the parent constraint (which is
+ * already ENFORCED), some constraints and action triggers on the
+ * child table may become redundant and need to be removed.
+ */
+ if (cmdcon->is_enforced && childcon->conenforced)
+ {
+ if (currcon->confrelid == pkrelid)
+ {
+ Relation rel = table_open(childcon->conrelid, lockmode);
+
+ AttachPartitionForeignKey(wqueue, rel, childcon->oid,
+ conoid,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger,
+ tgrel);
+
+ table_close(rel, NoLock);
+ }
+ }
+ else
+ ATExecAlterConstrEnforceability(wqueue, cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, childtup, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
+ }
systable_endscan(pscan);
}
@@ -20746,7 +20868,9 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
{
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
+ HeapTuple parentContup;
Form_pg_constraint conform;
+ Oid parentConstrIsEnforced;
contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
if (!HeapTupleIsValid(contup))
@@ -20765,12 +20889,34 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
continue;
}
+ /* Get the enforcibility of the parent constraint */
+ parentContup = SearchSysCache1(CONSTROID,
+ ObjectIdGetDatum(conform->conparentid));
+ if (!HeapTupleIsValid(parentContup))
+ elog(ERROR, "cache lookup failed for constraint %u",
+ conform->conparentid);
+ parentConstrIsEnforced =
+ ((Form_pg_constraint) GETSTRUCT(parentContup))->conenforced;
+ ReleaseSysCache(parentContup);
+
/*
* The constraint on this table must be marked no longer a child of
* the parent's constraint, as do its check triggers.
*/
ConstraintSetParentConstraint(fk->conoid, InvalidOid, InvalidOid);
+ /*
+ * Unsetting the parent is sufficient when the parent constraint is
+ * NOT ENFORCED and the child constraint is ENFORCED, as we link them
+ * by setting the constraint parent, while leaving the rest unchanged.
+ * For more details, see AttachPartitionForeignKey().
+ */
+ if (!parentConstrIsEnforced && fk->conenforced)
+ {
+ ReleaseSysCache(contup);
+ continue;
+ }
+
/*
* Also, look up the partition's "check" triggers corresponding to the
* ENFORCED constraint being detached and detach them from the parent
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 78ccf940647..84eddbd9e2c 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -1670,8 +1670,6 @@ ALTER TABLE fk_partitioned_fk ADD CONSTRAINT fk_partitioned_fk_a_b_fkey FOREIGN
REFERENCES fk_notpartitioned_pk NOT ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
-ALTER TABLE fk_partitioned_fk_2 ADD CONSTRAINT fk_partitioned_fk_a_b_fkey FOREIGN KEY (a, b)
- REFERENCES fk_notpartitioned_pk NOT ENFORCED;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
CREATE TABLE fk_partitioned_fk_3 (fdrop1 int, fdrop2 int, fdrop3 int, fdrop4 int, b int, a int)
@@ -1680,8 +1678,65 @@ ALTER TABLE fk_partitioned_fk_3 DROP COLUMN fdrop1, DROP COLUMN fdrop2,
DROP COLUMN fdrop3, DROP COLUMN fdrop4;
CREATE TABLE fk_partitioned_fk_3_0 PARTITION OF fk_partitioned_fk_3 FOR VALUES WITH (MODULUS 5, REMAINDER 0);
CREATE TABLE fk_partitioned_fk_3_1 PARTITION OF fk_partitioned_fk_3 FOR VALUES WITH (MODULUS 5, REMAINDER 1);
+-- Merge the not-enforced parent constraint with the enforced and not-enforced child constraints
+ALTER TABLE fk_partitioned_fk_3_0 ADD CONSTRAINT fk_partitioned_fk_3_0_a_b_fkey FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ENFORCED;
+ALTER TABLE fk_partitioned_fk_3_1 ADD CONSTRAINT fk_partitioned_fk_3_1_a_b_fkey FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk_3 ADD CONSTRAINT fk_partitioned_fk_3_a_b_fkey FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+-- Merge the non-enforced parent constraint with both the enforced and
+-- non-enforced child constraints, where the referenced table is partitioned.
+CREATE TABLE fk_partitioned_pk (a int, b int, PRIMARY KEY (a, b)) PARTITION BY RANGE (a, b);
+CREATE TABLE fk_partitioned_pk_1 PARTITION OF fk_partitioned_pk FOR VALUES FROM (0,0) TO (1000,1000);
+-- Merge the enforced parent constraint with the enforced and not-enforced child constraints.
+ALTER TABLE fk_partitioned_fk_3_0 ADD CONSTRAINT fk_partitioned_fk_3_0_a_b_fkey1 FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk ENFORCED;
+ALTER TABLE fk_partitioned_fk_3_1 ADD CONSTRAINT fk_partitioned_fk_3_1_a_b_fkey1 FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk_3 ADD CONSTRAINT fk_partitioned_fk_3_a_b_fkey1 FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk ENFORCED;
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+ conname | conenforced | convalidated | conrelid | confrelid
+---------------------------------+-------------+--------------+-----------------------+----------------------
+ fk_partitioned_fk_3_0_a_b_fkey | t | t | fk_partitioned_fk_3_0 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey | f | f | fk_partitioned_fk_3_1 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_a_b_fkey | f | f | fk_partitioned_fk_3 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_0_a_b_fkey1 | t | t | fk_partitioned_fk_3_0 | fk_partitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey1 | t | t | fk_partitioned_fk_3_1 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey1 | t | t | fk_partitioned_fk_3 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey2 | t | t | fk_partitioned_fk_3 | fk_partitioned_pk_1
+(7 rows)
+
+ALTER TABLE fk_partitioned_fk_3 DETACH PARTITION fk_partitioned_fk_3_0;
+ALTER TABLE fk_partitioned_fk_3 ATTACH PARTITION fk_partitioned_fk_3_0 FOR VALUES WITH (MODULUS 5, REMAINDER 0);
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+ conname | conenforced | convalidated | conrelid | confrelid
+---------------------------------+-------------+--------------+-----------------------+----------------------
+ fk_partitioned_fk_3_0_a_b_fkey | t | t | fk_partitioned_fk_3_0 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey | f | f | fk_partitioned_fk_3_1 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_a_b_fkey | f | f | fk_partitioned_fk_3 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_0_a_b_fkey1 | t | t | fk_partitioned_fk_3_0 | fk_partitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey1 | t | t | fk_partitioned_fk_3_1 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey1 | t | t | fk_partitioned_fk_3 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey2 | t | t | fk_partitioned_fk_3 | fk_partitioned_pk_1
+(7 rows)
+
+ALTER TABLE fk_partitioned_fk_3 ALTER CONSTRAINT fk_partitioned_fk_3_a_b_fkey1 NOT ENFORCED;
+-- Merging an enforced parent constraint (validated) with a not-enforced child
+-- constraint will implicitly change the child constraint to enforced and apply
+-- the validation as well.
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_3
FOR VALUES FROM (2000,2000) TO (3000,3000);
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+ conname | conenforced | convalidated | conrelid | confrelid
+---------------------------------+-------------+--------------+-----------------------+----------------------
+ fk_partitioned_fk_3_0_a_b_fkey | t | t | fk_partitioned_fk_3_0 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey | t | t | fk_partitioned_fk_3_1 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_a_b_fkey | t | t | fk_partitioned_fk_3 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_0_a_b_fkey1 | f | f | fk_partitioned_fk_3_0 | fk_partitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey1 | f | f | fk_partitioned_fk_3_1 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey1 | f | f | fk_partitioned_fk_3 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey2 | f | f | fk_partitioned_fk_3 | fk_partitioned_pk_1
+(7 rows)
+
-- Creating a foreign key with ONLY on a partitioned table referencing
-- a non-partitioned table fails.
ALTER TABLE ONLY fk_partitioned_fk ADD FOREIGN KEY (a, b)
@@ -1702,16 +1757,16 @@ INSERT INTO fk_partitioned_fk_2 (a,b) VALUES (1500, 1501);
ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
DETAIL: Key (a, b)=(1500, 1501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (2500, 2502);
-ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_3_1_a_b_fkey"
DETAIL: Key (a, b)=(2500, 2502) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk_3 (a,b) VALUES (2500, 2502);
-ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_3_1_a_b_fkey"
DETAIL: Key (a, b)=(2500, 2502) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (2501, 2503);
-ERROR: insert or update on table "fk_partitioned_fk_3_0" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_0" violates foreign key constraint "fk_partitioned_fk_3_0_a_b_fkey"
DETAIL: Key (a, b)=(2501, 2503) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk_3 (a,b) VALUES (2501, 2503);
-ERROR: insert or update on table "fk_partitioned_fk_3_0" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_0" violates foreign key constraint "fk_partitioned_fk_3_0_a_b_fkey"
DETAIL: Key (a, b)=(2501, 2503) is not present in table "fk_notpartitioned_pk".
-- but if we insert the values that make them valid, then they work
INSERT INTO fk_notpartitioned_pk VALUES (500, 501), (1500, 1501),
@@ -1722,7 +1777,7 @@ INSERT INTO fk_partitioned_fk (a,b) VALUES (2500, 2502);
INSERT INTO fk_partitioned_fk (a,b) VALUES (2501, 2503);
-- this update fails because there is no referenced row
UPDATE fk_partitioned_fk SET a = a + 1 WHERE a = 2501;
-ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_3_1_a_b_fkey"
DETAIL: Key (a, b)=(2502, 2503) is not present in table "fk_notpartitioned_pk".
-- but we can fix it thusly:
INSERT INTO fk_notpartitioned_pk (a,b) VALUES (2502, 2503);
@@ -1865,8 +1920,6 @@ WHERE conrelid::regclass::text like 'fk_partitioned_fk%' ORDER BY oid::regclass:
DROP TABLE fk_partitioned_fk, fk_notpartitioned_pk;
-- NOT VALID foreign key on a non-partitioned table referencing a partitioned table
-CREATE TABLE fk_partitioned_pk (a int, b int, PRIMARY KEY (a, b)) PARTITION BY RANGE (a, b);
-CREATE TABLE fk_partitioned_pk_1 PARTITION OF fk_partitioned_pk FOR VALUES FROM (0,0) TO (1000,1000);
CREATE TABLE fk_notpartitioned_fk (b int, a int);
ALTER TABLE fk_notpartitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk NOT VALID;
-- Constraint will be invalid.
@@ -2082,7 +2135,7 @@ CREATE TABLE fk_partitioned_fk_2 (b int, a int,
FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
BEGIN;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
--- should be having two constraints
+-- should only have one constraint
\d fk_partitioned_fk_2
Table "public.fk_partitioned_fk_2"
Column | Type | Collation | Nullable | Default
@@ -2091,7 +2144,6 @@ ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN
a | integer | | |
Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
Foreign-key constraints:
- "fk_partitioned_fk_2_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED
TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
DROP TABLE fk_partitioned_fk_2;
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index 83de3338292..b4bc2a5531e 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -1245,8 +1245,6 @@ ALTER TABLE fk_partitioned_fk ADD CONSTRAINT fk_partitioned_fk_a_b_fkey FOREIGN
REFERENCES fk_notpartitioned_pk NOT ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
-ALTER TABLE fk_partitioned_fk_2 ADD CONSTRAINT fk_partitioned_fk_a_b_fkey FOREIGN KEY (a, b)
- REFERENCES fk_notpartitioned_pk NOT ENFORCED;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
CREATE TABLE fk_partitioned_fk_3 (fdrop1 int, fdrop2 int, fdrop3 int, fdrop4 int, b int, a int)
@@ -1255,9 +1253,40 @@ ALTER TABLE fk_partitioned_fk_3 DROP COLUMN fdrop1, DROP COLUMN fdrop2,
DROP COLUMN fdrop3, DROP COLUMN fdrop4;
CREATE TABLE fk_partitioned_fk_3_0 PARTITION OF fk_partitioned_fk_3 FOR VALUES WITH (MODULUS 5, REMAINDER 0);
CREATE TABLE fk_partitioned_fk_3_1 PARTITION OF fk_partitioned_fk_3 FOR VALUES WITH (MODULUS 5, REMAINDER 1);
+-- Merge the not-enforced parent constraint with the enforced and not-enforced child constraints
+ALTER TABLE fk_partitioned_fk_3_0 ADD CONSTRAINT fk_partitioned_fk_3_0_a_b_fkey FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ENFORCED;
+ALTER TABLE fk_partitioned_fk_3_1 ADD CONSTRAINT fk_partitioned_fk_3_1_a_b_fkey FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk_3 ADD CONSTRAINT fk_partitioned_fk_3_a_b_fkey FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+
+-- Merge the non-enforced parent constraint with both the enforced and
+-- non-enforced child constraints, where the referenced table is partitioned.
+CREATE TABLE fk_partitioned_pk (a int, b int, PRIMARY KEY (a, b)) PARTITION BY RANGE (a, b);
+CREATE TABLE fk_partitioned_pk_1 PARTITION OF fk_partitioned_pk FOR VALUES FROM (0,0) TO (1000,1000);
+-- Merge the enforced parent constraint with the enforced and not-enforced child constraints.
+ALTER TABLE fk_partitioned_fk_3_0 ADD CONSTRAINT fk_partitioned_fk_3_0_a_b_fkey1 FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk ENFORCED;
+ALTER TABLE fk_partitioned_fk_3_1 ADD CONSTRAINT fk_partitioned_fk_3_1_a_b_fkey1 FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk_3 ADD CONSTRAINT fk_partitioned_fk_3_a_b_fkey1 FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk ENFORCED;
+
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+
+ALTER TABLE fk_partitioned_fk_3 DETACH PARTITION fk_partitioned_fk_3_0;
+ALTER TABLE fk_partitioned_fk_3 ATTACH PARTITION fk_partitioned_fk_3_0 FOR VALUES WITH (MODULUS 5, REMAINDER 0);
+
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+
+ALTER TABLE fk_partitioned_fk_3 ALTER CONSTRAINT fk_partitioned_fk_3_a_b_fkey1 NOT ENFORCED;
+
+-- Merging an enforced parent constraint (validated) with a not-enforced child
+-- constraint will implicitly change the child constraint to enforced and apply
+-- the validation as well.
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_3
FOR VALUES FROM (2000,2000) TO (3000,3000);
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+
-- Creating a foreign key with ONLY on a partitioned table referencing
-- a non-partitioned table fails.
ALTER TABLE ONLY fk_partitioned_fk ADD FOREIGN KEY (a, b)
@@ -1384,8 +1413,6 @@ WHERE conrelid::regclass::text like 'fk_partitioned_fk%' ORDER BY oid::regclass:
DROP TABLE fk_partitioned_fk, fk_notpartitioned_pk;
-- NOT VALID foreign key on a non-partitioned table referencing a partitioned table
-CREATE TABLE fk_partitioned_pk (a int, b int, PRIMARY KEY (a, b)) PARTITION BY RANGE (a, b);
-CREATE TABLE fk_partitioned_pk_1 PARTITION OF fk_partitioned_pk FOR VALUES FROM (0,0) TO (1000,1000);
CREATE TABLE fk_notpartitioned_fk (b int, a int);
ALTER TABLE fk_notpartitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk NOT VALID;
@@ -1526,7 +1553,7 @@ CREATE TABLE fk_partitioned_fk_2 (b int, a int,
FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
BEGIN;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
--- should be having two constraints
+-- should only have one constraint
\d fk_partitioned_fk_2
DROP TABLE fk_partitioned_fk_2;
ROLLBACK;
--
2.43.5
On 25.03.25 17:48, Peter Eisentraut wrote:
I have committed patches 0001 through 0003. I made some small changes:
I will work through the remaining patches. It looks good to me so far.
For the time being, here are the remaining patches rebased.
I think you could squash these together at this point. This is
especially useful since 0003 overwrites part of the changes in 0002, so
it's better to see them all together.
Some details:
In CloneFkReferenced() and also in DetachPartitionFinalize(), you have
this change:
- fkconstraint->initially_valid = true;
+ fkconstraint->initially_valid = constrForm->convalidated;
I'm having a hard time understanding this. Is this an pre-existing
problem? What does this change do?
Most of the other stuff is mechanical and fits into existing structures,
so it seems ok.
Small cosmetic suggestion: write count(*) instead of count(1). This
fits better with existing practices.
The merge rules for inheritance and partitioning are still confusing. I
don't understand the behavior inh_check_constraint10 in the inherit.sql
test:
+-- the invalid state of the child constraint will be ignored here.
+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;
Why is that correct? At least we should explain it here, or somewhere.
I'm also confused about the changes of the constraint names in the
foreign_key.sql test:
-ERROR: insert or update on table "fk_partitioned_fk_2" violates
foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_2" violates
foreign key constraint "fk_partitioned_fk_2_a_b_fkey"
And then patch 0003 changes it again. This seems pretty random. We
should make sure that tests don't contain unrelated changes like that.
(Or at least it's not clear why they are related.)
There is also in 0002
+-- should be having two constraints
and then in 0003:
--- should be having two constraints
+-- should only have one constraint
So another reason for squashing these together, so we don't have
confusing intermediate states.
That said, is there a simpler way? Patch 0003 appears to add a lot of
complexity. Could we make this simpler by saying, if you have otherwise
matching constraints with different enforceability, make this an error.
Then users can themselves adjust the enforceability how they want to
make it match.
Attachments:
v18.1-0001-Remove-hastriggers-flag-check-before-fetching-.patchtext/plain; charset=UTF-8; name=v18.1-0001-Remove-hastriggers-flag-check-before-fetching-.patchDownload
From d9183039dffefd8f8443608e0e0cb85ae6233bdc Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 7 Oct 2024 12:40:07 +0530
Subject: [PATCH v18.1 1/3] Remove hastriggers flag check before fetching FK
constraints.
With NOT ENFORCED, foreign key constraints will be created without
triggers. Therefore, the criteria for fetching foreign keys based on
the presence of triggers no longer apply.
----
NOTE: This patch is intended to reduce the diff noise from the main
patch and is not meant to be committed separately. It should be
squashed with the main patch that adds ENFORCED/NOT ENFORCED.
----
---
src/backend/utils/cache/relcache.c | 5 -
src/bin/pg_dump/pg_dump.c | 8 +-
src/bin/psql/describe.c | 216 ++++++++++++++---------------
3 files changed, 103 insertions(+), 126 deletions(-)
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 9f54a9e72b7..e6721056536 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4666,11 +4666,6 @@ RelationGetFKeyList(Relation relation)
if (relation->rd_fkeyvalid)
return relation->rd_fkeylist;
- /* Fast path: non-partitioned tables without triggers can't have FKs */
- if (!relation->rd_rel->relhastriggers &&
- relation->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
- return NIL;
-
/*
* We build the list we intend to return (in the caller's context) while
* doing the scan. After successfully completing the scan, we copy that
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index e41e645f649..dc9797aa251 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -8016,13 +8016,7 @@ getConstraints(Archive *fout, TableInfo tblinfo[], int numTables)
{
TableInfo *tinfo = &tblinfo[i];
- /*
- * For partitioned tables, foreign keys have no triggers so they must
- * be included anyway in case some foreign keys are defined.
- */
- if ((!tinfo->hastriggers &&
- tinfo->relkind != RELKIND_PARTITIONED_TABLE) ||
- !(tinfo->dobj.dump & DUMP_COMPONENT_DEFINITION))
+ if (!(tinfo->dobj.dump & DUMP_COMPONENT_DEFINITION))
continue;
/* OK, we need info for this table */
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index bf565afcc4e..e4a4c5071dd 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2550,136 +2550,124 @@ describeOneTableDetails(const char *schemaname,
PQclear(result);
}
- /*
- * Print foreign-key constraints (there are none if no triggers,
- * except if the table is partitioned, in which case the triggers
- * appear in the partitions)
- */
- if (tableinfo.hastriggers ||
- tableinfo.relkind == RELKIND_PARTITIONED_TABLE)
+ /* Print foreign-key constraints */
+ if (pset.sversion >= 120000 &&
+ (tableinfo.ispartition || tableinfo.relkind == RELKIND_PARTITIONED_TABLE))
{
- if (pset.sversion >= 120000 &&
- (tableinfo.ispartition || tableinfo.relkind == RELKIND_PARTITIONED_TABLE))
- {
- /*
- * Put the constraints defined in this table first, followed
- * by the constraints defined in ancestor partitioned tables.
- */
- printfPQExpBuffer(&buf,
- "SELECT conrelid = '%s'::pg_catalog.regclass AS sametable,\n"
- " conname,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef,\n"
- " conrelid::pg_catalog.regclass AS ontable\n"
- " FROM pg_catalog.pg_constraint,\n"
- " pg_catalog.pg_partition_ancestors('%s')\n"
- " WHERE conrelid = relid AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
- "ORDER BY sametable DESC, conname;",
- oid, oid);
- }
- else
- {
- printfPQExpBuffer(&buf,
- "SELECT true as sametable, conname,\n"
- " pg_catalog.pg_get_constraintdef(r.oid, true) as condef,\n"
- " conrelid::pg_catalog.regclass AS ontable\n"
- "FROM pg_catalog.pg_constraint r\n"
- "WHERE r.conrelid = '%s' AND r.contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n",
- oid);
-
- if (pset.sversion >= 120000)
- appendPQExpBufferStr(&buf, " AND conparentid = 0\n");
- appendPQExpBufferStr(&buf, "ORDER BY conname");
- }
+ /*
+ * Put the constraints defined in this table first, followed
+ * by the constraints defined in ancestor partitioned tables.
+ */
+ printfPQExpBuffer(&buf,
+ "SELECT conrelid = '%s'::pg_catalog.regclass AS sametable,\n"
+ " conname,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef,\n"
+ " conrelid::pg_catalog.regclass AS ontable\n"
+ " FROM pg_catalog.pg_constraint,\n"
+ " pg_catalog.pg_partition_ancestors('%s')\n"
+ " WHERE conrelid = relid AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
+ "ORDER BY sametable DESC, conname;",
+ oid, oid);
+ }
+ else
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT true as sametable, conname,\n"
+ " pg_catalog.pg_get_constraintdef(r.oid, true) as condef,\n"
+ " conrelid::pg_catalog.regclass AS ontable\n"
+ "FROM pg_catalog.pg_constraint r\n"
+ "WHERE r.conrelid = '%s' AND r.contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n",
+ oid);
- result = PSQLexec(buf.data);
- if (!result)
- goto error_return;
- else
- tuples = PQntuples(result);
+ if (pset.sversion >= 120000)
+ appendPQExpBufferStr(&buf, " AND conparentid = 0\n");
+ appendPQExpBufferStr(&buf, "ORDER BY conname");
+ }
- if (tuples > 0)
- {
- int i_sametable = PQfnumber(result, "sametable"),
- i_conname = PQfnumber(result, "conname"),
- i_condef = PQfnumber(result, "condef"),
- i_ontable = PQfnumber(result, "ontable");
+ result = PSQLexec(buf.data);
+ if (!result)
+ goto error_return;
+ else
+ tuples = PQntuples(result);
- printTableAddFooter(&cont, _("Foreign-key constraints:"));
- for (i = 0; i < tuples; i++)
- {
- /*
- * Print untranslated constraint name and definition. Use
- * a "TABLE tab" prefix when the constraint is defined in
- * a parent partitioned table.
- */
- if (strcmp(PQgetvalue(result, i, i_sametable), "f") == 0)
- printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
- PQgetvalue(result, i, i_ontable),
- PQgetvalue(result, i, i_conname),
- PQgetvalue(result, i, i_condef));
- else
- printfPQExpBuffer(&buf, " \"%s\" %s",
- PQgetvalue(result, i, i_conname),
- PQgetvalue(result, i, i_condef));
+ if (tuples > 0)
+ {
+ int i_sametable = PQfnumber(result, "sametable"),
+ i_conname = PQfnumber(result, "conname"),
+ i_condef = PQfnumber(result, "condef"),
+ i_ontable = PQfnumber(result, "ontable");
- printTableAddFooter(&cont, buf.data);
- }
+ printTableAddFooter(&cont, _("Foreign-key constraints:"));
+ for (i = 0; i < tuples; i++)
+ {
+ /*
+ * Print untranslated constraint name and definition. Use
+ * a "TABLE tab" prefix when the constraint is defined in
+ * a parent partitioned table.
+ */
+ if (strcmp(PQgetvalue(result, i, i_sametable), "f") == 0)
+ printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
+ PQgetvalue(result, i, i_ontable),
+ PQgetvalue(result, i, i_conname),
+ PQgetvalue(result, i, i_condef));
+ else
+ printfPQExpBuffer(&buf, " \"%s\" %s",
+ PQgetvalue(result, i, i_conname),
+ PQgetvalue(result, i, i_condef));
+
+ printTableAddFooter(&cont, buf.data);
}
- PQclear(result);
}
+ PQclear(result);
/* print incoming foreign-key references */
- if (tableinfo.hastriggers ||
- tableinfo.relkind == RELKIND_PARTITIONED_TABLE)
+ if (pset.sversion >= 120000)
{
- if (pset.sversion >= 120000)
- {
- printfPQExpBuffer(&buf,
- "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
- " FROM pg_catalog.pg_constraint c\n"
- " WHERE confrelid IN (SELECT pg_catalog.pg_partition_ancestors('%s')\n"
- " UNION ALL VALUES ('%s'::pg_catalog.regclass))\n"
- " AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
- "ORDER BY conname;",
- oid, oid);
- }
- else
- {
- printfPQExpBuffer(&buf,
- "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
- " FROM pg_catalog.pg_constraint\n"
- " WHERE confrelid = %s AND contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n"
- "ORDER BY conname;",
- oid);
- }
+ printfPQExpBuffer(&buf,
+ "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
+ " FROM pg_catalog.pg_constraint c\n"
+ " WHERE confrelid IN (SELECT pg_catalog.pg_partition_ancestors('%s')\n"
+ " UNION ALL VALUES ('%s'::pg_catalog.regclass))\n"
+ " AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
+ "ORDER BY conname;",
+ oid, oid);
+ }
+ else
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
+ " FROM pg_catalog.pg_constraint\n"
+ " WHERE confrelid = %s AND contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n"
+ "ORDER BY conname;",
+ oid);
+ }
- result = PSQLexec(buf.data);
- if (!result)
- goto error_return;
- else
- tuples = PQntuples(result);
+ result = PSQLexec(buf.data);
+ if (!result)
+ goto error_return;
+ else
+ tuples = PQntuples(result);
- if (tuples > 0)
- {
- int i_conname = PQfnumber(result, "conname"),
- i_ontable = PQfnumber(result, "ontable"),
- i_condef = PQfnumber(result, "condef");
+ if (tuples > 0)
+ {
+ int i_conname = PQfnumber(result, "conname"),
+ i_ontable = PQfnumber(result, "ontable"),
+ i_condef = PQfnumber(result, "condef");
- printTableAddFooter(&cont, _("Referenced by:"));
- for (i = 0; i < tuples; i++)
- {
- printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
- PQgetvalue(result, i, i_ontable),
- PQgetvalue(result, i, i_conname),
- PQgetvalue(result, i, i_condef));
+ printTableAddFooter(&cont, _("Referenced by:"));
+ for (i = 0; i < tuples; i++)
+ {
+ printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
+ PQgetvalue(result, i, i_ontable),
+ PQgetvalue(result, i, i_conname),
+ PQgetvalue(result, i, i_condef));
- printTableAddFooter(&cont, buf.data);
- }
+ printTableAddFooter(&cont, buf.data);
}
- PQclear(result);
}
+ PQclear(result);
/* print any row-level policies */
if (pset.sversion >= 90500)
--
2.49.0
v18.1-0002-Add-support-for-NOT-ENFORCED-in-foreign-key-co.patchtext/plain; charset=UTF-8; name=v18.1-0002-Add-support-for-NOT-ENFORCED-in-foreign-key-co.patchDownload
From 6b09fb343378698657c0cecc6a85b85276b1f27d Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Fri, 7 Feb 2025 09:41:20 +0530
Subject: [PATCH v18.1 2/3] Add support for NOT ENFORCED in foreign key
constraints.
Typically, when a foreign key (FK) constraint is created on a table,
action and check triggers are added to maintain data integrity. With
this patch, if a constraint is marked as NOT ENFORCED, integrity
checks are no longer required, making these triggers unnecessary.
Consequently, when creating a NOT ENFORCED FK constraint, triggers
will not be created, and the constraint will be marked as NOT VALID.
Similarly, if an existing FK constraint is changed to NOT ENFORCED,
the associated triggers will be dropped, and the constraint will also
be marked as NOT VALID. Conversely, if a NOT ENFORCED FK constraint is
changed to ENFORCED, the necessary triggers will be created, and the
will be changed to VALID by performing necessary validation.
----
NOTE: In this patch, the tryAttachPartitionForeignKey() function will
not merge the constraint if the enforcibility differs. This will be
addressed in the next patch.
----
---
doc/src/sgml/catalogs.sgml | 2 +-
doc/src/sgml/ref/alter_table.sgml | 5 +-
doc/src/sgml/ref/create_table.sgml | 2 +-
src/backend/catalog/pg_constraint.c | 5 +-
src/backend/catalog/sql_features.txt | 2 +-
src/backend/commands/tablecmds.c | 457 ++++++++++++++++++----
src/backend/parser/gram.y | 15 +-
src/backend/parser/parse_utilcmd.c | 14 +-
src/backend/utils/cache/relcache.c | 1 +
src/include/nodes/parsenodes.h | 2 +
src/include/utils/rel.h | 3 +
src/test/regress/expected/constraints.out | 8 +-
src/test/regress/expected/foreign_key.out | 161 +++++++-
src/test/regress/expected/inherit.out | 81 ++--
src/test/regress/sql/foreign_key.sql | 107 ++++-
src/test/regress/sql/inherit.sql | 7 +
16 files changed, 725 insertions(+), 147 deletions(-)
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index fb050635551..014bb815665 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2620,7 +2620,7 @@ <title><structname>pg_constraint</structname> Columns</title>
</para>
<para>
Is the constraint enforced?
- Currently, can be false only for CHECK constraints
+ Currently, can be false only for foreign keys and CHECK constraints
</para></entry>
</row>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 11d1bc7dbe1..ece438f0075 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -58,7 +58,7 @@
ALTER [ COLUMN ] <replaceable class="parameter">column_name</replaceable> SET COMPRESSION <replaceable class="parameter">compression_method</replaceable>
ADD <replaceable class="parameter">table_constraint</replaceable> [ NOT VALID ]
ADD <replaceable class="parameter">table_constraint_using_index</replaceable>
- ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+ ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]
ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> [ INHERIT | NO INHERIT ]
VALIDATE CONSTRAINT <replaceable class="parameter">constraint_name</replaceable>
DROP CONSTRAINT [ IF EXISTS ] <replaceable class="parameter">constraint_name</replaceable> [ RESTRICT | CASCADE ]
@@ -589,7 +589,8 @@ <title>Description</title>
This form validates a foreign key or check constraint that was
previously created as <literal>NOT VALID</literal>, by scanning the
table to ensure there are no rows for which the constraint is not
- satisfied. Nothing happens if the constraint is already marked valid.
+ satisfied. If the constraint is not enforced, an error is thrown.
+ Nothing happens if the constraint is already marked valid.
(See <xref linkend="sql-altertable-notes"/> below for an explanation
of the usefulness of this command.)
</para>
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index e5c034d724e..4a41b2f5530 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -1409,7 +1409,7 @@ <title>Parameters</title>
</para>
<para>
- This is currently only supported for <literal>CHECK</literal>
+ This is currently only supported for foreign key and <literal>CHECK</literal>
constraints.
</para>
</listitem>
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index ac80652baf2..0467e7442ff 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -100,8 +100,9 @@ CreateConstraintEntry(const char *constraintName,
ObjectAddresses *addrs_auto;
ObjectAddresses *addrs_normal;
- /* Only CHECK constraint can be not enforced */
- Assert(isEnforced || constraintType == CONSTRAINT_CHECK);
+ /* Only CHECK or FOREIGN KEY constraint can be not enforced */
+ Assert(isEnforced || constraintType == CONSTRAINT_CHECK ||
+ constraintType == CONSTRAINT_FOREIGN);
/* NOT ENFORCED constraint must be NOT VALID */
Assert(isEnforced || !isValidated);
diff --git a/src/backend/catalog/sql_features.txt b/src/backend/catalog/sql_features.txt
index 2f250d2c57b..ebe85337c28 100644
--- a/src/backend/catalog/sql_features.txt
+++ b/src/backend/catalog/sql_features.txt
@@ -281,7 +281,7 @@ F461 Named character sets NO
F471 Scalar subquery values YES
F481 Expanded NULL predicate YES
F491 Constraint management YES
-F492 Optional table constraint enforcement NO check constraints only
+F492 Optional table constraint enforcement YES except not-null constraints
F501 Features and conformance views YES
F501 Features and conformance views 01 SQL_FEATURES view YES
F501 Features and conformance views 02 SQL_SIZING view YES
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index afb25007613..6b56d59e77d 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -395,6 +395,14 @@ static ObjectAddress ATExecAlterConstraint(List **wqueue, Relation rel,
static bool ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon, Relation conrel,
Relation tgrel, Relation rel, HeapTuple contuple,
bool recurse, LOCKMODE lockmode);
+static bool ATExecAlterConstrEnforceability(List **wqueue, ATAlterConstraint *cmdcon,
+ Relation conrel, Relation tgrel,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, LOCKMODE lockmode,
+ Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger);
static bool ATExecAlterConstrDeferrability(List **wqueue, ATAlterConstraint *cmdcon,
Relation conrel, Relation tgrel, Relation rel,
HeapTuple contuple, bool recurse,
@@ -405,6 +413,14 @@ static bool ATExecAlterConstrInheritability(List **wqueue, ATAlterConstraint *cm
static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
bool deferrable, bool initdeferred,
List **otherrelids);
+static void AlterConstrEnforceabilityRecurse(List **wqueue, ATAlterConstraint *cmdcon,
+ Relation conrel, Relation tgrel,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, LOCKMODE lockmode,
+ Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger);
static void AlterConstrDeferrabilityRecurse(List **wqueue, ATAlterConstraint *cmdcon,
Relation conrel, Relation tgrel, Relation rel,
HeapTuple contuple, bool recurse,
@@ -10561,7 +10577,7 @@ addFkConstraint(addFkConstraintSides fkside,
CONSTRAINT_FOREIGN,
fkconstraint->deferrable,
fkconstraint->initdeferred,
- true, /* Is Enforced */
+ fkconstraint->is_enforced,
fkconstraint->initially_valid,
parentConstr,
RelationGetRelid(rel),
@@ -10679,21 +10695,23 @@ addFkRecurseReferenced(Constraint *fkconstraint, Relation rel,
Oid parentDelTrigger, Oid parentUpdTrigger,
bool with_period)
{
- Oid deleteTriggerOid,
- updateTriggerOid;
+ Oid deleteTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
Assert(CheckRelationLockedByMe(pkrel, ShareRowExclusiveLock, true));
Assert(CheckRelationLockedByMe(rel, ShareRowExclusiveLock, true));
/*
- * Create the action triggers that enforce the constraint.
+ * Create action triggers to enforce the constraint, or skip them if the
+ * constraint is NOT ENFORCED.
*/
- createForeignKeyActionTriggers(RelationGetRelid(rel),
- RelationGetRelid(pkrel),
- fkconstraint,
- parentConstr, indexOid,
- parentDelTrigger, parentUpdTrigger,
- &deleteTriggerOid, &updateTriggerOid);
+ if (fkconstraint->is_enforced)
+ createForeignKeyActionTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
+ fkconstraint,
+ parentConstr, indexOid,
+ parentDelTrigger, parentUpdTrigger,
+ &deleteTriggerOid, &updateTriggerOid);
/*
* If the referenced table is partitioned, recurse on ourselves to handle
@@ -10814,8 +10832,8 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
Oid parentInsTrigger, Oid parentUpdTrigger,
bool with_period)
{
- Oid insertTriggerOid,
- updateTriggerOid;
+ Oid insertTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
Assert(OidIsValid(parentConstr));
Assert(CheckRelationLockedByMe(rel, ShareRowExclusiveLock, true));
@@ -10827,29 +10845,32 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
errmsg("foreign key constraints are not supported on foreign tables")));
/*
- * Add the check triggers to it and, if necessary, schedule it to be
- * checked in Phase 3.
+ * Add check triggers if the constraint is ENFORCED, and if needed,
+ * schedule them to be checked in Phase 3.
*
* If the relation is partitioned, drill down to do it to its partitions.
*/
- createForeignKeyCheckTriggers(RelationGetRelid(rel),
- RelationGetRelid(pkrel),
- fkconstraint,
- parentConstr,
- indexOid,
- parentInsTrigger, parentUpdTrigger,
- &insertTriggerOid, &updateTriggerOid);
+ if (fkconstraint->is_enforced)
+ createForeignKeyCheckTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
+ fkconstraint,
+ parentConstr,
+ indexOid,
+ parentInsTrigger, parentUpdTrigger,
+ &insertTriggerOid, &updateTriggerOid);
if (rel->rd_rel->relkind == RELKIND_RELATION)
{
/*
* Tell Phase 3 to check that the constraint is satisfied by existing
- * rows. We can skip this during table creation, when requested
- * explicitly by specifying NOT VALID in an ADD FOREIGN KEY command,
- * and when we're recreating a constraint following a SET DATA TYPE
- * operation that did not impugn its validity.
+ * rows. We can skip this during table creation, when constraint is
+ * specified as NOT ENFORCED, or when requested explicitly by
+ * specifying NOT VALID in an ADD FOREIGN KEY command, and when we're
+ * recreating a constraint following a SET DATA TYPE operation that
+ * did not impugn its validity.
*/
- if (wqueue && !old_check_ok && !fkconstraint->skip_validation)
+ if (wqueue && !old_check_ok && !fkconstraint->skip_validation &&
+ fkconstraint->is_enforced)
{
NewConstraint *newcon;
AlteredTableInfo *tab;
@@ -11080,8 +11101,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
AttrNumber confdelsetcols[INDEX_MAX_KEYS];
Constraint *fkconstraint;
ObjectAddress address;
- Oid deleteTriggerOid,
- updateTriggerOid;
+ Oid deleteTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(constrOid));
if (!HeapTupleIsValid(tuple))
@@ -11141,8 +11162,9 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
fkconstraint->fk_del_set_cols = NIL;
fkconstraint->old_conpfeqop = NIL;
fkconstraint->old_pktable_oid = InvalidOid;
+ fkconstraint->is_enforced = constrForm->conenforced;
fkconstraint->skip_validation = false;
- fkconstraint->initially_valid = true;
+ fkconstraint->initially_valid = constrForm->convalidated;
/* set up colnames that are used to generate the constraint name */
for (int i = 0; i < numfks; i++)
@@ -11170,9 +11192,10 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
* parent OIDs for similar triggers that will be created on the
* partition in addFkRecurseReferenced().
*/
- GetForeignKeyActionTriggers(trigrel, constrOid,
- constrForm->confrelid, constrForm->conrelid,
- &deleteTriggerOid, &updateTriggerOid);
+ if (constrForm->conenforced)
+ GetForeignKeyActionTriggers(trigrel, constrOid,
+ constrForm->confrelid, constrForm->conrelid,
+ &deleteTriggerOid, &updateTriggerOid);
/* Add this constraint ... */
address = addFkConstraint(addFkReferencedSide,
@@ -11305,8 +11328,8 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
Oid indexOid;
ObjectAddress address;
ListCell *lc;
- Oid insertTriggerOid,
- updateTriggerOid;
+ Oid insertTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
bool with_period;
tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(parentConstrOid));
@@ -11338,17 +11361,18 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
mapped_conkey[i] = attmap->attnums[conkey[i] - 1];
/*
- * Get the "check" triggers belonging to the constraint to pass as
- * parent OIDs for similar triggers that will be created on the
- * partition in addFkRecurseReferencing(). They are also passed to
- * tryAttachPartitionForeignKey() below to simply assign as parents to
- * the partition's existing "check" triggers, that is, if the
- * corresponding constraints is deemed attachable to the parent
- * constraint.
+ * Get the "check" triggers belonging to the constraint, if it is
+ * ENFORCED, to pass as parent OIDs for similar triggers that will be
+ * created on the partition in addFkRecurseReferencing(). They are
+ * also passed to tryAttachPartitionForeignKey() below to simply
+ * assign as parents to the partition's existing "check" triggers,
+ * that is, if the corresponding constraints is deemed attachable to
+ * the parent constraint.
*/
- GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
- constrForm->confrelid, constrForm->conrelid,
- &insertTriggerOid, &updateTriggerOid);
+ if (constrForm->conenforced)
+ GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
+ constrForm->confrelid, constrForm->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
/*
* Before creating a new constraint, see whether any existing FKs are
@@ -11401,6 +11425,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
fkconstraint->fk_del_set_cols = NIL;
fkconstraint->old_conpfeqop = NIL;
fkconstraint->old_pktable_oid = InvalidOid;
+ fkconstraint->is_enforced = constrForm->conenforced;
fkconstraint->skip_validation = false;
fkconstraint->initially_valid = constrForm->convalidated;
for (int i = 0; i < numfks; i++)
@@ -11518,6 +11543,7 @@ tryAttachPartitionForeignKey(List **wqueue,
if (OidIsValid(partConstr->conparentid) ||
partConstr->condeferrable != parentConstr->condeferrable ||
partConstr->condeferred != parentConstr->condeferred ||
+ partConstr->conenforced != parentConstr->conenforced ||
partConstr->confupdtype != parentConstr->confupdtype ||
partConstr->confdeltype != parentConstr->confdeltype ||
partConstr->confmatchtype != parentConstr->confmatchtype)
@@ -11561,8 +11587,7 @@ AttachPartitionForeignKey(List **wqueue,
bool queueValidation;
Oid partConstrFrelid;
Oid partConstrRelid;
- Oid insertTriggerOid,
- updateTriggerOid;
+ bool parentConstrIsEnforced;
/* Fetch the parent constraint tuple */
parentConstrTup = SearchSysCache1(CONSTROID,
@@ -11570,6 +11595,7 @@ AttachPartitionForeignKey(List **wqueue,
if (!HeapTupleIsValid(parentConstrTup))
elog(ERROR, "cache lookup failed for constraint %u", parentConstrOid);
parentConstr = (Form_pg_constraint) GETSTRUCT(parentConstrTup);
+ parentConstrIsEnforced = parentConstr->conenforced;
/* Fetch the child constraint tuple */
partcontup = SearchSysCache1(CONSTROID,
@@ -11619,17 +11645,24 @@ AttachPartitionForeignKey(List **wqueue,
/*
* Like the constraint, attach partition's "check" triggers to the
- * corresponding parent triggers.
+ * corresponding parent triggers if the constraint is ENFORCED.
+ * NOT ENFORCED constraints do not have these triggers.
*/
- GetForeignKeyCheckTriggers(trigrel,
- partConstrOid, partConstrFrelid, partConstrRelid,
- &insertTriggerOid, &updateTriggerOid);
- Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
- TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
- RelationGetRelid(partition));
- Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
- TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
- RelationGetRelid(partition));
+ if (parentConstrIsEnforced)
+ {
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
+ GetForeignKeyCheckTriggers(trigrel,
+ partConstrOid, partConstrFrelid, partConstrRelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
+ RelationGetRelid(partition));
+ Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
+ RelationGetRelid(partition));
+ }
/*
* We updated this pg_constraint row above to set its parent; validating
@@ -11743,6 +11776,10 @@ RemoveInheritedConstraint(Relation conrel, Relation trigrel, Oid conoid,
*
* The subroutine for tryAttachPartitionForeignKey handles the deletion of
* action triggers for the foreign key constraint.
+ *
+ * If valid confrelid and conrelid values are not provided, the respective
+ * trigger check will be skipped, and the trigger will be considered for
+ * removal.
*/
static void
DropForeignKeyConstraintTriggers(Relation trigrel, Oid conoid, Oid confrelid,
@@ -11763,11 +11800,28 @@ DropForeignKeyConstraintTriggers(Relation trigrel, Oid conoid, Oid confrelid,
Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
ObjectAddress trigger;
- if (trgform->tgconstrrelid != conrelid)
+ /* Invalid if trigger is not for a referential integrity constraint */
+ if (!OidIsValid(trgform->tgconstrrelid))
continue;
- if (trgform->tgrelid != confrelid)
+ if (OidIsValid(conrelid) && trgform->tgconstrrelid != conrelid)
+ continue;
+ if (OidIsValid(confrelid) && trgform->tgrelid != confrelid)
continue;
+ /* We should be droping trigger related to foreign key constraint */
+ Assert(trgform->tgfoid == F_RI_FKEY_CHECK_INS ||
+ trgform->tgfoid == F_RI_FKEY_CHECK_UPD ||
+ trgform->tgfoid == F_RI_FKEY_CASCADE_DEL ||
+ trgform->tgfoid == F_RI_FKEY_CASCADE_UPD ||
+ trgform->tgfoid == F_RI_FKEY_RESTRICT_DEL ||
+ trgform->tgfoid == F_RI_FKEY_RESTRICT_UPD ||
+ trgform->tgfoid == F_RI_FKEY_SETNULL_DEL ||
+ trgform->tgfoid == F_RI_FKEY_SETNULL_UPD ||
+ trgform->tgfoid == F_RI_FKEY_SETDEFAULT_DEL ||
+ trgform->tgfoid == F_RI_FKEY_SETDEFAULT_UPD ||
+ trgform->tgfoid == F_RI_FKEY_NOACTION_DEL ||
+ trgform->tgfoid == F_RI_FKEY_NOACTION_UPD);
+
/*
* The constraint is originally set up to contain this trigger as an
* implementation object, so there's a dependency record that links
@@ -11979,6 +12033,11 @@ ATExecAlterConstraint(List **wqueue, Relation rel, ATAlterConstraint *cmdcon,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("constraint \"%s\" of relation \"%s\" is not a foreign key constraint",
cmdcon->conname, RelationGetRelationName(rel))));
+ if (cmdcon->alterEnforceability && currcon->contype != CONSTRAINT_FOREIGN)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot alter enforceability of constraint \"%s\" of relation \"%s\"",
+ cmdcon->conname, RelationGetRelationName(rel))));
if (cmdcon->alterInheritability &&
currcon->contype != CONSTRAINT_NOTNULL)
ereport(ERROR,
@@ -12058,7 +12117,7 @@ ATExecAlterConstraint(List **wqueue, Relation rel, ATAlterConstraint *cmdcon,
/*
* A subroutine of ATExecAlterConstraint that calls the respective routines for
- * altering constraint attributes.
+ * altering constraint's enforceability, deferrability or inheritability.
*/
static bool
ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon,
@@ -12066,16 +12125,35 @@ ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon,
HeapTuple contuple, bool recurse,
LOCKMODE lockmode)
{
+ Form_pg_constraint currcon;
bool changed = false;
List *otherrelids = NIL;
+ currcon = (Form_pg_constraint) GETSTRUCT(contuple);
+
/*
- * Do the catalog work for the deferrability change, recurse if necessary.
- */
- if (cmdcon->alterDeferrability &&
- ATExecAlterConstrDeferrability(wqueue, cmdcon, conrel, tgrel, rel,
- contuple, recurse, &otherrelids,
- lockmode))
+ * Do the catalog work for the enforceability or deferrability change,
+ * recurse if necessary.
+ *
+ * Note that even if deferrability is requested to be altered along with
+ * enforceability, we don't need to explicitly update multiple entries in
+ * pg_trigger related to deferrability.
+ *
+ * Modifying enforceability involves either creating or dropping the
+ * trigger, during which the deferrability setting will be adjusted
+ * automatically.
+ */
+ if (cmdcon->alterEnforceability &&
+ ATExecAlterConstrEnforceability(wqueue, cmdcon, conrel, tgrel,
+ currcon->conrelid, currcon->confrelid,
+ contuple, lockmode, InvalidOid,
+ InvalidOid, InvalidOid, InvalidOid))
+ changed = true;
+
+ else if (cmdcon->alterDeferrability &&
+ ATExecAlterConstrDeferrability(wqueue, cmdcon, conrel, tgrel, rel,
+ contuple, recurse, &otherrelids,
+ lockmode))
{
/*
* AlterConstrUpdateConstraintEntry already invalidated relcache for
@@ -12100,6 +12178,151 @@ ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon,
return changed;
}
+/*
+ * Returns true if the constraint's enforceability is altered.
+ *
+ * Depending on whether the constraint is being set to ENFORCED or NOT
+ * ENFORCED, it creates or drops the trigger accordingly.
+ *
+ * Note that we must recurse even when trying to change a 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. 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.
+ */
+static bool
+ATExecAlterConstrEnforceability(List **wqueue, ATAlterConstraint *cmdcon,
+ Relation conrel, Relation tgrel,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, LOCKMODE lockmode,
+ Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger)
+{
+ Form_pg_constraint currcon;
+ Oid conoid;
+ Relation rel;
+ bool changed = false;
+
+ /* Since this function recurses, it could be driven to stack overflow */
+ check_stack_depth();
+
+ Assert(cmdcon->alterEnforceability);
+
+ currcon = (Form_pg_constraint) GETSTRUCT(contuple);
+ conoid = currcon->oid;
+
+ /* Should be foreign key constraint */
+ Assert(currcon->contype == CONSTRAINT_FOREIGN);
+
+ rel = table_open(currcon->conrelid, lockmode);
+
+ if (currcon->conenforced != cmdcon->is_enforced)
+ {
+ AlterConstrUpdateConstraintEntry(cmdcon, conrel, contuple);
+ changed = true;
+ }
+
+ /* Drop triggers */
+ if (!cmdcon->is_enforced)
+ {
+ /*
+ * When setting a constraint to NOT ENFORCED, the constraint triggers
+ * need to be dropped. Therefore, we must process the child relations
+ * first, followed by the parent, to account for dependencies.
+ */
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE)
+ AlterConstrEnforceabilityRecurse(wqueue, cmdcon, conrel, tgrel,
+ fkrelid, pkrelid, contuple,
+ lockmode, InvalidOid, InvalidOid,
+ InvalidOid, InvalidOid);
+
+ /* Drop all the triggers */
+ DropForeignKeyConstraintTriggers(tgrel, conoid, InvalidOid, InvalidOid);
+ }
+ else if (changed) /* Create triggers */
+ {
+ Oid ReferencedDelTriggerOid = InvalidOid,
+ ReferencedUpdTriggerOid = InvalidOid,
+ ReferencingInsTriggerOid = InvalidOid,
+ ReferencingUpdTriggerOid = InvalidOid;
+
+ /* Prepare the minimal information required for trigger creation. */
+ Constraint *fkconstraint = makeNode(Constraint);
+
+ fkconstraint->conname = pstrdup(NameStr(currcon->conname));
+ fkconstraint->fk_matchtype = currcon->confmatchtype;
+ fkconstraint->fk_upd_action = currcon->confupdtype;
+ fkconstraint->fk_del_action = currcon->confdeltype;
+
+ /* Create referenced triggers */
+ if (currcon->conrelid == fkrelid)
+ createForeignKeyActionTriggers(currcon->conrelid,
+ currcon->confrelid,
+ fkconstraint,
+ conoid,
+ currcon->conindid,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ &ReferencedDelTriggerOid,
+ &ReferencedUpdTriggerOid);
+
+ /* Create referencing triggers */
+ if (currcon->confrelid == pkrelid)
+ createForeignKeyCheckTriggers(currcon->conrelid,
+ pkrelid,
+ fkconstraint,
+ conoid,
+ currcon->conindid,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger,
+ &ReferencingInsTriggerOid,
+ &ReferencingUpdTriggerOid);
+
+ /*
+ * Tell Phase 3 to check that the constraint is satisfied by existing
+ * rows.
+ */
+ if (rel->rd_rel->relkind == RELKIND_RELATION)
+ {
+ AlteredTableInfo *tab;
+ NewConstraint *newcon;
+
+ newcon = (NewConstraint *) palloc0(sizeof(NewConstraint));
+ newcon->name = fkconstraint->conname;
+ newcon->contype = CONSTR_FOREIGN;
+ newcon->refrelid = currcon->confrelid;
+ newcon->refindid = currcon->conindid;
+ newcon->conid = currcon->oid;
+ newcon->qual = (Node *) fkconstraint;
+
+ /* Find or create work queue entry for this table */
+ tab = ATGetQueueEntry(wqueue, rel);
+ tab->constraints = lappend(tab->constraints, newcon);
+ }
+
+ /*
+ * If the table at either end of the constraint is partitioned, we
+ * need to recurse and create triggers for each constraint that is a
+ * child of this one.
+ */
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE)
+ AlterConstrEnforceabilityRecurse(wqueue, cmdcon, conrel, tgrel,
+ fkrelid, pkrelid, contuple,
+ lockmode, ReferencedDelTriggerOid,
+ ReferencedUpdTriggerOid,
+ ReferencingInsTriggerOid,
+ ReferencingUpdTriggerOid);
+ }
+
+ table_close(rel, NoLock);
+
+ return changed;
+}
+
/*
* Returns true if the constraint's deferrability is altered.
*
@@ -12304,6 +12527,55 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
systable_endscan(tgscan);
}
+/*
+ * Invokes ATExecAlterConstrEnforceability for each constraint that is a child of
+ * the specified constraint.
+ *
+ * Note that this doesn't handle recursion the normal way, viz. by scanning the
+ * list of child relations and recursing; instead it uses the conparentid
+ * relationships. This may need to be reconsidered.
+ *
+ * The arguments to this function have the same meaning as the arguments to
+ * ATExecAlterConstrEnforceability.
+ */
+static void
+AlterConstrEnforceabilityRecurse(List **wqueue, ATAlterConstraint *cmdcon,
+ Relation conrel, Relation tgrel,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, LOCKMODE lockmode,
+ Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger)
+{
+ Form_pg_constraint currcon;
+ Oid conoid;
+ ScanKeyData pkey;
+ SysScanDesc pscan;
+ HeapTuple childtup;
+
+ currcon = (Form_pg_constraint) GETSTRUCT(contuple);
+ conoid = currcon->oid;
+
+ ScanKeyInit(&pkey,
+ Anum_pg_constraint_conparentid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+
+ pscan = systable_beginscan(conrel, ConstraintParentIndexId,
+ true, NULL, 1, &pkey);
+
+ while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
+ ATExecAlterConstrEnforceability(wqueue, cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, childtup, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
+
+ systable_endscan(pscan);
+}
+
/*
* Invokes ATExecAlterConstrDeferrability for each constraint that is a child of
* the specified constraint.
@@ -12364,11 +12636,25 @@ AlterConstrUpdateConstraintEntry(ATAlterConstraint *cmdcon, Relation conrel,
HeapTuple copyTuple;
Form_pg_constraint copy_con;
- Assert(cmdcon->alterDeferrability || cmdcon->alterInheritability);
+ Assert(cmdcon->alterEnforceability || cmdcon->alterDeferrability ||
+ cmdcon->alterInheritability);
copyTuple = heap_copytuple(contuple);
copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
+ if (cmdcon->alterEnforceability)
+ {
+ copy_con->conenforced = cmdcon->is_enforced;
+
+ /*
+ * 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.
+ */
+ copy_con->convalidated = cmdcon->is_enforced;
+ }
if (cmdcon->alterDeferrability)
{
copy_con->condeferrable = cmdcon->deferrable;
@@ -17088,9 +17374,9 @@ MergeConstraintsIntoExisting(Relation child_rel, Relation parent_rel)
NameStr(child_con->conname), RelationGetRelationName(child_rel))));
/*
- * A non-enforced child constraint cannot be merged with an
- * enforced parent constraint. However, the reverse is allowed,
- * where the child constraint is enforced.
+ * A NOT ENFORCED child constraint cannot be merged with an
+ * ENFORCED parent constraint. However, the reverse is allowed,
+ * where the child constraint is ENFORCED.
*/
if (parent_con->conenforced && !child_con->conenforced)
ereport(ERROR,
@@ -20461,8 +20747,6 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
Form_pg_constraint conform;
- Oid insertTriggerOid,
- updateTriggerOid;
contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
if (!HeapTupleIsValid(contup))
@@ -20489,17 +20773,25 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
/*
* Also, look up the partition's "check" triggers corresponding to the
- * constraint being detached and detach them from the parent triggers.
+ * ENFORCED constraint being detached and detach them from the parent
+ * triggers. NOT ENFORCED constraints do not have these triggers;
+ * therefore, this step is not needed.
*/
- GetForeignKeyCheckTriggers(trigrel,
- fk->conoid, fk->confrelid, fk->conrelid,
- &insertTriggerOid, &updateTriggerOid);
- Assert(OidIsValid(insertTriggerOid));
- TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
- RelationGetRelid(partRel));
- Assert(OidIsValid(updateTriggerOid));
- TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
- RelationGetRelid(partRel));
+ if (fk->conenforced)
+ {
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ Assert(OidIsValid(updateTriggerOid));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ }
/*
* Lastly, create the action triggers on the referenced table, using
@@ -20539,8 +20831,9 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
fkconstraint->conname = pstrdup(NameStr(conform->conname));
fkconstraint->deferrable = conform->condeferrable;
fkconstraint->initdeferred = conform->condeferred;
+ fkconstraint->is_enforced = conform->conenforced;
fkconstraint->skip_validation = true;
- fkconstraint->initially_valid = true;
+ fkconstraint->initially_valid = conform->convalidated;
/* a few irrelevant fields omitted here */
fkconstraint->pktable = NULL;
fkconstraint->fk_attrs = NIL;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 0fc502a3a40..c982f856559 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -2665,12 +2665,17 @@ alter_table_cmd:
if ($4 & (CAS_DEFERRABLE | CAS_NOT_DEFERRABLE |
CAS_INITIALLY_DEFERRED | CAS_INITIALLY_IMMEDIATE))
c->alterDeferrability = true;
+ if ($4 & (CAS_NOT_ENFORCED | CAS_ENFORCED))
+ c->alterEnforceability = true;
if ($4 & CAS_NO_INHERIT)
c->alterInheritability = true;
processCASbits($4, @4, "FOREIGN KEY",
- &c->deferrable,
- &c->initdeferred,
- NULL, NULL, &c->noinherit, yyscanner);
+ &c->deferrable,
+ &c->initdeferred,
+ &c->is_enforced,
+ NULL,
+ &c->noinherit,
+ yyscanner);
$$ = (Node *) n;
}
/* ALTER TABLE <name> ALTER CONSTRAINT INHERIT */
@@ -4334,8 +4339,8 @@ ConstraintElem:
n->fk_del_set_cols = ($11)->deleteAction->cols;
processCASbits($12, @12, "FOREIGN KEY",
&n->deferrable, &n->initdeferred,
- NULL, &n->skip_validation, NULL,
- yyscanner);
+ &n->is_enforced, &n->skip_validation,
+ NULL, yyscanner);
n->initially_valid = !n->skip_validation;
$$ = (Node *) n;
}
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 896a7f2c59b..50f430a4e1a 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -2976,8 +2976,10 @@ transformFKConstraints(CreateStmtContext *cxt,
/*
* If CREATE TABLE or adding a column with NULL default, we can safely
- * skip validation of FK constraints, and nonetheless mark them valid.
- * (This will override any user-supplied NOT VALID flag.)
+ * skip validation of FK constraints, and mark them as valid based on the
+ * constraint enforcement flag, since NOT ENFORCED constraints must always
+ * be marked as NOT VALID. (This will override any user-supplied NOT VALID
+ * flag.)
*/
if (skipValidation)
{
@@ -2986,7 +2988,7 @@ transformFKConstraints(CreateStmtContext *cxt,
Constraint *constraint = (Constraint *) lfirst(fkclist);
constraint->skip_validation = true;
- constraint->initially_valid = true;
+ constraint->initially_valid = constraint->is_enforced;
}
}
@@ -3981,7 +3983,8 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
case CONSTR_ATTR_ENFORCED:
if (lastprimarycon == NULL ||
- lastprimarycon->contype != CONSTR_CHECK)
+ (lastprimarycon->contype != CONSTR_CHECK &&
+ lastprimarycon->contype != CONSTR_FOREIGN))
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("misplaced ENFORCED clause"),
@@ -3997,7 +4000,8 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
case CONSTR_ATTR_NOT_ENFORCED:
if (lastprimarycon == NULL ||
- lastprimarycon->contype != CONSTR_CHECK)
+ (lastprimarycon->contype != CONSTR_CHECK &&
+ lastprimarycon->contype != CONSTR_FOREIGN))
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("misplaced NOT ENFORCED clause"),
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index e6721056536..18a14ae186e 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4697,6 +4697,7 @@ RelationGetFKeyList(Relation relation)
info->conoid = constraint->oid;
info->conrelid = constraint->conrelid;
info->confrelid = constraint->confrelid;
+ info->conenforced = constraint->conenforced;
DeconstructFkConstraintRow(htup, &info->nkeys,
info->conkey,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index df331b1c0d9..00fefa9483a 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2495,6 +2495,8 @@ typedef struct ATAlterConstraint
{
NodeTag type;
char *conname; /* Constraint name */
+ bool alterEnforceability; /* changing enforceability properties? */
+ bool is_enforced; /* ENFORCED? */
bool alterDeferrability; /* changing deferrability properties? */
bool deferrable; /* DEFERRABLE? */
bool initdeferred; /* INITIALLY DEFERRED? */
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index d94fddd7cef..b552359915f 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -284,6 +284,9 @@ typedef struct ForeignKeyCacheInfo
/* number of columns in the foreign key */
int nkeys;
+ /* Is enforced ? */
+ bool conenforced;
+
/*
* these arrays each have nkeys valid entries:
*/
diff --git a/src/test/regress/expected/constraints.out b/src/test/regress/expected/constraints.out
index 4f39100fcdf..a719d2f74e9 100644
--- a/src/test/regress/expected/constraints.out
+++ b/src/test/regress/expected/constraints.out
@@ -745,13 +745,9 @@ ERROR: misplaced NOT ENFORCED clause
LINE 1: CREATE TABLE UNIQUE_NOTEN_TBL(i int UNIQUE NOT ENFORCED);
^
ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
-ERROR: FOREIGN KEY constraints cannot be marked ENFORCED
-LINE 1: ...TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
- ^
+ERROR: cannot alter enforceability of constraint "unique_tbl_i_key" of relation "unique_tbl"
ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORCED;
-ERROR: FOREIGN KEY constraints cannot be marked NOT ENFORCED
-LINE 1: ...ABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORC...
- ^
+ERROR: cannot alter enforceability of constraint "unique_tbl_i_key" of relation "unique_tbl"
DROP TABLE unique_tbl;
--
-- EXCLUDE constraints
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 7f678349a8e..1d0a4548732 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -1,12 +1,43 @@
--
-- FOREIGN KEY
--
--- MATCH FULL
+-- NOT ENFORCED
--
-- First test, check and cascade
--
CREATE TABLE PKTABLE ( ptest1 int PRIMARY KEY, ptest2 text );
-CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE, ftest2 int );
+CREATE TABLE FKTABLE ( ftest1 int CONSTRAINT fktable_ftest1_fkey REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE NOT ENFORCED,
+ ftest2 int );
+-- Inserting into the foreign key table will not result in an error, even if
+-- there is no matching key in the referenced table.
+INSERT INTO FKTABLE VALUES (1, 2);
+INSERT INTO FKTABLE VALUES (2, 3);
+-- Check FKTABLE
+SELECT * FROM FKTABLE;
+ ftest1 | ftest2
+--------+--------
+ 1 | 2
+ 2 | 3
+(2 rows)
+
+-- Reverting it back to ENFORCED will result in failure because constraint validation will be triggered,
+-- as it was previously in a valid state.
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_fkey"
+DETAIL: Key (ftest1)=(1) is not present in table "pktable".
+-- Remove any existing rows that violate the constraint, then attempt to change
+-- it.
+TRUNCATE FKTABLE;
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+-- Any further inserts will fail due to the enforcement.
+INSERT INTO FKTABLE VALUES (3, 4);
+ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_fkey"
+DETAIL: Key (ftest1)=(3) is not present in table "pktable".
+--
+-- MATCH FULL
+--
+-- First test, check and cascade
+--
-- Insert test data into PKTABLE
INSERT INTO PKTABLE VALUES (1, 'Test1');
INSERT INTO PKTABLE VALUES (2, 'Test2');
@@ -351,6 +382,43 @@ INSERT INTO FKTABLE VALUES (1, NULL);
ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL;
ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_ftest2_fkey"
DETAIL: MATCH FULL does not allow mixing of null and nonnull key values.
+-- Modifying other attributes of a constraint should not affect its enforceability, and vice versa
+ALTER TABLE FKTABLE ADD CONSTRAINT fk_con FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE NOT VALID NOT ENFORCED;
+ALTER TABLE FKTABLE ALTER CONSTRAINT fk_con DEFERRABLE INITIALLY DEFERRED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fk_con';
+ condeferrable | condeferred | conenforced | convalidated
+---------------+-------------+-------------+--------------
+ t | t | f | f
+(1 row)
+
+ALTER TABLE FKTABLE ALTER CONSTRAINT fk_con NOT ENFORCED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fk_con';
+ condeferrable | condeferred | conenforced | convalidated
+---------------+-------------+-------------+--------------
+ t | t | f | f
+(1 row)
+
+-- Enforceability also changes the validate state, as data validation will be
+-- performed during this transformation.
+ALTER TABLE FKTABLE ALTER CONSTRAINT fk_con ENFORCED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fk_con';
+ condeferrable | condeferred | conenforced | convalidated
+---------------+-------------+-------------+--------------
+ t | t | t | t
+(1 row)
+
+-- Can change enforceability and deferrability together
+ALTER TABLE FKTABLE ALTER CONSTRAINT fk_con NOT ENFORCED NOT DEFERRABLE;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fk_con';
+ condeferrable | condeferred | conenforced | convalidated
+---------------+-------------+-------------+--------------
+ f | f | f | f
+(1 row)
+
DROP TABLE FKTABLE;
DROP TABLE PKTABLE;
-- MATCH SIMPLE
@@ -1276,6 +1344,13 @@ INSERT INTO fktable VALUES (0, 20);
ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_fk_fkey"
DETAIL: Key (fk)=(20) is not present in table "pktable".
COMMIT;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT ENFORCED;
+BEGIN;
+-- doesn't match FK, no error.
+UPDATE pktable SET id = 10 WHERE id = 5;
+-- doesn't match PK, no error.
+INSERT INTO fktable VALUES (0, 20);
+ROLLBACK;
-- try additional syntax
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE;
-- illegal options
@@ -1289,6 +1364,14 @@ ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT VALID;
ERROR: FOREIGN KEY constraints cannot be marked NOT VALID
LINE 1: ...ER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT VALID;
^
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORCED;
+ERROR: conflicting constraint properties
+LINE 1: ...fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORC...
+ ^
+CREATE TEMP TABLE fktable2 (fk int references pktable ENFORCED NOT ENFORCED);
+ERROR: multiple ENFORCED/NOT ENFORCED clauses not allowed
+LINE 1: ...ABLE fktable2 (fk int references pktable ENFORCED NOT ENFORC...
+ ^
-- test order of firing of FK triggers when several RI-induced changes need to
-- be made to the same row. This was broken by subtransaction-related
-- changes in 8.0.
@@ -1586,10 +1669,13 @@ ALTER TABLE fk_partitioned_fk DROP COLUMN fdrop1;
CREATE TABLE fk_partitioned_fk_1 (fdrop1 int, fdrop2 int, a int, fdrop3 int, b int);
ALTER TABLE fk_partitioned_fk_1 DROP COLUMN fdrop1, DROP COLUMN fdrop2, DROP COLUMN fdrop3;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FROM (0,0) TO (1000,1000);
-ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk;
+ALTER TABLE fk_partitioned_fk ADD CONSTRAINT fk_partitioned_fk_a_b_fkey FOREIGN KEY (a, b)
+ REFERENCES fk_notpartitioned_pk NOT ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
+ALTER TABLE fk_partitioned_fk_2 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
CREATE TABLE fk_partitioned_fk_3 (fdrop1 int, fdrop2 int, fdrop3 int, fdrop4 int, b int, a int)
PARTITION BY HASH (a);
ALTER TABLE fk_partitioned_fk_3 DROP COLUMN fdrop1, DROP COLUMN fdrop2,
@@ -1612,10 +1698,10 @@ INSERT INTO fk_partitioned_fk_1 (a,b) VALUES (500, 501);
ERROR: insert or update on table "fk_partitioned_fk_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
DETAIL: Key (a, b)=(500, 501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (1500, 1501);
-ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_2_a_b_fkey"
DETAIL: Key (a, b)=(1500, 1501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk_2 (a,b) VALUES (1500, 1501);
-ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_2_a_b_fkey"
DETAIL: Key (a, b)=(1500, 1501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (2500, 2502);
ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
@@ -1665,6 +1751,37 @@ Indexes:
Referenced by:
TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b)
+-- Check the exsting FK trigger
+CREATE TEMP TABLE tmp_trg_info AS SELECT tgtype, tgrelid::regclass, tgconstraint
+FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+SELECT count(1) FROM tmp_trg_info;
+ count
+-------
+ 14
+(1 row)
+
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+-- No triggers
+SELECT count(1) FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+ count
+-------
+ 0
+(1 row)
+
+-- Changing it back to ENFORCED will recreate the necessary triggers.
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+-- Should be exactly the same number of triggers found as before
+SELECT COUNT(1) FROM pg_trigger pgt JOIN tmp_trg_info tt
+ON (pgt.tgtype = tt.tgtype AND pgt.tgrelid = tt.tgrelid AND pgt.tgconstraint = tt.tgconstraint);
+ count
+-------
+ 14
+(1 row)
+
ALTER TABLE fk_partitioned_fk DROP CONSTRAINT fk_partitioned_fk_a_b_fkey;
-- done.
DROP TABLE fk_notpartitioned_pk, fk_partitioned_fk;
@@ -1962,6 +2079,40 @@ Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
Foreign-key constraints:
TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
+DROP TABLE fk_partitioned_fk_2;
+CREATE TABLE fk_partitioned_fk_2 (b int, a int,
+ FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
+BEGIN;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should be having two constraints
+\d fk_partitioned_fk_2
+ Table "public.fk_partitioned_fk_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ b | integer | | |
+ a | integer | | |
+Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
+Foreign-key constraints:
+ "fk_partitioned_fk_2_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED
+ TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
+
+DROP TABLE fk_partitioned_fk_2;
+ROLLBACK;
+BEGIN;
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have only one constraint
+\d fk_partitioned_fk_2
+ Table "public.fk_partitioned_fk_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ b | integer | | |
+ a | integer | | |
+Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
+Foreign-key constraints:
+ TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED
+
+ROLLBACK;
DROP TABLE fk_partitioned_fk_2;
CREATE TABLE fk_partitioned_fk_4 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE) PARTITION BY RANGE (b, a);
CREATE TABLE fk_partitioned_fk_4_1 PARTITION OF fk_partitioned_fk_4 FOR VALUES FROM (1,1) TO (100,100);
diff --git a/src/test/regress/expected/inherit.out b/src/test/regress/expected/inherit.out
index 4d07d0bd79b..caab1164fdb 100644
--- a/src/test/regress/expected/inherit.out
+++ b/src/test/regress/expected/inherit.out
@@ -1332,6 +1332,13 @@ NOTICE: merging constraint "inh_check_constraint5" with inherited definition
alter table p1 add constraint inh_check_constraint6 check (f1 < 10) not enforced;
alter table p1_c1 add constraint inh_check_constraint6 check (f1 < 10) enforced;
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 invalid state of the child constraint will be ignored here.
+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
create table p1_c2(f1 int constraint inh_check_constraint4 check (f1 < 10)) inherits(p1);
NOTICE: merging column "f1" with inherited definition
NOTICE: merging constraint "inh_check_constraint4" with inherited definition
@@ -1356,39 +1363,47 @@ ERROR: constraint "inh_check_constraint6" conflicts with NOT ENFORCED constrain
select conrelid::regclass::text as relname, conname, conislocal, coninhcount, conenforced, convalidated
from pg_constraint where conname like 'inh\_check\_constraint%'
order by 1, 2;
- relname | conname | conislocal | coninhcount | conenforced | convalidated
----------+-----------------------+------------+-------------+-------------+--------------
- p1 | inh_check_constraint1 | t | 0 | t | t
- p1 | inh_check_constraint2 | t | 0 | t | t
- p1 | inh_check_constraint3 | t | 0 | f | f
- p1 | inh_check_constraint4 | t | 0 | f | f
- p1 | inh_check_constraint5 | t | 0 | f | f
- p1 | inh_check_constraint6 | t | 0 | f | f
- p1 | inh_check_constraint8 | t | 0 | t | t
- p1_c1 | inh_check_constraint1 | t | 1 | t | t
- 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
- p1_c1 | inh_check_constraint5 | t | 1 | t | t
- p1_c1 | inh_check_constraint6 | t | 1 | t | t
- p1_c1 | inh_check_constraint7 | t | 0 | f | f
- p1_c1 | inh_check_constraint8 | f | 1 | t | t
- p1_c2 | inh_check_constraint1 | f | 1 | t | t
- p1_c2 | inh_check_constraint2 | f | 1 | t | t
- p1_c2 | inh_check_constraint3 | f | 1 | f | f
- p1_c2 | inh_check_constraint4 | t | 1 | t | t
- p1_c2 | inh_check_constraint5 | f | 1 | f | f
- p1_c2 | inh_check_constraint6 | f | 1 | f | f
- p1_c2 | inh_check_constraint8 | f | 1 | t | t
- p1_c3 | inh_check_constraint1 | f | 2 | t | t
- p1_c3 | inh_check_constraint2 | f | 2 | t | t
- p1_c3 | inh_check_constraint3 | f | 2 | f | f
- p1_c3 | inh_check_constraint4 | f | 2 | f | f
- p1_c3 | inh_check_constraint5 | f | 2 | t | t
- p1_c3 | inh_check_constraint6 | f | 2 | t | t
- p1_c3 | inh_check_constraint7 | f | 1 | f | f
- p1_c3 | inh_check_constraint8 | f | 2 | t | t
-(30 rows)
+ relname | conname | conislocal | coninhcount | conenforced | convalidated
+---------+------------------------+------------+-------------+-------------+--------------
+ p1 | inh_check_constraint1 | t | 0 | t | t
+ p1 | inh_check_constraint10 | t | 0 | f | f
+ p1 | inh_check_constraint2 | t | 0 | t | t
+ p1 | inh_check_constraint3 | t | 0 | f | f
+ p1 | inh_check_constraint4 | t | 0 | f | f
+ p1 | inh_check_constraint5 | t | 0 | f | f
+ p1 | inh_check_constraint6 | t | 0 | f | f
+ 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_constraint2 | t | 1 | t | t
+ p1_c1 | inh_check_constraint3 | t | 1 | f | f
+ p1_c1 | inh_check_constraint4 | t | 1 | f | f
+ p1_c1 | inh_check_constraint5 | t | 1 | t | t
+ p1_c1 | inh_check_constraint6 | t | 1 | t | t
+ p1_c1 | inh_check_constraint7 | t | 0 | f | f
+ p1_c1 | inh_check_constraint8 | f | 1 | t | t
+ p1_c1 | inh_check_constraint9 | t | 1 | t | f
+ p1_c2 | inh_check_constraint1 | f | 1 | t | t
+ p1_c2 | inh_check_constraint10 | f | 1 | f | f
+ p1_c2 | inh_check_constraint2 | f | 1 | t | t
+ p1_c2 | inh_check_constraint3 | f | 1 | f | f
+ p1_c2 | inh_check_constraint4 | t | 1 | t | t
+ p1_c2 | inh_check_constraint5 | f | 1 | f | f
+ p1_c2 | inh_check_constraint6 | f | 1 | f | f
+ p1_c2 | inh_check_constraint8 | f | 1 | t | t
+ p1_c2 | inh_check_constraint9 | f | 1 | f | f
+ p1_c3 | inh_check_constraint1 | f | 2 | t | t
+ p1_c3 | inh_check_constraint10 | f | 2 | t | t
+ p1_c3 | inh_check_constraint2 | f | 2 | t | t
+ p1_c3 | inh_check_constraint3 | f | 2 | f | f
+ p1_c3 | inh_check_constraint4 | f | 2 | f | f
+ p1_c3 | inh_check_constraint5 | f | 2 | t | t
+ p1_c3 | inh_check_constraint6 | f | 2 | t | t
+ p1_c3 | inh_check_constraint7 | f | 1 | f | f
+ p1_c3 | inh_check_constraint8 | f | 2 | t | t
+ p1_c3 | inh_check_constraint9 | f | 2 | t | t
+(38 rows)
drop table p1 cascade;
NOTICE: drop cascades to 3 other objects
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index 44945b0453a..7e5be0702da 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -2,13 +2,39 @@
-- FOREIGN KEY
--
--- MATCH FULL
+-- NOT ENFORCED
--
-- First test, check and cascade
--
CREATE TABLE PKTABLE ( ptest1 int PRIMARY KEY, ptest2 text );
-CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE, ftest2 int );
+CREATE TABLE FKTABLE ( ftest1 int CONSTRAINT fktable_ftest1_fkey REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE NOT ENFORCED,
+ ftest2 int );
+
+-- Inserting into the foreign key table will not result in an error, even if
+-- there is no matching key in the referenced table.
+INSERT INTO FKTABLE VALUES (1, 2);
+INSERT INTO FKTABLE VALUES (2, 3);
+
+-- Check FKTABLE
+SELECT * FROM FKTABLE;
+
+-- Reverting it back to ENFORCED will result in failure because constraint validation will be triggered,
+-- as it was previously in a valid state.
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+
+-- Remove any existing rows that violate the constraint, then attempt to change
+-- it.
+TRUNCATE FKTABLE;
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+-- Any further inserts will fail due to the enforcement.
+INSERT INTO FKTABLE VALUES (3, 4);
+
+--
+-- MATCH FULL
+--
+-- First test, check and cascade
+--
-- Insert test data into PKTABLE
INSERT INTO PKTABLE VALUES (1, 'Test1');
INSERT INTO PKTABLE VALUES (2, 'Test2');
@@ -230,6 +256,27 @@ CREATE TABLE FKTABLE ( ftest1 int, ftest2 int );
ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL;
+-- Modifying other attributes of a constraint should not affect its enforceability, and vice versa
+ALTER TABLE FKTABLE ADD CONSTRAINT fk_con FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE NOT VALID NOT ENFORCED;
+ALTER TABLE FKTABLE ALTER CONSTRAINT fk_con DEFERRABLE INITIALLY DEFERRED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fk_con';
+
+ALTER TABLE FKTABLE ALTER CONSTRAINT fk_con NOT ENFORCED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fk_con';
+
+-- Enforceability also changes the validate state, as data validation will be
+-- performed during this transformation.
+ALTER TABLE FKTABLE ALTER CONSTRAINT fk_con ENFORCED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fk_con';
+
+-- Can change enforceability and deferrability together
+ALTER TABLE FKTABLE ALTER CONSTRAINT fk_con NOT ENFORCED NOT DEFERRABLE;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fk_con';
+
DROP TABLE FKTABLE;
DROP TABLE PKTABLE;
@@ -968,12 +1015,25 @@ CREATE TEMP TABLE fktable (
COMMIT;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT ENFORCED;
+
+BEGIN;
+
+-- doesn't match FK, no error.
+UPDATE pktable SET id = 10 WHERE id = 5;
+-- doesn't match PK, no error.
+INSERT INTO fktable VALUES (0, 20);
+
+ROLLBACK;
+
-- try additional syntax
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE;
-- illegal options
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE INITIALLY DEFERRED;
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NO INHERIT;
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT VALID;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORCED;
+CREATE TEMP TABLE fktable2 (fk int references pktable ENFORCED NOT ENFORCED);
-- test order of firing of FK triggers when several RI-induced changes need to
-- be made to the same row. This was broken by subtransaction-related
@@ -1184,11 +1244,13 @@ CREATE TABLE fk_partitioned_fk (b int, fdrop1 int, a int) PARTITION BY RANGE (a,
CREATE TABLE fk_partitioned_fk_1 (fdrop1 int, fdrop2 int, a int, fdrop3 int, b int);
ALTER TABLE fk_partitioned_fk_1 DROP COLUMN fdrop1, DROP COLUMN fdrop2, DROP COLUMN fdrop3;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FROM (0,0) TO (1000,1000);
-ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk;
+ALTER TABLE fk_partitioned_fk ADD CONSTRAINT fk_partitioned_fk_a_b_fkey FOREIGN KEY (a, b)
+ REFERENCES fk_notpartitioned_pk NOT ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
+ALTER TABLE fk_partitioned_fk_2 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
-
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
CREATE TABLE fk_partitioned_fk_3 (fdrop1 int, fdrop2 int, fdrop3 int, fdrop4 int, b int, a int)
PARTITION BY HASH (a);
ALTER TABLE fk_partitioned_fk_3 DROP COLUMN fdrop1, DROP COLUMN fdrop2,
@@ -1234,6 +1296,27 @@ CREATE TABLE fk_partitioned_fk_3_1 PARTITION OF fk_partitioned_fk_3 FOR VALUES W
UPDATE fk_notpartitioned_pk SET b = 2504 WHERE a = 2500;
-- check psql behavior
\d fk_notpartitioned_pk
+
+-- Check the exsting FK trigger
+CREATE TEMP TABLE tmp_trg_info AS SELECT tgtype, tgrelid::regclass, tgconstraint
+FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+SELECT count(1) FROM tmp_trg_info;
+
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+-- No triggers
+SELECT count(1) FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+
+-- Changing it back to ENFORCED will recreate the necessary triggers.
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+
+-- Should be exactly the same number of triggers found as before
+SELECT COUNT(1) FROM pg_trigger pgt JOIN tmp_trg_info tt
+ON (pgt.tgtype = tt.tgtype AND pgt.tgrelid = tt.tgrelid AND pgt.tgconstraint = tt.tgconstraint);
+
ALTER TABLE fk_partitioned_fk DROP CONSTRAINT fk_partitioned_fk_a_b_fkey;
-- done.
DROP TABLE fk_notpartitioned_pk, fk_partitioned_fk;
@@ -1441,6 +1524,22 @@ CREATE TABLE fk_partitioned_fk_2 (b int, c text, a int,
\d fk_partitioned_fk_2
DROP TABLE fk_partitioned_fk_2;
+CREATE TABLE fk_partitioned_fk_2 (b int, a int,
+ FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
+BEGIN;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should be having two constraints
+\d fk_partitioned_fk_2
+DROP TABLE fk_partitioned_fk_2;
+ROLLBACK;
+BEGIN;
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- should have only one constraint
+\d fk_partitioned_fk_2
+ROLLBACK;
+DROP TABLE fk_partitioned_fk_2;
+
CREATE TABLE fk_partitioned_fk_4 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE) PARTITION BY RANGE (b, a);
CREATE TABLE fk_partitioned_fk_4_1 PARTITION OF fk_partitioned_fk_4 FOR VALUES FROM (1,1) TO (100,100);
CREATE TABLE fk_partitioned_fk_4_2 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE SET NULL);
diff --git a/src/test/regress/sql/inherit.sql b/src/test/regress/sql/inherit.sql
index 941189761fd..5f0b2617464 100644
--- a/src/test/regress/sql/inherit.sql
+++ b/src/test/regress/sql/inherit.sql
@@ -481,6 +481,13 @@ CREATE TABLE test_foreign_constraints_inh () INHERITS (test_foreign_constraints)
alter table p1 add constraint inh_check_constraint6 check (f1 < 10) not enforced;
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 invalid state of the child constraint will be ignored here.
+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;
+
create table p1_c2(f1 int constraint inh_check_constraint4 check (f1 < 10)) inherits(p1);
-- but reverse is not allowed
--
2.49.0
v18.1-0003-Merge-the-parent-and-child-constraints-with-di.patchtext/plain; charset=UTF-8; name=v18.1-0003-Merge-the-parent-and-child-constraints-with-di.patchDownload
From c80eabbaf55edb4c44729499e9c9cd16596f1ddf Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 10 Feb 2025 10:59:28 +0530
Subject: [PATCH v18.1 3/3] Merge the parent and child constraints with
differing enforcibility.
If an ENFORCED parent constraint is attached to a NOT ENFORCED child
constraint, the child constraint will be made ENFORCED, with
validation applied if the parent constraint is validated as well.
Otherwise, a new ENFORCED constraint (with validation, if the parent
constraint is validated) would need to be created on the child table,
which would be unnecessary if a similar constraint already exists and
can be attached.
On the other hand, having a NOT ENFORCED parent constraint with an
ENFORCED child constraint does not cause any issues, and no changes
are required.
----
NOTE: This patch is intended to reduce the diff noise from the main
patch and is not meant to be committed separately. It should be
squashed with the main patch that adds ENFORCED/NOT ENFORCED.
----
---
src/backend/commands/tablecmds.c | 164 ++++++++++++++++++++--
src/test/regress/expected/foreign_key.out | 77 ++++++++--
src/test/regress/sql/foreign_key.sql | 37 ++++-
3 files changed, 253 insertions(+), 25 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 6b56d59e77d..e2707132771 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -11543,7 +11543,6 @@ tryAttachPartitionForeignKey(List **wqueue,
if (OidIsValid(partConstr->conparentid) ||
partConstr->condeferrable != parentConstr->condeferrable ||
partConstr->condeferred != parentConstr->condeferred ||
- partConstr->conenforced != parentConstr->conenforced ||
partConstr->confupdtype != parentConstr->confupdtype ||
partConstr->confdeltype != parentConstr->confdeltype ||
partConstr->confmatchtype != parentConstr->confmatchtype)
@@ -11588,6 +11587,8 @@ AttachPartitionForeignKey(List **wqueue,
Oid partConstrFrelid;
Oid partConstrRelid;
bool parentConstrIsEnforced;
+ bool partConstrIsEnforced;
+ bool partConstrParentIsSet;
/* Fetch the parent constraint tuple */
parentConstrTup = SearchSysCache1(CONSTROID,
@@ -11603,13 +11604,47 @@ AttachPartitionForeignKey(List **wqueue,
if (!HeapTupleIsValid(partcontup))
elog(ERROR, "cache lookup failed for constraint %u", partConstrOid);
partConstr = (Form_pg_constraint) GETSTRUCT(partcontup);
+ partConstrIsEnforced = partConstr->conenforced;
partConstrFrelid = partConstr->confrelid;
partConstrRelid = partConstr->conrelid;
+ /*
+ * The case where the parent constraint is NOT ENFORCED and the child
+ * constraint is ENFORCED is acceptable because the not enforced parent
+ * constraint lacks triggers, eliminating any redundancy issues with the
+ * enforced child constraint. In this scenario, the child constraint
+ * remains enforced, and its trigger is retained, ensuring that
+ * referential integrity checks for the child continue as before, even
+ * with the parent constraint not enforced. The relationship between the
+ * two constraints is preserved by setting the parent constraint, which
+ * allows us to locate the child constraint. This becomes important if the
+ * parent constraint is later changed to enforced, at which point the
+ * necessary trigger will be created for the parent, and any redundancy
+ * from these triggers will be appropriately handled.
+ */
+ if (!parentConstrIsEnforced && partConstrIsEnforced)
+ {
+ ReleaseSysCache(partcontup);
+ ReleaseSysCache(parentConstrTup);
+
+ ConstraintSetParentConstraint(partConstrOid, parentConstrOid,
+ RelationGetRelid(partition));
+ CommandCounterIncrement();
+
+ return;
+ }
+
/*
* If the referenced table is partitioned, then the partition we're
* attaching now has extra pg_constraint rows and action triggers that are
* no longer needed. Remove those.
+ *
+ * Note that this must be done beforehand, particularly in situations
+ * where we might decide to change the constraint to an ENFORCED state
+ * which will create the required triggers and add the child constraint to
+ * the validation queue. To avoid generating unnecessary triggers and
+ * adding them to the validation queue, it is crucial to eliminate any
+ * redundant constraints beforehand.
*/
if (get_rel_relkind(partConstrFrelid) == RELKIND_PARTITIONED_TABLE)
{
@@ -11628,6 +11663,53 @@ AttachPartitionForeignKey(List **wqueue,
*/
queueValidation = parentConstr->convalidated && !partConstr->convalidated;
+ /*
+ * The case where the parent constraint is ENFORCED and the child
+ * constraint is NOT ENFORCED is not acceptable, as it would violate
+ * referential integrity. In such cases, the child constraint will first
+ * be enforced before merging it with the enforced parent constraint.
+ * Subsequently, removing action triggers, setting up constraint triggers,
+ * and handling check triggers for the parent will be managed in the usual
+ * manner, similar to how two enforced constraints are merged.
+ */
+ if (parentConstrIsEnforced && !partConstrIsEnforced)
+ {
+ ATAlterConstraint *cmdcon = makeNode(ATAlterConstraint);
+ Relation conrel;
+
+ cmdcon->conname = NameStr(partConstr->conname);
+ cmdcon->deferrable = partConstr->condeferrable;
+ cmdcon->initdeferred = partConstr->condeferred;
+ cmdcon->alterEnforceability = true;
+ cmdcon->is_enforced = true;
+
+ conrel = table_open(ConstraintRelationId, RowExclusiveLock);
+
+ ATExecAlterConstrEnforceability(wqueue, cmdcon, conrel, trigrel,
+ partConstr->conrelid,
+ partConstr->confrelid,
+ partcontup, AccessExclusiveLock,
+ InvalidOid, InvalidOid, InvalidOid,
+ InvalidOid);
+
+ table_close(conrel, RowExclusiveLock);
+
+ CommandCounterIncrement();
+
+ /*
+ * No further validation is needed, as changing the constraint to
+ * enforced will implicitly trigger the same validation.
+ */
+ queueValidation = false;
+ }
+
+ /*
+ * The constraint parent shouldn't be set beforehand, or if it's already
+ * set, it should be the specified parent.
+ */
+ partConstrParentIsSet = OidIsValid(partConstr->conparentid);
+ Assert(!partConstrParentIsSet || partConstr->conparentid == parentConstrOid);
+
ReleaseSysCache(partcontup);
ReleaseSysCache(parentConstrTup);
@@ -11640,8 +11722,10 @@ AttachPartitionForeignKey(List **wqueue,
DropForeignKeyConstraintTriggers(trigrel, partConstrOid, partConstrFrelid,
partConstrRelid);
- ConstraintSetParentConstraint(partConstrOid, parentConstrOid,
- RelationGetRelid(partition));
+ /* Skip if the parent is already set */
+ if (!partConstrParentIsSet)
+ ConstraintSetParentConstraint(partConstrOid, parentConstrOid,
+ RelationGetRelid(partition));
/*
* Like the constraint, attach partition's "check" triggers to the
@@ -12241,6 +12325,17 @@ ATExecAlterConstrEnforceability(List **wqueue, ATAlterConstraint *cmdcon,
/* Drop all the triggers */
DropForeignKeyConstraintTriggers(tgrel, conoid, InvalidOid, InvalidOid);
+
+ /*
+ * If the referenced table is partitioned, the child constraint we're
+ * changing to NOT ENFORCED may have additional pg_constraint rows and
+ * action triggers that remain untouched while this child constraint
+ * is attached to the NOT ENFORCED parent. These must now be removed.
+ * For more details, see AttachPartitionForeignKey().
+ */
+ if (OidIsValid(currcon->conparentid) &&
+ get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE)
+ RemoveInheritedConstraint(conrel, tgrel, currcon->conrelid, conoid);
}
else if (changed) /* Create triggers */
{
@@ -12566,13 +12661,40 @@ AlterConstrEnforceabilityRecurse(List **wqueue, ATAlterConstraint *cmdcon,
true, NULL, 1, &pkey);
while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
- ATExecAlterConstrEnforceability(wqueue, cmdcon, conrel, tgrel, fkrelid,
- pkrelid, childtup, lockmode,
- ReferencedParentDelTrigger,
- ReferencedParentUpdTrigger,
- ReferencingParentInsTrigger,
- ReferencingParentUpdTrigger);
+ {
+ Form_pg_constraint childcon;
+
+ childcon = (Form_pg_constraint) GETSTRUCT(childtup);
+
+ /*
+ * When the parent constraint is modified to be ENFORCED, and the
+ * child constraint is attached to the parent constraint (which is
+ * already ENFORCED), some constraints and action triggers on the
+ * child table may become redundant and need to be removed.
+ */
+ if (cmdcon->is_enforced && childcon->conenforced)
+ {
+ if (currcon->confrelid == pkrelid)
+ {
+ Relation rel = table_open(childcon->conrelid, lockmode);
+
+ AttachPartitionForeignKey(wqueue, rel, childcon->oid,
+ conoid,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger,
+ tgrel);
+ table_close(rel, NoLock);
+ }
+ }
+ else
+ ATExecAlterConstrEnforceability(wqueue, cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, childtup, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
+ }
systable_endscan(pscan);
}
@@ -20746,7 +20868,9 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
{
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
+ HeapTuple parentContup;
Form_pg_constraint conform;
+ Oid parentConstrIsEnforced;
contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
if (!HeapTupleIsValid(contup))
@@ -20765,12 +20889,34 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
continue;
}
+ /* Get the enforcibility of the parent constraint */
+ parentContup = SearchSysCache1(CONSTROID,
+ ObjectIdGetDatum(conform->conparentid));
+ if (!HeapTupleIsValid(parentContup))
+ elog(ERROR, "cache lookup failed for constraint %u",
+ conform->conparentid);
+ parentConstrIsEnforced =
+ ((Form_pg_constraint) GETSTRUCT(parentContup))->conenforced;
+ ReleaseSysCache(parentContup);
+
/*
* The constraint on this table must be marked no longer a child of
* the parent's constraint, as do its check triggers.
*/
ConstraintSetParentConstraint(fk->conoid, InvalidOid, InvalidOid);
+ /*
+ * Unsetting the parent is sufficient when the parent constraint is
+ * NOT ENFORCED and the child constraint is ENFORCED, as we link them
+ * by setting the constraint parent, while leaving the rest unchanged.
+ * For more details, see AttachPartitionForeignKey().
+ */
+ if (!parentConstrIsEnforced && fk->conenforced)
+ {
+ ReleaseSysCache(contup);
+ continue;
+ }
+
/*
* Also, look up the partition's "check" triggers corresponding to the
* ENFORCED constraint being detached and detach them from the parent
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 1d0a4548732..e4e45d0668a 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -1673,7 +1673,6 @@ ALTER TABLE fk_partitioned_fk ADD CONSTRAINT fk_partitioned_fk_a_b_fkey FOREIGN
REFERENCES fk_notpartitioned_pk NOT ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
-ALTER TABLE fk_partitioned_fk_2 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
CREATE TABLE fk_partitioned_fk_3 (fdrop1 int, fdrop2 int, fdrop3 int, fdrop4 int, b int, a int)
@@ -1682,8 +1681,65 @@ ALTER TABLE fk_partitioned_fk_3 DROP COLUMN fdrop1, DROP COLUMN fdrop2,
DROP COLUMN fdrop3, DROP COLUMN fdrop4;
CREATE TABLE fk_partitioned_fk_3_0 PARTITION OF fk_partitioned_fk_3 FOR VALUES WITH (MODULUS 5, REMAINDER 0);
CREATE TABLE fk_partitioned_fk_3_1 PARTITION OF fk_partitioned_fk_3 FOR VALUES WITH (MODULUS 5, REMAINDER 1);
+-- Merge the not-enforced parent constraint with the enforced and not-enforced child constraints
+ALTER TABLE fk_partitioned_fk_3_0 ADD CONSTRAINT fk_partitioned_fk_3_0_a_b_fkey FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ENFORCED;
+ALTER TABLE fk_partitioned_fk_3_1 ADD CONSTRAINT fk_partitioned_fk_3_1_a_b_fkey FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk_3 ADD CONSTRAINT fk_partitioned_fk_3_a_b_fkey FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+-- Merge the non-enforced parent constraint with both the enforced and
+-- non-enforced child constraints, where the referenced table is partitioned.
+CREATE TABLE fk_partitioned_pk (a int, b int, PRIMARY KEY (a, b)) PARTITION BY RANGE (a, b);
+CREATE TABLE fk_partitioned_pk_1 PARTITION OF fk_partitioned_pk FOR VALUES FROM (0,0) TO (1000,1000);
+-- Merge the enforced parent constraint with the enforced and not-enforced child constraints.
+ALTER TABLE fk_partitioned_fk_3_0 ADD CONSTRAINT fk_partitioned_fk_3_0_a_b_fkey1 FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk ENFORCED;
+ALTER TABLE fk_partitioned_fk_3_1 ADD CONSTRAINT fk_partitioned_fk_3_1_a_b_fkey1 FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk_3 ADD CONSTRAINT fk_partitioned_fk_3_a_b_fkey1 FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk ENFORCED;
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+ conname | conenforced | convalidated | conrelid | confrelid
+---------------------------------+-------------+--------------+-----------------------+----------------------
+ fk_partitioned_fk_3_0_a_b_fkey | t | t | fk_partitioned_fk_3_0 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey | f | f | fk_partitioned_fk_3_1 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_a_b_fkey | f | f | fk_partitioned_fk_3 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_0_a_b_fkey1 | t | t | fk_partitioned_fk_3_0 | fk_partitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey1 | t | t | fk_partitioned_fk_3_1 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey1 | t | t | fk_partitioned_fk_3 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey2 | t | t | fk_partitioned_fk_3 | fk_partitioned_pk_1
+(7 rows)
+
+ALTER TABLE fk_partitioned_fk_3 DETACH PARTITION fk_partitioned_fk_3_0;
+ALTER TABLE fk_partitioned_fk_3 ATTACH PARTITION fk_partitioned_fk_3_0 FOR VALUES WITH (MODULUS 5, REMAINDER 0);
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+ conname | conenforced | convalidated | conrelid | confrelid
+---------------------------------+-------------+--------------+-----------------------+----------------------
+ fk_partitioned_fk_3_0_a_b_fkey | t | t | fk_partitioned_fk_3_0 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey | f | f | fk_partitioned_fk_3_1 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_a_b_fkey | f | f | fk_partitioned_fk_3 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_0_a_b_fkey1 | t | t | fk_partitioned_fk_3_0 | fk_partitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey1 | t | t | fk_partitioned_fk_3_1 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey1 | t | t | fk_partitioned_fk_3 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey2 | t | t | fk_partitioned_fk_3 | fk_partitioned_pk_1
+(7 rows)
+
+ALTER TABLE fk_partitioned_fk_3 ALTER CONSTRAINT fk_partitioned_fk_3_a_b_fkey1 NOT ENFORCED;
+-- Merging an enforced parent constraint (validated) with a not-enforced child
+-- constraint will implicitly change the child constraint to enforced and apply
+-- the validation as well.
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_3
FOR VALUES FROM (2000,2000) TO (3000,3000);
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+ conname | conenforced | convalidated | conrelid | confrelid
+---------------------------------+-------------+--------------+-----------------------+----------------------
+ fk_partitioned_fk_3_0_a_b_fkey | t | t | fk_partitioned_fk_3_0 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey | t | t | fk_partitioned_fk_3_1 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_a_b_fkey | t | t | fk_partitioned_fk_3 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_0_a_b_fkey1 | f | f | fk_partitioned_fk_3_0 | fk_partitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey1 | f | f | fk_partitioned_fk_3_1 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey1 | f | f | fk_partitioned_fk_3 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey2 | f | f | fk_partitioned_fk_3 | fk_partitioned_pk_1
+(7 rows)
+
-- Creating a foreign key with ONLY on a partitioned table referencing
-- a non-partitioned table fails.
ALTER TABLE ONLY fk_partitioned_fk ADD FOREIGN KEY (a, b)
@@ -1698,22 +1754,22 @@ INSERT INTO fk_partitioned_fk_1 (a,b) VALUES (500, 501);
ERROR: insert or update on table "fk_partitioned_fk_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
DETAIL: Key (a, b)=(500, 501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (1500, 1501);
-ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_2_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
DETAIL: Key (a, b)=(1500, 1501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk_2 (a,b) VALUES (1500, 1501);
-ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_2_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
DETAIL: Key (a, b)=(1500, 1501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (2500, 2502);
-ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_3_1_a_b_fkey"
DETAIL: Key (a, b)=(2500, 2502) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk_3 (a,b) VALUES (2500, 2502);
-ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_3_1_a_b_fkey"
DETAIL: Key (a, b)=(2500, 2502) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (2501, 2503);
-ERROR: insert or update on table "fk_partitioned_fk_3_0" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_0" violates foreign key constraint "fk_partitioned_fk_3_0_a_b_fkey"
DETAIL: Key (a, b)=(2501, 2503) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk_3 (a,b) VALUES (2501, 2503);
-ERROR: insert or update on table "fk_partitioned_fk_3_0" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_0" violates foreign key constraint "fk_partitioned_fk_3_0_a_b_fkey"
DETAIL: Key (a, b)=(2501, 2503) is not present in table "fk_notpartitioned_pk".
-- but if we insert the values that make them valid, then they work
INSERT INTO fk_notpartitioned_pk VALUES (500, 501), (1500, 1501),
@@ -1724,7 +1780,7 @@ INSERT INTO fk_partitioned_fk (a,b) VALUES (2500, 2502);
INSERT INTO fk_partitioned_fk (a,b) VALUES (2501, 2503);
-- this update fails because there is no referenced row
UPDATE fk_partitioned_fk SET a = a + 1 WHERE a = 2501;
-ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_3_1_a_b_fkey"
DETAIL: Key (a, b)=(2502, 2503) is not present in table "fk_notpartitioned_pk".
-- but we can fix it thusly:
INSERT INTO fk_notpartitioned_pk (a,b) VALUES (2502, 2503);
@@ -1867,8 +1923,6 @@ WHERE conrelid::regclass::text like 'fk_partitioned_fk%' ORDER BY oid::regclass:
DROP TABLE fk_partitioned_fk, fk_notpartitioned_pk;
-- NOT VALID foreign key on a non-partitioned table referencing a partitioned table
-CREATE TABLE fk_partitioned_pk (a int, b int, PRIMARY KEY (a, b)) PARTITION BY RANGE (a, b);
-CREATE TABLE fk_partitioned_pk_1 PARTITION OF fk_partitioned_pk FOR VALUES FROM (0,0) TO (1000,1000);
CREATE TABLE fk_notpartitioned_fk (b int, a int);
ALTER TABLE fk_notpartitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk NOT VALID;
-- Constraint will be invalid.
@@ -2084,7 +2138,7 @@ CREATE TABLE fk_partitioned_fk_2 (b int, a int,
FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
BEGIN;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
--- should be having two constraints
+-- should only have one constraint
\d fk_partitioned_fk_2
Table "public.fk_partitioned_fk_2"
Column | Type | Collation | Nullable | Default
@@ -2093,7 +2147,6 @@ ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN
a | integer | | |
Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
Foreign-key constraints:
- "fk_partitioned_fk_2_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED
TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
DROP TABLE fk_partitioned_fk_2;
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index 7e5be0702da..fa51c7110ef 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -1248,18 +1248,49 @@ CREATE TABLE fk_partitioned_fk_1 (fdrop1 int, fdrop2 int, a int, fdrop3 int, b i
REFERENCES fk_notpartitioned_pk NOT ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
-ALTER TABLE fk_partitioned_fk_2 ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+
CREATE TABLE fk_partitioned_fk_3 (fdrop1 int, fdrop2 int, fdrop3 int, fdrop4 int, b int, a int)
PARTITION BY HASH (a);
ALTER TABLE fk_partitioned_fk_3 DROP COLUMN fdrop1, DROP COLUMN fdrop2,
DROP COLUMN fdrop3, DROP COLUMN fdrop4;
CREATE TABLE fk_partitioned_fk_3_0 PARTITION OF fk_partitioned_fk_3 FOR VALUES WITH (MODULUS 5, REMAINDER 0);
CREATE TABLE fk_partitioned_fk_3_1 PARTITION OF fk_partitioned_fk_3 FOR VALUES WITH (MODULUS 5, REMAINDER 1);
+-- Merge the not-enforced parent constraint with the enforced and not-enforced child constraints
+ALTER TABLE fk_partitioned_fk_3_0 ADD CONSTRAINT fk_partitioned_fk_3_0_a_b_fkey FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ENFORCED;
+ALTER TABLE fk_partitioned_fk_3_1 ADD CONSTRAINT fk_partitioned_fk_3_1_a_b_fkey FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk_3 ADD CONSTRAINT fk_partitioned_fk_3_a_b_fkey FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+
+-- Merge the non-enforced parent constraint with both the enforced and
+-- non-enforced child constraints, where the referenced table is partitioned.
+CREATE TABLE fk_partitioned_pk (a int, b int, PRIMARY KEY (a, b)) PARTITION BY RANGE (a, b);
+CREATE TABLE fk_partitioned_pk_1 PARTITION OF fk_partitioned_pk FOR VALUES FROM (0,0) TO (1000,1000);
+-- Merge the enforced parent constraint with the enforced and not-enforced child constraints.
+ALTER TABLE fk_partitioned_fk_3_0 ADD CONSTRAINT fk_partitioned_fk_3_0_a_b_fkey1 FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk ENFORCED;
+ALTER TABLE fk_partitioned_fk_3_1 ADD CONSTRAINT fk_partitioned_fk_3_1_a_b_fkey1 FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk_3 ADD CONSTRAINT fk_partitioned_fk_3_a_b_fkey1 FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk ENFORCED;
+
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+
+ALTER TABLE fk_partitioned_fk_3 DETACH PARTITION fk_partitioned_fk_3_0;
+ALTER TABLE fk_partitioned_fk_3 ATTACH PARTITION fk_partitioned_fk_3_0 FOR VALUES WITH (MODULUS 5, REMAINDER 0);
+
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+
+ALTER TABLE fk_partitioned_fk_3 ALTER CONSTRAINT fk_partitioned_fk_3_a_b_fkey1 NOT ENFORCED;
+
+-- Merging an enforced parent constraint (validated) with a not-enforced child
+-- constraint will implicitly change the child constraint to enforced and apply
+-- the validation as well.
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_3
FOR VALUES FROM (2000,2000) TO (3000,3000);
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+
-- Creating a foreign key with ONLY on a partitioned table referencing
-- a non-partitioned table fails.
ALTER TABLE ONLY fk_partitioned_fk ADD FOREIGN KEY (a, b)
@@ -1386,8 +1417,6 @@ CREATE TABLE fk_partitioned_fk_3_1 (a int, b int);
DROP TABLE fk_partitioned_fk, fk_notpartitioned_pk;
-- NOT VALID foreign key on a non-partitioned table referencing a partitioned table
-CREATE TABLE fk_partitioned_pk (a int, b int, PRIMARY KEY (a, b)) PARTITION BY RANGE (a, b);
-CREATE TABLE fk_partitioned_pk_1 PARTITION OF fk_partitioned_pk FOR VALUES FROM (0,0) TO (1000,1000);
CREATE TABLE fk_notpartitioned_fk (b int, a int);
ALTER TABLE fk_notpartitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk NOT VALID;
@@ -1528,7 +1557,7 @@ CREATE TABLE fk_partitioned_fk_2 (b int, a int,
FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
BEGIN;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
--- should be having two constraints
+-- should only have one constraint
\d fk_partitioned_fk_2
DROP TABLE fk_partitioned_fk_2;
ROLLBACK;
--
2.49.0
On Thu, Mar 27, 2025 at 6:28 PM Peter Eisentraut <peter@eisentraut.org> wrote:
On 25.03.25 17:48, Peter Eisentraut wrote:
I have committed patches 0001 through 0003. I made some small changes:
I will work through the remaining patches. It looks good to me so far.
For the time being, here are the remaining patches rebased.
I think you could squash these together at this point. This is
especially useful since 0003 overwrites part of the changes in 0002, so
it's better to see them all together.Some details:
In CloneFkReferenced() and also in DetachPartitionFinalize(), you have
this change:- fkconstraint->initially_valid = true; + fkconstraint->initially_valid = constrForm->convalidated;I'm having a hard time understanding this. Is this an pre-existing
problem? What does this change do?
No issue for the master head since constraints are always valid for
newly created tables. However, I wanted to ensure that the validation
status aligns with enforceability. When constraints are not enforced,
the convalidated flag must be false, so I didn't want to mark it as
true blindly, so fetching its value.
Most of the other stuff is mechanical and fits into existing structures,
so it seems ok.Small cosmetic suggestion: write count(*) instead of count(1). This
fits better with existing practices.
Ok.
The merge rules for inheritance and partitioning are still confusing. I
don't understand the behavior inh_check_constraint10 in the inherit.sql
test:+-- the invalid state of the child constraint will be ignored here. +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;Why is that correct? At least we should explain it here, or somewhere.
A NOT ENFORCED constraint will be considered NOT VALID, so the next
constraint, even if specified with a NOT VALID or VALID clause, will
be ignored. I'll improve the comment a bit.
I'm also confused about the changes of the constraint names in the
foreign_key.sql test:-ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_a_b_fkey" +ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_2_a_b_fkey"And then patch 0003 changes it again. This seems pretty random. We
should make sure that tests don't contain unrelated changes like that.
(Or at least it's not clear why they are related.)
I have fixed it in the v19 version, which I just posted a few moments ago.
There is also in 0002
+-- should be having two constraints
and then in 0003:
--- should be having two constraints +-- should only have one constraintSo another reason for squashing these together, so we don't have
confusing intermediate states.
Sure.
That said, is there a simpler way? Patch 0003 appears to add a lot of
complexity. Could we make this simpler by saying, if you have otherwise
matching constraints with different enforceability, make this an error.
Then users can themselves adjust the enforceability how they want to
make it match.
We can simply discard this patch, as it still reflects the correct
behavior. It creates a new constraint without affecting the existing
constraint with differing enforceability on the child. I noticed
similar behavior with deferrability -- when it differs, the
constraints are not merged, and a new constraint is created on the
child. Let me know your thoughts so I can avoid squashing patch 0006.
Regards,
Amul
On 2025-Mar-27, Amul Sul wrote:
On Thu, Mar 27, 2025 at 6:28 PM Peter Eisentraut <peter@eisentraut.org> wrote:
That said, is there a simpler way? Patch 0003 appears to add a lot of
complexity. Could we make this simpler by saying, if you have otherwise
matching constraints with different enforceability, make this an error.
Then users can themselves adjust the enforceability how they want to
make it match.We can simply discard this patch, as it still reflects the correct
behavior. It creates a new constraint without affecting the existing
constraint with differing enforceability on the child. I noticed
similar behavior with deferrability -- when it differs, the
constraints are not merged, and a new constraint is created on the
child. Let me know your thoughts so I can avoid squashing patch 0006.
I didn't read that patch and I don't know what level of complexity we're
talking about, but the idea of creating a second constraint beside an
existing one itches me. I'm pretty certain most users would rather not
end up with redundant constraints that only differ in enforceability or
whatever other properties. I failed to realize that this was happening
when adding FKs on partitioned tables, and I now think it was a mistake.
(As I said in some previous thread, I'd rather have this kind of
situation raise an error so that the user can do something about it,
rather than silently moving ahead with a worse solution like creating a
redundant constraint.)
--
Álvaro Herrera PostgreSQL Developer — https://www.EnterpriseDB.com/
"After a quick R of TFM, all I can say is HOLY CR** THAT IS COOL! PostgreSQL was
amazing when I first started using it at 7.2, and I'm continually astounded by
learning new features and techniques made available by the continuing work of
the development team."
Berend Tober, http://archives.postgresql.org/pgsql-hackers/2007-08/msg01009.php
On Thu, Mar 27, 2025 at 7:45 PM Álvaro Herrera <alvherre@alvh.no-ip.org> wrote:
On 2025-Mar-27, Amul Sul wrote:
On Thu, Mar 27, 2025 at 6:28 PM Peter Eisentraut <peter@eisentraut.org> wrote:
That said, is there a simpler way? Patch 0003 appears to add a lot of
complexity. Could we make this simpler by saying, if you have otherwise
matching constraints with different enforceability, make this an error.
Then users can themselves adjust the enforceability how they want to
make it match.We can simply discard this patch, as it still reflects the correct
behavior. It creates a new constraint without affecting the existing
constraint with differing enforceability on the child. I noticed
similar behavior with deferrability -- when it differs, the
constraints are not merged, and a new constraint is created on the
child. Let me know your thoughts so I can avoid squashing patch 0006.I didn't read that patch and I don't know what level of complexity we're
talking about, but the idea of creating a second constraint beside an
existing one itches me. I'm pretty certain most users would rather not
end up with redundant constraints that only differ in enforceability or
whatever other properties. I failed to realize that this was happening
when adding FKs on partitioned tables, and I now think it was a mistake.
(As I said in some previous thread, I'd rather have this kind of
situation raise an error so that the user can do something about it,
rather than silently moving ahead with a worse solution like creating a
redundant constraint.)
Okay, in the attached version, I’ve added an error in
tryAttachPartitionForeignKey() if the enforceability is different.
Please have a look at the 0005 patch and let me know if it looks good.
The rest of the patches remain unchanged.
I haven’t squashed the patches because, if we decide to keep the error
and avoid further complexity, we can simply discard the 0006 patch.
Regards,
Amul
Attachments:
v20-0004-Remove-hastriggers-flag-check-before-fetching-FK.patchapplication/octet-stream; name=v20-0004-Remove-hastriggers-flag-check-before-fetching-FK.patchDownload
From 4d88157a2f98aa31ea3e752d3228ba866d032d2b Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Thu, 27 Mar 2025 16:15:05 +0530
Subject: [PATCH v20 4/6] Remove hastriggers flag check before fetching FK
constraints.
With NOT ENFORCED, foreign key constraints will be created without
triggers. Therefore, the criteria for fetching foreign keys based on
the presence of triggers no longer apply.
----
NOTE: This patch is intended to reduce the diff noise from the main
patch and is not meant to be committed separately. It should be
squashed with the main patch that adds ENFORCED/NOT ENFORCED.
----
---
src/backend/utils/cache/relcache.c | 5 -
src/bin/pg_dump/pg_dump.c | 8 +-
src/bin/psql/describe.c | 226 ++++++++++++++---------------
3 files changed, 108 insertions(+), 131 deletions(-)
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 9f54a9e72b7..e6721056536 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4666,11 +4666,6 @@ RelationGetFKeyList(Relation relation)
if (relation->rd_fkeyvalid)
return relation->rd_fkeylist;
- /* Fast path: non-partitioned tables without triggers can't have FKs */
- if (!relation->rd_rel->relhastriggers &&
- relation->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
- return NIL;
-
/*
* We build the list we intend to return (in the caller's context) while
* doing the scan. After successfully completing the scan, we copy that
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index e41e645f649..dc9797aa251 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -8016,13 +8016,7 @@ getConstraints(Archive *fout, TableInfo tblinfo[], int numTables)
{
TableInfo *tinfo = &tblinfo[i];
- /*
- * For partitioned tables, foreign keys have no triggers so they must
- * be included anyway in case some foreign keys are defined.
- */
- if ((!tinfo->hastriggers &&
- tinfo->relkind != RELKIND_PARTITIONED_TABLE) ||
- !(tinfo->dobj.dump & DUMP_COMPONENT_DEFINITION))
+ if (!(tinfo->dobj.dump & DUMP_COMPONENT_DEFINITION))
continue;
/* OK, we need info for this table */
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index bf565afcc4e..e4a4c5071dd 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2550,136 +2550,124 @@ describeOneTableDetails(const char *schemaname,
PQclear(result);
}
- /*
- * Print foreign-key constraints (there are none if no triggers,
- * except if the table is partitioned, in which case the triggers
- * appear in the partitions)
- */
- if (tableinfo.hastriggers ||
- tableinfo.relkind == RELKIND_PARTITIONED_TABLE)
+ /* Print foreign-key constraints */
+ if (pset.sversion >= 120000 &&
+ (tableinfo.ispartition || tableinfo.relkind == RELKIND_PARTITIONED_TABLE))
{
- if (pset.sversion >= 120000 &&
- (tableinfo.ispartition || tableinfo.relkind == RELKIND_PARTITIONED_TABLE))
+ /*
+ * Put the constraints defined in this table first, followed
+ * by the constraints defined in ancestor partitioned tables.
+ */
+ printfPQExpBuffer(&buf,
+ "SELECT conrelid = '%s'::pg_catalog.regclass AS sametable,\n"
+ " conname,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef,\n"
+ " conrelid::pg_catalog.regclass AS ontable\n"
+ " FROM pg_catalog.pg_constraint,\n"
+ " pg_catalog.pg_partition_ancestors('%s')\n"
+ " WHERE conrelid = relid AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
+ "ORDER BY sametable DESC, conname;",
+ oid, oid);
+ }
+ else
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT true as sametable, conname,\n"
+ " pg_catalog.pg_get_constraintdef(r.oid, true) as condef,\n"
+ " conrelid::pg_catalog.regclass AS ontable\n"
+ "FROM pg_catalog.pg_constraint r\n"
+ "WHERE r.conrelid = '%s' AND r.contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n",
+ oid);
+
+ if (pset.sversion >= 120000)
+ appendPQExpBufferStr(&buf, " AND conparentid = 0\n");
+ appendPQExpBufferStr(&buf, "ORDER BY conname");
+ }
+
+ result = PSQLexec(buf.data);
+ if (!result)
+ goto error_return;
+ else
+ tuples = PQntuples(result);
+
+ if (tuples > 0)
+ {
+ int i_sametable = PQfnumber(result, "sametable"),
+ i_conname = PQfnumber(result, "conname"),
+ i_condef = PQfnumber(result, "condef"),
+ i_ontable = PQfnumber(result, "ontable");
+
+ printTableAddFooter(&cont, _("Foreign-key constraints:"));
+ for (i = 0; i < tuples; i++)
{
/*
- * Put the constraints defined in this table first, followed
- * by the constraints defined in ancestor partitioned tables.
+ * Print untranslated constraint name and definition. Use
+ * a "TABLE tab" prefix when the constraint is defined in
+ * a parent partitioned table.
*/
- printfPQExpBuffer(&buf,
- "SELECT conrelid = '%s'::pg_catalog.regclass AS sametable,\n"
- " conname,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef,\n"
- " conrelid::pg_catalog.regclass AS ontable\n"
- " FROM pg_catalog.pg_constraint,\n"
- " pg_catalog.pg_partition_ancestors('%s')\n"
- " WHERE conrelid = relid AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
- "ORDER BY sametable DESC, conname;",
- oid, oid);
- }
- else
- {
- printfPQExpBuffer(&buf,
- "SELECT true as sametable, conname,\n"
- " pg_catalog.pg_get_constraintdef(r.oid, true) as condef,\n"
- " conrelid::pg_catalog.regclass AS ontable\n"
- "FROM pg_catalog.pg_constraint r\n"
- "WHERE r.conrelid = '%s' AND r.contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n",
- oid);
-
- if (pset.sversion >= 120000)
- appendPQExpBufferStr(&buf, " AND conparentid = 0\n");
- appendPQExpBufferStr(&buf, "ORDER BY conname");
- }
-
- result = PSQLexec(buf.data);
- if (!result)
- goto error_return;
- else
- tuples = PQntuples(result);
-
- if (tuples > 0)
- {
- int i_sametable = PQfnumber(result, "sametable"),
- i_conname = PQfnumber(result, "conname"),
- i_condef = PQfnumber(result, "condef"),
- i_ontable = PQfnumber(result, "ontable");
-
- printTableAddFooter(&cont, _("Foreign-key constraints:"));
- for (i = 0; i < tuples; i++)
- {
- /*
- * Print untranslated constraint name and definition. Use
- * a "TABLE tab" prefix when the constraint is defined in
- * a parent partitioned table.
- */
- if (strcmp(PQgetvalue(result, i, i_sametable), "f") == 0)
- printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
- PQgetvalue(result, i, i_ontable),
- PQgetvalue(result, i, i_conname),
- PQgetvalue(result, i, i_condef));
- else
- printfPQExpBuffer(&buf, " \"%s\" %s",
- PQgetvalue(result, i, i_conname),
- PQgetvalue(result, i, i_condef));
-
- printTableAddFooter(&cont, buf.data);
- }
- }
- PQclear(result);
- }
-
- /* print incoming foreign-key references */
- if (tableinfo.hastriggers ||
- tableinfo.relkind == RELKIND_PARTITIONED_TABLE)
- {
- if (pset.sversion >= 120000)
- {
- printfPQExpBuffer(&buf,
- "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
- " FROM pg_catalog.pg_constraint c\n"
- " WHERE confrelid IN (SELECT pg_catalog.pg_partition_ancestors('%s')\n"
- " UNION ALL VALUES ('%s'::pg_catalog.regclass))\n"
- " AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
- "ORDER BY conname;",
- oid, oid);
- }
- else
- {
- printfPQExpBuffer(&buf,
- "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
- " FROM pg_catalog.pg_constraint\n"
- " WHERE confrelid = %s AND contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n"
- "ORDER BY conname;",
- oid);
- }
-
- result = PSQLexec(buf.data);
- if (!result)
- goto error_return;
- else
- tuples = PQntuples(result);
-
- if (tuples > 0)
- {
- int i_conname = PQfnumber(result, "conname"),
- i_ontable = PQfnumber(result, "ontable"),
- i_condef = PQfnumber(result, "condef");
-
- printTableAddFooter(&cont, _("Referenced by:"));
- for (i = 0; i < tuples; i++)
- {
+ if (strcmp(PQgetvalue(result, i, i_sametable), "f") == 0)
printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
PQgetvalue(result, i, i_ontable),
PQgetvalue(result, i, i_conname),
PQgetvalue(result, i, i_condef));
+ else
+ printfPQExpBuffer(&buf, " \"%s\" %s",
+ PQgetvalue(result, i, i_conname),
+ PQgetvalue(result, i, i_condef));
- printTableAddFooter(&cont, buf.data);
- }
+ printTableAddFooter(&cont, buf.data);
}
- PQclear(result);
}
+ PQclear(result);
+
+ /* print incoming foreign-key references */
+ if (pset.sversion >= 120000)
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
+ " FROM pg_catalog.pg_constraint c\n"
+ " WHERE confrelid IN (SELECT pg_catalog.pg_partition_ancestors('%s')\n"
+ " UNION ALL VALUES ('%s'::pg_catalog.regclass))\n"
+ " AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
+ "ORDER BY conname;",
+ oid, oid);
+ }
+ else
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
+ " FROM pg_catalog.pg_constraint\n"
+ " WHERE confrelid = %s AND contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n"
+ "ORDER BY conname;",
+ oid);
+ }
+
+ result = PSQLexec(buf.data);
+ if (!result)
+ goto error_return;
+ else
+ tuples = PQntuples(result);
+
+ if (tuples > 0)
+ {
+ int i_conname = PQfnumber(result, "conname"),
+ i_ontable = PQfnumber(result, "ontable"),
+ i_condef = PQfnumber(result, "condef");
+
+ printTableAddFooter(&cont, _("Referenced by:"));
+ for (i = 0; i < tuples; i++)
+ {
+ printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
+ PQgetvalue(result, i, i_ontable),
+ PQgetvalue(result, i, i_conname),
+ PQgetvalue(result, i, i_condef));
+
+ printTableAddFooter(&cont, buf.data);
+ }
+ }
+ PQclear(result);
/* print any row-level policies */
if (pset.sversion >= 90500)
--
2.43.5
v20-0005-Add-support-for-NOT-ENFORCED-in-foreign-key-cons.patchapplication/octet-stream; name=v20-0005-Add-support-for-NOT-ENFORCED-in-foreign-key-cons.patchDownload
From f7294dcf2a3de69475afee77ca0a41135c60f366 Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Fri, 28 Mar 2025 13:25:55 +0530
Subject: [PATCH v20 5/6] Add support for NOT ENFORCED in foreign key
constraints.
Typically, when a foreign key (FK) constraint is created on a table,
action and check triggers are added to maintain data integrity. With
this patch, if a constraint is marked as NOT ENFORCED, integrity
checks are no longer required, making these triggers unnecessary.
Consequently, when creating a NOT ENFORCED FK constraint, triggers
will not be created, and the constraint will be marked as NOT VALID.
Similarly, if an existing FK constraint is changed to NOT ENFORCED,
the associated triggers will be dropped, and the constraint will also
be marked as NOT VALID. Conversely, if a NOT ENFORCED FK constraint is
changed to ENFORCED, the necessary triggers will be created, and the
will be changed to VALID by performing necessary validation.
----
NOTE: In this patch, the tryAttachPartitionForeignKey() function will
not merge the constraint if the enforcibility differs. This will be
addressed in the next patch.
----
---
doc/src/sgml/catalogs.sgml | 2 +-
doc/src/sgml/ref/alter_table.sgml | 5 +-
doc/src/sgml/ref/create_table.sgml | 2 +-
src/backend/catalog/pg_constraint.c | 5 +-
src/backend/catalog/sql_features.txt | 2 +-
src/backend/commands/tablecmds.c | 471 ++++++++++++++++++----
src/backend/parser/gram.y | 11 +-
src/backend/parser/parse_utilcmd.c | 14 +-
src/backend/utils/cache/relcache.c | 1 +
src/include/nodes/parsenodes.h | 2 +
src/include/utils/rel.h | 3 +
src/test/regress/expected/constraints.out | 8 +-
src/test/regress/expected/foreign_key.out | 164 +++++++-
src/test/regress/expected/inherit.out | 81 ++--
src/test/regress/sql/foreign_key.sql | 114 +++++-
src/test/regress/sql/inherit.sql | 7 +
16 files changed, 744 insertions(+), 148 deletions(-)
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index fb050635551..014bb815665 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2620,7 +2620,7 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l
</para>
<para>
Is the constraint enforced?
- Currently, can be false only for CHECK constraints
+ Currently, can be false only for foreign keys and CHECK constraints
</para></entry>
</row>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 11d1bc7dbe1..ece438f0075 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -58,7 +58,7 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable>
ALTER [ COLUMN ] <replaceable class="parameter">column_name</replaceable> SET COMPRESSION <replaceable class="parameter">compression_method</replaceable>
ADD <replaceable class="parameter">table_constraint</replaceable> [ NOT VALID ]
ADD <replaceable class="parameter">table_constraint_using_index</replaceable>
- ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+ ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]
ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> [ INHERIT | NO INHERIT ]
VALIDATE CONSTRAINT <replaceable class="parameter">constraint_name</replaceable>
DROP CONSTRAINT [ IF EXISTS ] <replaceable class="parameter">constraint_name</replaceable> [ RESTRICT | CASCADE ]
@@ -589,7 +589,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
This form validates a foreign key or check constraint that was
previously created as <literal>NOT VALID</literal>, by scanning the
table to ensure there are no rows for which the constraint is not
- satisfied. Nothing happens if the constraint is already marked valid.
+ satisfied. If the constraint is not enforced, an error is thrown.
+ Nothing happens if the constraint is already marked valid.
(See <xref linkend="sql-altertable-notes"/> below for an explanation
of the usefulness of this command.)
</para>
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index e5c034d724e..4a41b2f5530 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -1409,7 +1409,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</para>
<para>
- This is currently only supported for <literal>CHECK</literal>
+ This is currently only supported for foreign key and <literal>CHECK</literal>
constraints.
</para>
</listitem>
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index ac80652baf2..0467e7442ff 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -100,8 +100,9 @@ CreateConstraintEntry(const char *constraintName,
ObjectAddresses *addrs_auto;
ObjectAddresses *addrs_normal;
- /* Only CHECK constraint can be not enforced */
- Assert(isEnforced || constraintType == CONSTRAINT_CHECK);
+ /* Only CHECK or FOREIGN KEY constraint can be not enforced */
+ Assert(isEnforced || constraintType == CONSTRAINT_CHECK ||
+ constraintType == CONSTRAINT_FOREIGN);
/* NOT ENFORCED constraint must be NOT VALID */
Assert(isEnforced || !isValidated);
diff --git a/src/backend/catalog/sql_features.txt b/src/backend/catalog/sql_features.txt
index 2f250d2c57b..ebe85337c28 100644
--- a/src/backend/catalog/sql_features.txt
+++ b/src/backend/catalog/sql_features.txt
@@ -281,7 +281,7 @@ F461 Named character sets NO
F471 Scalar subquery values YES
F481 Expanded NULL predicate YES
F491 Constraint management YES
-F492 Optional table constraint enforcement NO check constraints only
+F492 Optional table constraint enforcement YES except not-null constraints
F501 Features and conformance views YES
F501 Features and conformance views 01 SQL_FEATURES view YES
F501 Features and conformance views 02 SQL_SIZING view YES
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index afb25007613..045eb1a4b28 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -395,6 +395,14 @@ static ObjectAddress ATExecAlterConstraint(List **wqueue, Relation rel,
static bool ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon, Relation conrel,
Relation tgrel, Relation rel, HeapTuple contuple,
bool recurse, LOCKMODE lockmode);
+static bool ATExecAlterConstrEnforceability(List **wqueue, ATAlterConstraint *cmdcon,
+ Relation conrel, Relation tgrel,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, LOCKMODE lockmode,
+ Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger);
static bool ATExecAlterConstrDeferrability(List **wqueue, ATAlterConstraint *cmdcon,
Relation conrel, Relation tgrel, Relation rel,
HeapTuple contuple, bool recurse,
@@ -405,6 +413,14 @@ static bool ATExecAlterConstrInheritability(List **wqueue, ATAlterConstraint *cm
static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
bool deferrable, bool initdeferred,
List **otherrelids);
+static void AlterConstrEnforceabilityRecurse(List **wqueue, ATAlterConstraint *cmdcon,
+ Relation conrel, Relation tgrel,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, LOCKMODE lockmode,
+ Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger);
static void AlterConstrDeferrabilityRecurse(List **wqueue, ATAlterConstraint *cmdcon,
Relation conrel, Relation tgrel, Relation rel,
HeapTuple contuple, bool recurse,
@@ -10561,7 +10577,7 @@ addFkConstraint(addFkConstraintSides fkside,
CONSTRAINT_FOREIGN,
fkconstraint->deferrable,
fkconstraint->initdeferred,
- true, /* Is Enforced */
+ fkconstraint->is_enforced,
fkconstraint->initially_valid,
parentConstr,
RelationGetRelid(rel),
@@ -10679,21 +10695,23 @@ addFkRecurseReferenced(Constraint *fkconstraint, Relation rel,
Oid parentDelTrigger, Oid parentUpdTrigger,
bool with_period)
{
- Oid deleteTriggerOid,
- updateTriggerOid;
+ Oid deleteTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
Assert(CheckRelationLockedByMe(pkrel, ShareRowExclusiveLock, true));
Assert(CheckRelationLockedByMe(rel, ShareRowExclusiveLock, true));
/*
- * Create the action triggers that enforce the constraint.
+ * Create action triggers to enforce the constraint, or skip them if the
+ * constraint is NOT ENFORCED.
*/
- createForeignKeyActionTriggers(RelationGetRelid(rel),
- RelationGetRelid(pkrel),
- fkconstraint,
- parentConstr, indexOid,
- parentDelTrigger, parentUpdTrigger,
- &deleteTriggerOid, &updateTriggerOid);
+ if (fkconstraint->is_enforced)
+ createForeignKeyActionTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
+ fkconstraint,
+ parentConstr, indexOid,
+ parentDelTrigger, parentUpdTrigger,
+ &deleteTriggerOid, &updateTriggerOid);
/*
* If the referenced table is partitioned, recurse on ourselves to handle
@@ -10814,8 +10832,8 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
Oid parentInsTrigger, Oid parentUpdTrigger,
bool with_period)
{
- Oid insertTriggerOid,
- updateTriggerOid;
+ Oid insertTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
Assert(OidIsValid(parentConstr));
Assert(CheckRelationLockedByMe(rel, ShareRowExclusiveLock, true));
@@ -10827,29 +10845,32 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
errmsg("foreign key constraints are not supported on foreign tables")));
/*
- * Add the check triggers to it and, if necessary, schedule it to be
- * checked in Phase 3.
+ * Add check triggers if the constraint is ENFORCED, and if needed,
+ * schedule them to be checked in Phase 3.
*
* If the relation is partitioned, drill down to do it to its partitions.
*/
- createForeignKeyCheckTriggers(RelationGetRelid(rel),
- RelationGetRelid(pkrel),
- fkconstraint,
- parentConstr,
- indexOid,
- parentInsTrigger, parentUpdTrigger,
- &insertTriggerOid, &updateTriggerOid);
+ if (fkconstraint->is_enforced)
+ createForeignKeyCheckTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
+ fkconstraint,
+ parentConstr,
+ indexOid,
+ parentInsTrigger, parentUpdTrigger,
+ &insertTriggerOid, &updateTriggerOid);
if (rel->rd_rel->relkind == RELKIND_RELATION)
{
/*
* Tell Phase 3 to check that the constraint is satisfied by existing
- * rows. We can skip this during table creation, when requested
- * explicitly by specifying NOT VALID in an ADD FOREIGN KEY command,
- * and when we're recreating a constraint following a SET DATA TYPE
- * operation that did not impugn its validity.
+ * rows. We can skip this during table creation, when constraint is
+ * specified as NOT ENFORCED, or when requested explicitly by
+ * specifying NOT VALID in an ADD FOREIGN KEY command, and when we're
+ * recreating a constraint following a SET DATA TYPE operation that
+ * did not impugn its validity.
*/
- if (wqueue && !old_check_ok && !fkconstraint->skip_validation)
+ if (wqueue && !old_check_ok && !fkconstraint->skip_validation &&
+ fkconstraint->is_enforced)
{
NewConstraint *newcon;
AlteredTableInfo *tab;
@@ -11080,8 +11101,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
AttrNumber confdelsetcols[INDEX_MAX_KEYS];
Constraint *fkconstraint;
ObjectAddress address;
- Oid deleteTriggerOid,
- updateTriggerOid;
+ Oid deleteTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(constrOid));
if (!HeapTupleIsValid(tuple))
@@ -11141,8 +11162,9 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
fkconstraint->fk_del_set_cols = NIL;
fkconstraint->old_conpfeqop = NIL;
fkconstraint->old_pktable_oid = InvalidOid;
+ fkconstraint->is_enforced = constrForm->conenforced;
fkconstraint->skip_validation = false;
- fkconstraint->initially_valid = true;
+ fkconstraint->initially_valid = constrForm->convalidated;
/* set up colnames that are used to generate the constraint name */
for (int i = 0; i < numfks; i++)
@@ -11170,9 +11192,10 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
* parent OIDs for similar triggers that will be created on the
* partition in addFkRecurseReferenced().
*/
- GetForeignKeyActionTriggers(trigrel, constrOid,
- constrForm->confrelid, constrForm->conrelid,
- &deleteTriggerOid, &updateTriggerOid);
+ if (constrForm->conenforced)
+ GetForeignKeyActionTriggers(trigrel, constrOid,
+ constrForm->confrelid, constrForm->conrelid,
+ &deleteTriggerOid, &updateTriggerOid);
/* Add this constraint ... */
address = addFkConstraint(addFkReferencedSide,
@@ -11305,8 +11328,8 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
Oid indexOid;
ObjectAddress address;
ListCell *lc;
- Oid insertTriggerOid,
- updateTriggerOid;
+ Oid insertTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
bool with_period;
tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(parentConstrOid));
@@ -11338,17 +11361,18 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
mapped_conkey[i] = attmap->attnums[conkey[i] - 1];
/*
- * Get the "check" triggers belonging to the constraint to pass as
- * parent OIDs for similar triggers that will be created on the
- * partition in addFkRecurseReferencing(). They are also passed to
- * tryAttachPartitionForeignKey() below to simply assign as parents to
- * the partition's existing "check" triggers, that is, if the
- * corresponding constraints is deemed attachable to the parent
- * constraint.
+ * Get the "check" triggers belonging to the constraint, if it is
+ * ENFORCED, to pass as parent OIDs for similar triggers that will be
+ * created on the partition in addFkRecurseReferencing(). They are
+ * also passed to tryAttachPartitionForeignKey() below to simply
+ * assign as parents to the partition's existing "check" triggers,
+ * that is, if the corresponding constraints is deemed attachable to
+ * the parent constraint.
*/
- GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
- constrForm->confrelid, constrForm->conrelid,
- &insertTriggerOid, &updateTriggerOid);
+ if (constrForm->conenforced)
+ GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
+ constrForm->confrelid, constrForm->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
/*
* Before creating a new constraint, see whether any existing FKs are
@@ -11401,6 +11425,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
fkconstraint->fk_del_set_cols = NIL;
fkconstraint->old_conpfeqop = NIL;
fkconstraint->old_pktable_oid = InvalidOid;
+ fkconstraint->is_enforced = constrForm->conenforced;
fkconstraint->skip_validation = false;
fkconstraint->initially_valid = constrForm->convalidated;
for (int i = 0; i < numfks; i++)
@@ -11515,6 +11540,23 @@ tryAttachPartitionForeignKey(List **wqueue,
if (!HeapTupleIsValid(partcontup))
elog(ERROR, "cache lookup failed for constraint %u", fk->conoid);
partConstr = (Form_pg_constraint) GETSTRUCT(partcontup);
+
+ /*
+ * An error should be raised if the constraint enforceability is different.
+ * Returning false without raising an error, as we do for other attributes,
+ * could lead to a duplicate constraint with the same enforceability as the
+ * parent. While this may be acceptable, it may not be ideal. Therefore,
+ * it's better to raise an error and allow the user to correct the
+ * enforceability before proceeding.
+ */
+ if (partConstr->conenforced != parentConstr->conenforced)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("constraint \"%s\" enforceability conflicts with constraint \"%s\" on relation \"%s\"",
+ NameStr(parentConstr->conname),
+ NameStr(partConstr->conname),
+ RelationGetRelationName(partition))));
+
if (OidIsValid(partConstr->conparentid) ||
partConstr->condeferrable != parentConstr->condeferrable ||
partConstr->condeferred != parentConstr->condeferred ||
@@ -11561,8 +11603,7 @@ AttachPartitionForeignKey(List **wqueue,
bool queueValidation;
Oid partConstrFrelid;
Oid partConstrRelid;
- Oid insertTriggerOid,
- updateTriggerOid;
+ bool parentConstrIsEnforced;
/* Fetch the parent constraint tuple */
parentConstrTup = SearchSysCache1(CONSTROID,
@@ -11570,6 +11611,7 @@ AttachPartitionForeignKey(List **wqueue,
if (!HeapTupleIsValid(parentConstrTup))
elog(ERROR, "cache lookup failed for constraint %u", parentConstrOid);
parentConstr = (Form_pg_constraint) GETSTRUCT(parentConstrTup);
+ parentConstrIsEnforced = parentConstr->conenforced;
/* Fetch the child constraint tuple */
partcontup = SearchSysCache1(CONSTROID,
@@ -11619,17 +11661,24 @@ AttachPartitionForeignKey(List **wqueue,
/*
* Like the constraint, attach partition's "check" triggers to the
- * corresponding parent triggers.
+ * corresponding parent triggers if the constraint is ENFORCED. NOT
+ * ENFORCED constraints do not have these triggers.
*/
- GetForeignKeyCheckTriggers(trigrel,
- partConstrOid, partConstrFrelid, partConstrRelid,
- &insertTriggerOid, &updateTriggerOid);
- Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
- TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
- RelationGetRelid(partition));
- Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
- TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
- RelationGetRelid(partition));
+ if (parentConstrIsEnforced)
+ {
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
+ GetForeignKeyCheckTriggers(trigrel,
+ partConstrOid, partConstrFrelid, partConstrRelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
+ RelationGetRelid(partition));
+ Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
+ RelationGetRelid(partition));
+ }
/*
* We updated this pg_constraint row above to set its parent; validating
@@ -11743,6 +11792,10 @@ RemoveInheritedConstraint(Relation conrel, Relation trigrel, Oid conoid,
*
* The subroutine for tryAttachPartitionForeignKey handles the deletion of
* action triggers for the foreign key constraint.
+ *
+ * If valid confrelid and conrelid values are not provided, the respective
+ * trigger check will be skipped, and the trigger will be considered for
+ * removal.
*/
static void
DropForeignKeyConstraintTriggers(Relation trigrel, Oid conoid, Oid confrelid,
@@ -11763,10 +11816,27 @@ DropForeignKeyConstraintTriggers(Relation trigrel, Oid conoid, Oid confrelid,
Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
ObjectAddress trigger;
- if (trgform->tgconstrrelid != conrelid)
+ /* Invalid if trigger is not for a referential integrity constraint */
+ if (!OidIsValid(trgform->tgconstrrelid))
continue;
- if (trgform->tgrelid != confrelid)
+ if (OidIsValid(conrelid) && trgform->tgconstrrelid != conrelid)
continue;
+ if (OidIsValid(confrelid) && trgform->tgrelid != confrelid)
+ continue;
+
+ /* We should be droping trigger related to foreign key constraint */
+ Assert(trgform->tgfoid == F_RI_FKEY_CHECK_INS ||
+ trgform->tgfoid == F_RI_FKEY_CHECK_UPD ||
+ trgform->tgfoid == F_RI_FKEY_CASCADE_DEL ||
+ trgform->tgfoid == F_RI_FKEY_CASCADE_UPD ||
+ trgform->tgfoid == F_RI_FKEY_RESTRICT_DEL ||
+ trgform->tgfoid == F_RI_FKEY_RESTRICT_UPD ||
+ trgform->tgfoid == F_RI_FKEY_SETNULL_DEL ||
+ trgform->tgfoid == F_RI_FKEY_SETNULL_UPD ||
+ trgform->tgfoid == F_RI_FKEY_SETDEFAULT_DEL ||
+ trgform->tgfoid == F_RI_FKEY_SETDEFAULT_UPD ||
+ trgform->tgfoid == F_RI_FKEY_NOACTION_DEL ||
+ trgform->tgfoid == F_RI_FKEY_NOACTION_UPD);
/*
* The constraint is originally set up to contain this trigger as an
@@ -11979,6 +12049,11 @@ ATExecAlterConstraint(List **wqueue, Relation rel, ATAlterConstraint *cmdcon,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("constraint \"%s\" of relation \"%s\" is not a foreign key constraint",
cmdcon->conname, RelationGetRelationName(rel))));
+ if (cmdcon->alterEnforceability && currcon->contype != CONSTRAINT_FOREIGN)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot alter enforceability of constraint \"%s\" of relation \"%s\"",
+ cmdcon->conname, RelationGetRelationName(rel))));
if (cmdcon->alterInheritability &&
currcon->contype != CONSTRAINT_NOTNULL)
ereport(ERROR,
@@ -12058,7 +12133,7 @@ ATExecAlterConstraint(List **wqueue, Relation rel, ATAlterConstraint *cmdcon,
/*
* A subroutine of ATExecAlterConstraint that calls the respective routines for
- * altering constraint attributes.
+ * altering constraint's enforceability, deferrability or inheritability.
*/
static bool
ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon,
@@ -12066,16 +12141,35 @@ ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon,
HeapTuple contuple, bool recurse,
LOCKMODE lockmode)
{
+ Form_pg_constraint currcon;
bool changed = false;
List *otherrelids = NIL;
+ currcon = (Form_pg_constraint) GETSTRUCT(contuple);
+
/*
- * Do the catalog work for the deferrability change, recurse if necessary.
+ * Do the catalog work for the enforceability or deferrability change,
+ * recurse if necessary.
+ *
+ * Note that even if deferrability is requested to be altered along with
+ * enforceability, we don't need to explicitly update multiple entries in
+ * pg_trigger related to deferrability.
+ *
+ * Modifying enforceability involves either creating or dropping the
+ * trigger, during which the deferrability setting will be adjusted
+ * automatically.
*/
- if (cmdcon->alterDeferrability &&
- ATExecAlterConstrDeferrability(wqueue, cmdcon, conrel, tgrel, rel,
- contuple, recurse, &otherrelids,
- lockmode))
+ if (cmdcon->alterEnforceability &&
+ ATExecAlterConstrEnforceability(wqueue, cmdcon, conrel, tgrel,
+ currcon->conrelid, currcon->confrelid,
+ contuple, lockmode, InvalidOid,
+ InvalidOid, InvalidOid, InvalidOid))
+ changed = true;
+
+ else if (cmdcon->alterDeferrability &&
+ ATExecAlterConstrDeferrability(wqueue, cmdcon, conrel, tgrel, rel,
+ contuple, recurse, &otherrelids,
+ lockmode))
{
/*
* AlterConstrUpdateConstraintEntry already invalidated relcache for
@@ -12100,6 +12194,151 @@ ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon,
return changed;
}
+/*
+ * Returns true if the constraint's enforceability is altered.
+ *
+ * Depending on whether the constraint is being set to ENFORCED or NOT
+ * ENFORCED, it creates or drops the trigger accordingly.
+ *
+ * Note that we must recurse even when trying to change a 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. 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.
+ */
+static bool
+ATExecAlterConstrEnforceability(List **wqueue, ATAlterConstraint *cmdcon,
+ Relation conrel, Relation tgrel,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, LOCKMODE lockmode,
+ Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger)
+{
+ Form_pg_constraint currcon;
+ Oid conoid;
+ Relation rel;
+ bool changed = false;
+
+ /* Since this function recurses, it could be driven to stack overflow */
+ check_stack_depth();
+
+ Assert(cmdcon->alterEnforceability);
+
+ currcon = (Form_pg_constraint) GETSTRUCT(contuple);
+ conoid = currcon->oid;
+
+ /* Should be foreign key constraint */
+ Assert(currcon->contype == CONSTRAINT_FOREIGN);
+
+ rel = table_open(currcon->conrelid, lockmode);
+
+ if (currcon->conenforced != cmdcon->is_enforced)
+ {
+ AlterConstrUpdateConstraintEntry(cmdcon, conrel, contuple);
+ changed = true;
+ }
+
+ /* Drop triggers */
+ if (!cmdcon->is_enforced)
+ {
+ /*
+ * When setting a constraint to NOT ENFORCED, the constraint triggers
+ * need to be dropped. Therefore, we must process the child relations
+ * first, followed by the parent, to account for dependencies.
+ */
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE)
+ AlterConstrEnforceabilityRecurse(wqueue, cmdcon, conrel, tgrel,
+ fkrelid, pkrelid, contuple,
+ lockmode, InvalidOid, InvalidOid,
+ InvalidOid, InvalidOid);
+
+ /* Drop all the triggers */
+ DropForeignKeyConstraintTriggers(tgrel, conoid, InvalidOid, InvalidOid);
+ }
+ else if (changed) /* Create triggers */
+ {
+ Oid ReferencedDelTriggerOid = InvalidOid,
+ ReferencedUpdTriggerOid = InvalidOid,
+ ReferencingInsTriggerOid = InvalidOid,
+ ReferencingUpdTriggerOid = InvalidOid;
+
+ /* Prepare the minimal information required for trigger creation. */
+ Constraint *fkconstraint = makeNode(Constraint);
+
+ fkconstraint->conname = pstrdup(NameStr(currcon->conname));
+ fkconstraint->fk_matchtype = currcon->confmatchtype;
+ fkconstraint->fk_upd_action = currcon->confupdtype;
+ fkconstraint->fk_del_action = currcon->confdeltype;
+
+ /* Create referenced triggers */
+ if (currcon->conrelid == fkrelid)
+ createForeignKeyActionTriggers(currcon->conrelid,
+ currcon->confrelid,
+ fkconstraint,
+ conoid,
+ currcon->conindid,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ &ReferencedDelTriggerOid,
+ &ReferencedUpdTriggerOid);
+
+ /* Create referencing triggers */
+ if (currcon->confrelid == pkrelid)
+ createForeignKeyCheckTriggers(currcon->conrelid,
+ pkrelid,
+ fkconstraint,
+ conoid,
+ currcon->conindid,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger,
+ &ReferencingInsTriggerOid,
+ &ReferencingUpdTriggerOid);
+
+ /*
+ * Tell Phase 3 to check that the constraint is satisfied by existing
+ * rows.
+ */
+ if (rel->rd_rel->relkind == RELKIND_RELATION)
+ {
+ AlteredTableInfo *tab;
+ NewConstraint *newcon;
+
+ newcon = (NewConstraint *) palloc0(sizeof(NewConstraint));
+ newcon->name = fkconstraint->conname;
+ newcon->contype = CONSTR_FOREIGN;
+ newcon->refrelid = currcon->confrelid;
+ newcon->refindid = currcon->conindid;
+ newcon->conid = currcon->oid;
+ newcon->qual = (Node *) fkconstraint;
+
+ /* Find or create work queue entry for this table */
+ tab = ATGetQueueEntry(wqueue, rel);
+ tab->constraints = lappend(tab->constraints, newcon);
+ }
+
+ /*
+ * If the table at either end of the constraint is partitioned, we
+ * need to recurse and create triggers for each constraint that is a
+ * child of this one.
+ */
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE)
+ AlterConstrEnforceabilityRecurse(wqueue, cmdcon, conrel, tgrel,
+ fkrelid, pkrelid, contuple,
+ lockmode, ReferencedDelTriggerOid,
+ ReferencedUpdTriggerOid,
+ ReferencingInsTriggerOid,
+ ReferencingUpdTriggerOid);
+ }
+
+ table_close(rel, NoLock);
+
+ return changed;
+}
+
/*
* Returns true if the constraint's deferrability is altered.
*
@@ -12304,6 +12543,55 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
systable_endscan(tgscan);
}
+/*
+ * Invokes ATExecAlterConstrEnforceability for each constraint that is a child of
+ * the specified constraint.
+ *
+ * Note that this doesn't handle recursion the normal way, viz. by scanning the
+ * list of child relations and recursing; instead it uses the conparentid
+ * relationships. This may need to be reconsidered.
+ *
+ * The arguments to this function have the same meaning as the arguments to
+ * ATExecAlterConstrEnforceability.
+ */
+static void
+AlterConstrEnforceabilityRecurse(List **wqueue, ATAlterConstraint *cmdcon,
+ Relation conrel, Relation tgrel,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, LOCKMODE lockmode,
+ Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger)
+{
+ Form_pg_constraint currcon;
+ Oid conoid;
+ ScanKeyData pkey;
+ SysScanDesc pscan;
+ HeapTuple childtup;
+
+ currcon = (Form_pg_constraint) GETSTRUCT(contuple);
+ conoid = currcon->oid;
+
+ ScanKeyInit(&pkey,
+ Anum_pg_constraint_conparentid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+
+ pscan = systable_beginscan(conrel, ConstraintParentIndexId,
+ true, NULL, 1, &pkey);
+
+ while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
+ ATExecAlterConstrEnforceability(wqueue, cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, childtup, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
+
+ systable_endscan(pscan);
+}
+
/*
* Invokes ATExecAlterConstrDeferrability for each constraint that is a child of
* the specified constraint.
@@ -12364,11 +12652,25 @@ AlterConstrUpdateConstraintEntry(ATAlterConstraint *cmdcon, Relation conrel,
HeapTuple copyTuple;
Form_pg_constraint copy_con;
- Assert(cmdcon->alterDeferrability || cmdcon->alterInheritability);
+ Assert(cmdcon->alterEnforceability || cmdcon->alterDeferrability ||
+ cmdcon->alterInheritability);
copyTuple = heap_copytuple(contuple);
copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
+ if (cmdcon->alterEnforceability)
+ {
+ copy_con->conenforced = cmdcon->is_enforced;
+
+ /*
+ * 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.
+ */
+ copy_con->convalidated = cmdcon->is_enforced;
+ }
if (cmdcon->alterDeferrability)
{
copy_con->condeferrable = cmdcon->deferrable;
@@ -17088,9 +17390,9 @@ MergeConstraintsIntoExisting(Relation child_rel, Relation parent_rel)
NameStr(child_con->conname), RelationGetRelationName(child_rel))));
/*
- * A non-enforced child constraint cannot be merged with an
- * enforced parent constraint. However, the reverse is allowed,
- * where the child constraint is enforced.
+ * A NOT ENFORCED child constraint cannot be merged with an
+ * ENFORCED parent constraint. However, the reverse is allowed,
+ * where the child constraint is ENFORCED.
*/
if (parent_con->conenforced && !child_con->conenforced)
ereport(ERROR,
@@ -20461,8 +20763,6 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
Form_pg_constraint conform;
- Oid insertTriggerOid,
- updateTriggerOid;
contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
if (!HeapTupleIsValid(contup))
@@ -20489,17 +20789,25 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
/*
* Also, look up the partition's "check" triggers corresponding to the
- * constraint being detached and detach them from the parent triggers.
+ * ENFORCED constraint being detached and detach them from the parent
+ * triggers. NOT ENFORCED constraints do not have these triggers;
+ * therefore, this step is not needed.
*/
- GetForeignKeyCheckTriggers(trigrel,
- fk->conoid, fk->confrelid, fk->conrelid,
- &insertTriggerOid, &updateTriggerOid);
- Assert(OidIsValid(insertTriggerOid));
- TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
- RelationGetRelid(partRel));
- Assert(OidIsValid(updateTriggerOid));
- TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
- RelationGetRelid(partRel));
+ if (fk->conenforced)
+ {
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ Assert(OidIsValid(updateTriggerOid));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ }
/*
* Lastly, create the action triggers on the referenced table, using
@@ -20539,8 +20847,9 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
fkconstraint->conname = pstrdup(NameStr(conform->conname));
fkconstraint->deferrable = conform->condeferrable;
fkconstraint->initdeferred = conform->condeferred;
+ fkconstraint->is_enforced = conform->conenforced;
fkconstraint->skip_validation = true;
- fkconstraint->initially_valid = true;
+ fkconstraint->initially_valid = conform->convalidated;
/* a few irrelevant fields omitted here */
fkconstraint->pktable = NULL;
fkconstraint->fk_attrs = NIL;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 0fc502a3a40..27257ec5dc1 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -2662,6 +2662,8 @@ alter_table_cmd:
n->subtype = AT_AlterConstraint;
n->def = (Node *) c;
c->conname = $3;
+ if ($4 & (CAS_NOT_ENFORCED | CAS_ENFORCED))
+ c->alterEnforceability = true;
if ($4 & (CAS_DEFERRABLE | CAS_NOT_DEFERRABLE |
CAS_INITIALLY_DEFERRED | CAS_INITIALLY_IMMEDIATE))
c->alterDeferrability = true;
@@ -2670,7 +2672,10 @@ alter_table_cmd:
processCASbits($4, @4, "FOREIGN KEY",
&c->deferrable,
&c->initdeferred,
- NULL, NULL, &c->noinherit, yyscanner);
+ &c->is_enforced,
+ NULL,
+ &c->noinherit,
+ yyscanner);
$$ = (Node *) n;
}
/* ALTER TABLE <name> ALTER CONSTRAINT INHERIT */
@@ -4334,8 +4339,8 @@ ConstraintElem:
n->fk_del_set_cols = ($11)->deleteAction->cols;
processCASbits($12, @12, "FOREIGN KEY",
&n->deferrable, &n->initdeferred,
- NULL, &n->skip_validation, NULL,
- yyscanner);
+ &n->is_enforced, &n->skip_validation,
+ NULL, yyscanner);
n->initially_valid = !n->skip_validation;
$$ = (Node *) n;
}
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 896a7f2c59b..50f430a4e1a 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -2976,8 +2976,10 @@ transformFKConstraints(CreateStmtContext *cxt,
/*
* If CREATE TABLE or adding a column with NULL default, we can safely
- * skip validation of FK constraints, and nonetheless mark them valid.
- * (This will override any user-supplied NOT VALID flag.)
+ * skip validation of FK constraints, and mark them as valid based on the
+ * constraint enforcement flag, since NOT ENFORCED constraints must always
+ * be marked as NOT VALID. (This will override any user-supplied NOT VALID
+ * flag.)
*/
if (skipValidation)
{
@@ -2986,7 +2988,7 @@ transformFKConstraints(CreateStmtContext *cxt,
Constraint *constraint = (Constraint *) lfirst(fkclist);
constraint->skip_validation = true;
- constraint->initially_valid = true;
+ constraint->initially_valid = constraint->is_enforced;
}
}
@@ -3981,7 +3983,8 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
case CONSTR_ATTR_ENFORCED:
if (lastprimarycon == NULL ||
- lastprimarycon->contype != CONSTR_CHECK)
+ (lastprimarycon->contype != CONSTR_CHECK &&
+ lastprimarycon->contype != CONSTR_FOREIGN))
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("misplaced ENFORCED clause"),
@@ -3997,7 +4000,8 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
case CONSTR_ATTR_NOT_ENFORCED:
if (lastprimarycon == NULL ||
- lastprimarycon->contype != CONSTR_CHECK)
+ (lastprimarycon->contype != CONSTR_CHECK &&
+ lastprimarycon->contype != CONSTR_FOREIGN))
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("misplaced NOT ENFORCED clause"),
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index e6721056536..18a14ae186e 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4697,6 +4697,7 @@ RelationGetFKeyList(Relation relation)
info->conoid = constraint->oid;
info->conrelid = constraint->conrelid;
info->confrelid = constraint->confrelid;
+ info->conenforced = constraint->conenforced;
DeconstructFkConstraintRow(htup, &info->nkeys,
info->conkey,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index df331b1c0d9..00fefa9483a 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2495,6 +2495,8 @@ typedef struct ATAlterConstraint
{
NodeTag type;
char *conname; /* Constraint name */
+ bool alterEnforceability; /* changing enforceability properties? */
+ bool is_enforced; /* ENFORCED? */
bool alterDeferrability; /* changing deferrability properties? */
bool deferrable; /* DEFERRABLE? */
bool initdeferred; /* INITIALLY DEFERRED? */
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index d94fddd7cef..b552359915f 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -284,6 +284,9 @@ typedef struct ForeignKeyCacheInfo
/* number of columns in the foreign key */
int nkeys;
+ /* Is enforced ? */
+ bool conenforced;
+
/*
* these arrays each have nkeys valid entries:
*/
diff --git a/src/test/regress/expected/constraints.out b/src/test/regress/expected/constraints.out
index 4f39100fcdf..a719d2f74e9 100644
--- a/src/test/regress/expected/constraints.out
+++ b/src/test/regress/expected/constraints.out
@@ -745,13 +745,9 @@ ERROR: misplaced NOT ENFORCED clause
LINE 1: CREATE TABLE UNIQUE_NOTEN_TBL(i int UNIQUE NOT ENFORCED);
^
ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
-ERROR: FOREIGN KEY constraints cannot be marked ENFORCED
-LINE 1: ...TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
- ^
+ERROR: cannot alter enforceability of constraint "unique_tbl_i_key" of relation "unique_tbl"
ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORCED;
-ERROR: FOREIGN KEY constraints cannot be marked NOT ENFORCED
-LINE 1: ...ABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORC...
- ^
+ERROR: cannot alter enforceability of constraint "unique_tbl_i_key" of relation "unique_tbl"
DROP TABLE unique_tbl;
--
-- EXCLUDE constraints
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 7f678349a8e..960239889ac 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -1,21 +1,49 @@
--
-- FOREIGN KEY
--
--- MATCH FULL
+-- NOT ENFORCED
--
-- First test, check and cascade
--
CREATE TABLE PKTABLE ( ptest1 int PRIMARY KEY, ptest2 text );
-CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE, ftest2 int );
--- Insert test data into PKTABLE
+CREATE TABLE FKTABLE ( ftest1 int CONSTRAINT fktable_ftest1_fkey REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE NOT ENFORCED,
+ ftest2 int );
+-- Inserting into the foreign key table will not result in an error, even if
+-- there is no matching key in the referenced table.
+INSERT INTO FKTABLE VALUES (1, 2);
+INSERT INTO FKTABLE VALUES (2, 3);
+-- Check FKTABLE
+SELECT * FROM FKTABLE;
+ ftest1 | ftest2
+--------+--------
+ 1 | 2
+ 2 | 3
+(2 rows)
+
+-- Reverting it back to ENFORCED will result in failure because constraint validation will be triggered,
+-- as it was previously in a valid state.
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_fkey"
+DETAIL: Key (ftest1)=(1) is not present in table "pktable".
+-- Insert referenced data that satisfies the constraint, then attempted to
+-- change it.
INSERT INTO PKTABLE VALUES (1, 'Test1');
INSERT INTO PKTABLE VALUES (2, 'Test2');
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+-- Any further inserts will fail due to the enforcement.
+INSERT INTO FKTABLE VALUES (3, 4);
+ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_fkey"
+DETAIL: Key (ftest1)=(3) is not present in table "pktable".
+--
+-- MATCH FULL
+--
+-- First test, check and cascade
+--
+-- Insert test data into PKTABLE
INSERT INTO PKTABLE VALUES (3, 'Test3');
INSERT INTO PKTABLE VALUES (4, 'Test4');
INSERT INTO PKTABLE VALUES (5, 'Test5');
-- Insert successful rows into FK TABLE
-INSERT INTO FKTABLE VALUES (1, 2);
-INSERT INTO FKTABLE VALUES (2, 3);
INSERT INTO FKTABLE VALUES (3, 4);
INSERT INTO FKTABLE VALUES (NULL, 1);
-- Insert a failed row into FK TABLE
@@ -351,6 +379,43 @@ INSERT INTO FKTABLE VALUES (1, NULL);
ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL;
ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_ftest2_fkey"
DETAIL: MATCH FULL does not allow mixing of null and nonnull key values.
+-- Modifying other attributes of a constraint should not affect its enforceability, and vice versa
+ALTER TABLE FKTABLE ADD CONSTRAINT fk_con FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE NOT VALID NOT ENFORCED;
+ALTER TABLE FKTABLE ALTER CONSTRAINT fk_con DEFERRABLE INITIALLY DEFERRED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fk_con';
+ condeferrable | condeferred | conenforced | convalidated
+---------------+-------------+-------------+--------------
+ t | t | f | f
+(1 row)
+
+ALTER TABLE FKTABLE ALTER CONSTRAINT fk_con NOT ENFORCED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fk_con';
+ condeferrable | condeferred | conenforced | convalidated
+---------------+-------------+-------------+--------------
+ t | t | f | f
+(1 row)
+
+-- Enforceability also changes the validate state, as data validation will be
+-- performed during this transformation.
+ALTER TABLE FKTABLE ALTER CONSTRAINT fk_con ENFORCED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fk_con';
+ condeferrable | condeferred | conenforced | convalidated
+---------------+-------------+-------------+--------------
+ t | t | t | t
+(1 row)
+
+-- Can change enforceability and deferrability together
+ALTER TABLE FKTABLE ALTER CONSTRAINT fk_con NOT ENFORCED NOT DEFERRABLE;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fk_con';
+ condeferrable | condeferred | conenforced | convalidated
+---------------+-------------+-------------+--------------
+ f | f | f | f
+(1 row)
+
DROP TABLE FKTABLE;
DROP TABLE PKTABLE;
-- MATCH SIMPLE
@@ -1276,6 +1341,13 @@ INSERT INTO fktable VALUES (0, 20);
ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_fk_fkey"
DETAIL: Key (fk)=(20) is not present in table "pktable".
COMMIT;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT ENFORCED;
+BEGIN;
+-- doesn't match FK, but no error.
+UPDATE pktable SET id = 10 WHERE id = 5;
+-- doesn't match PK, but no error.
+INSERT INTO fktable VALUES (0, 20);
+ROLLBACK;
-- try additional syntax
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE;
-- illegal options
@@ -1289,6 +1361,14 @@ ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT VALID;
ERROR: FOREIGN KEY constraints cannot be marked NOT VALID
LINE 1: ...ER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT VALID;
^
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORCED;
+ERROR: conflicting constraint properties
+LINE 1: ...fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORC...
+ ^
+CREATE TEMP TABLE fktable2 (fk int references pktable ENFORCED NOT ENFORCED);
+ERROR: multiple ENFORCED/NOT ENFORCED clauses not allowed
+LINE 1: ...ABLE fktable2 (fk int references pktable ENFORCED NOT ENFORC...
+ ^
-- test order of firing of FK triggers when several RI-induced changes need to
-- be made to the same row. This was broken by subtransaction-related
-- changes in 8.0.
@@ -1586,10 +1666,14 @@ ALTER TABLE fk_partitioned_fk DROP COLUMN fdrop1;
CREATE TABLE fk_partitioned_fk_1 (fdrop1 int, fdrop2 int, a int, fdrop3 int, b int);
ALTER TABLE fk_partitioned_fk_1 DROP COLUMN fdrop1, DROP COLUMN fdrop2, DROP COLUMN fdrop3;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FROM (0,0) TO (1000,1000);
-ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk;
+ALTER TABLE fk_partitioned_fk ADD CONSTRAINT fk_partitioned_fk_a_b_fkey FOREIGN KEY (a, b)
+ REFERENCES fk_notpartitioned_pk NOT ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
+ALTER TABLE fk_partitioned_fk_2 ADD CONSTRAINT fk_partitioned_fk_a_b_fkey FOREIGN KEY (a, b)
+ REFERENCES fk_notpartitioned_pk NOT ENFORCED;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
CREATE TABLE fk_partitioned_fk_3 (fdrop1 int, fdrop2 int, fdrop3 int, fdrop4 int, b int, a int)
PARTITION BY HASH (a);
ALTER TABLE fk_partitioned_fk_3 DROP COLUMN fdrop1, DROP COLUMN fdrop2,
@@ -1665,6 +1749,37 @@ Indexes:
Referenced by:
TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b)
+-- Check the exsting FK trigger
+CREATE TEMP TABLE tmp_trg_info AS SELECT tgtype, tgrelid::regclass, tgconstraint
+FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+SELECT count(1) FROM tmp_trg_info;
+ count
+-------
+ 14
+(1 row)
+
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+-- No triggers
+SELECT count(1) FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+ count
+-------
+ 0
+(1 row)
+
+-- Changing it back to ENFORCED will recreate the necessary triggers.
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+-- Should be exactly the same number of triggers found as before
+SELECT COUNT(1) FROM pg_trigger pgt JOIN tmp_trg_info tt
+ON (pgt.tgtype = tt.tgtype AND pgt.tgrelid = tt.tgrelid AND pgt.tgconstraint = tt.tgconstraint);
+ count
+-------
+ 14
+(1 row)
+
ALTER TABLE fk_partitioned_fk DROP CONSTRAINT fk_partitioned_fk_a_b_fkey;
-- done.
DROP TABLE fk_notpartitioned_pk, fk_partitioned_fk;
@@ -1962,6 +2077,43 @@ Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
Foreign-key constraints:
TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
+DROP TABLE fk_partitioned_fk_2;
+CREATE TABLE fk_partitioned_fk_2 (b int, a int,
+ CONSTRAINT fk_part_con FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
+-- fail -- cannot merge constraints with different enforceability.
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+ERROR: constraint "fk_partitioned_fk_a_b_fkey" enforceability conflicts with constraint "fk_part_con" on relation "fk_partitioned_fk_2"
+-- If the constraint is modified to match the enforceability of the parent, it will work.
+BEGIN;
+-- change child constraint
+ALTER TABLE fk_partitioned_fk_2 ALTER CONSTRAINT fk_part_con ENFORCED;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+\d fk_partitioned_fk_2
+ Table "public.fk_partitioned_fk_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ b | integer | | |
+ a | integer | | |
+Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
+Foreign-key constraints:
+ TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
+
+ROLLBACK;
+BEGIN;
+-- or change parent constraint
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+\d fk_partitioned_fk_2
+ Table "public.fk_partitioned_fk_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ b | integer | | |
+ a | integer | | |
+Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
+Foreign-key constraints:
+ TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED
+
+ROLLBACK;
DROP TABLE fk_partitioned_fk_2;
CREATE TABLE fk_partitioned_fk_4 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE) PARTITION BY RANGE (b, a);
CREATE TABLE fk_partitioned_fk_4_1 PARTITION OF fk_partitioned_fk_4 FOR VALUES FROM (1,1) TO (100,100);
diff --git a/src/test/regress/expected/inherit.out b/src/test/regress/expected/inherit.out
index 4d07d0bd79b..caab1164fdb 100644
--- a/src/test/regress/expected/inherit.out
+++ b/src/test/regress/expected/inherit.out
@@ -1332,6 +1332,13 @@ NOTICE: merging constraint "inh_check_constraint5" with inherited definition
alter table p1 add constraint inh_check_constraint6 check (f1 < 10) not enforced;
alter table p1_c1 add constraint inh_check_constraint6 check (f1 < 10) enforced;
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 invalid state of the child constraint will be ignored here.
+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
create table p1_c2(f1 int constraint inh_check_constraint4 check (f1 < 10)) inherits(p1);
NOTICE: merging column "f1" with inherited definition
NOTICE: merging constraint "inh_check_constraint4" with inherited definition
@@ -1356,39 +1363,47 @@ ERROR: constraint "inh_check_constraint6" conflicts with NOT ENFORCED constrain
select conrelid::regclass::text as relname, conname, conislocal, coninhcount, conenforced, convalidated
from pg_constraint where conname like 'inh\_check\_constraint%'
order by 1, 2;
- relname | conname | conislocal | coninhcount | conenforced | convalidated
----------+-----------------------+------------+-------------+-------------+--------------
- p1 | inh_check_constraint1 | t | 0 | t | t
- p1 | inh_check_constraint2 | t | 0 | t | t
- p1 | inh_check_constraint3 | t | 0 | f | f
- p1 | inh_check_constraint4 | t | 0 | f | f
- p1 | inh_check_constraint5 | t | 0 | f | f
- p1 | inh_check_constraint6 | t | 0 | f | f
- p1 | inh_check_constraint8 | t | 0 | t | t
- p1_c1 | inh_check_constraint1 | t | 1 | t | t
- 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
- p1_c1 | inh_check_constraint5 | t | 1 | t | t
- p1_c1 | inh_check_constraint6 | t | 1 | t | t
- p1_c1 | inh_check_constraint7 | t | 0 | f | f
- p1_c1 | inh_check_constraint8 | f | 1 | t | t
- p1_c2 | inh_check_constraint1 | f | 1 | t | t
- p1_c2 | inh_check_constraint2 | f | 1 | t | t
- p1_c2 | inh_check_constraint3 | f | 1 | f | f
- p1_c2 | inh_check_constraint4 | t | 1 | t | t
- p1_c2 | inh_check_constraint5 | f | 1 | f | f
- p1_c2 | inh_check_constraint6 | f | 1 | f | f
- p1_c2 | inh_check_constraint8 | f | 1 | t | t
- p1_c3 | inh_check_constraint1 | f | 2 | t | t
- p1_c3 | inh_check_constraint2 | f | 2 | t | t
- p1_c3 | inh_check_constraint3 | f | 2 | f | f
- p1_c3 | inh_check_constraint4 | f | 2 | f | f
- p1_c3 | inh_check_constraint5 | f | 2 | t | t
- p1_c3 | inh_check_constraint6 | f | 2 | t | t
- p1_c3 | inh_check_constraint7 | f | 1 | f | f
- p1_c3 | inh_check_constraint8 | f | 2 | t | t
-(30 rows)
+ relname | conname | conislocal | coninhcount | conenforced | convalidated
+---------+------------------------+------------+-------------+-------------+--------------
+ p1 | inh_check_constraint1 | t | 0 | t | t
+ p1 | inh_check_constraint10 | t | 0 | f | f
+ p1 | inh_check_constraint2 | t | 0 | t | t
+ p1 | inh_check_constraint3 | t | 0 | f | f
+ p1 | inh_check_constraint4 | t | 0 | f | f
+ p1 | inh_check_constraint5 | t | 0 | f | f
+ p1 | inh_check_constraint6 | t | 0 | f | f
+ 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_constraint2 | t | 1 | t | t
+ p1_c1 | inh_check_constraint3 | t | 1 | f | f
+ p1_c1 | inh_check_constraint4 | t | 1 | f | f
+ p1_c1 | inh_check_constraint5 | t | 1 | t | t
+ p1_c1 | inh_check_constraint6 | t | 1 | t | t
+ p1_c1 | inh_check_constraint7 | t | 0 | f | f
+ p1_c1 | inh_check_constraint8 | f | 1 | t | t
+ p1_c1 | inh_check_constraint9 | t | 1 | t | f
+ p1_c2 | inh_check_constraint1 | f | 1 | t | t
+ p1_c2 | inh_check_constraint10 | f | 1 | f | f
+ p1_c2 | inh_check_constraint2 | f | 1 | t | t
+ p1_c2 | inh_check_constraint3 | f | 1 | f | f
+ p1_c2 | inh_check_constraint4 | t | 1 | t | t
+ p1_c2 | inh_check_constraint5 | f | 1 | f | f
+ p1_c2 | inh_check_constraint6 | f | 1 | f | f
+ p1_c2 | inh_check_constraint8 | f | 1 | t | t
+ p1_c2 | inh_check_constraint9 | f | 1 | f | f
+ p1_c3 | inh_check_constraint1 | f | 2 | t | t
+ p1_c3 | inh_check_constraint10 | f | 2 | t | t
+ p1_c3 | inh_check_constraint2 | f | 2 | t | t
+ p1_c3 | inh_check_constraint3 | f | 2 | f | f
+ p1_c3 | inh_check_constraint4 | f | 2 | f | f
+ p1_c3 | inh_check_constraint5 | f | 2 | t | t
+ p1_c3 | inh_check_constraint6 | f | 2 | t | t
+ p1_c3 | inh_check_constraint7 | f | 1 | f | f
+ p1_c3 | inh_check_constraint8 | f | 2 | t | t
+ p1_c3 | inh_check_constraint9 | f | 2 | t | t
+(38 rows)
drop table p1 cascade;
NOTICE: drop cascades to 3 other objects
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index 44945b0453a..8db546b30ee 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -2,23 +2,46 @@
-- FOREIGN KEY
--
--- MATCH FULL
+-- NOT ENFORCED
--
-- First test, check and cascade
--
CREATE TABLE PKTABLE ( ptest1 int PRIMARY KEY, ptest2 text );
-CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE, ftest2 int );
+CREATE TABLE FKTABLE ( ftest1 int CONSTRAINT fktable_ftest1_fkey REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE NOT ENFORCED,
+ ftest2 int );
--- Insert test data into PKTABLE
+-- Inserting into the foreign key table will not result in an error, even if
+-- there is no matching key in the referenced table.
+INSERT INTO FKTABLE VALUES (1, 2);
+INSERT INTO FKTABLE VALUES (2, 3);
+
+-- Check FKTABLE
+SELECT * FROM FKTABLE;
+
+-- Reverting it back to ENFORCED will result in failure because constraint validation will be triggered,
+-- as it was previously in a valid state.
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+
+-- Insert referenced data that satisfies the constraint, then attempted to
+-- change it.
INSERT INTO PKTABLE VALUES (1, 'Test1');
INSERT INTO PKTABLE VALUES (2, 'Test2');
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+
+-- Any further inserts will fail due to the enforcement.
+INSERT INTO FKTABLE VALUES (3, 4);
+
+--
+-- MATCH FULL
+--
+-- First test, check and cascade
+--
+-- Insert test data into PKTABLE
INSERT INTO PKTABLE VALUES (3, 'Test3');
INSERT INTO PKTABLE VALUES (4, 'Test4');
INSERT INTO PKTABLE VALUES (5, 'Test5');
-- Insert successful rows into FK TABLE
-INSERT INTO FKTABLE VALUES (1, 2);
-INSERT INTO FKTABLE VALUES (2, 3);
INSERT INTO FKTABLE VALUES (3, 4);
INSERT INTO FKTABLE VALUES (NULL, 1);
@@ -230,6 +253,27 @@ INSERT INTO FKTABLE VALUES (1, NULL);
ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL;
+-- Modifying other attributes of a constraint should not affect its enforceability, and vice versa
+ALTER TABLE FKTABLE ADD CONSTRAINT fk_con FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE NOT VALID NOT ENFORCED;
+ALTER TABLE FKTABLE ALTER CONSTRAINT fk_con DEFERRABLE INITIALLY DEFERRED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fk_con';
+
+ALTER TABLE FKTABLE ALTER CONSTRAINT fk_con NOT ENFORCED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fk_con';
+
+-- Enforceability also changes the validate state, as data validation will be
+-- performed during this transformation.
+ALTER TABLE FKTABLE ALTER CONSTRAINT fk_con ENFORCED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fk_con';
+
+-- Can change enforceability and deferrability together
+ALTER TABLE FKTABLE ALTER CONSTRAINT fk_con NOT ENFORCED NOT DEFERRABLE;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fk_con';
+
DROP TABLE FKTABLE;
DROP TABLE PKTABLE;
@@ -968,12 +1012,25 @@ INSERT INTO fktable VALUES (0, 20);
COMMIT;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT ENFORCED;
+
+BEGIN;
+
+-- doesn't match FK, but no error.
+UPDATE pktable SET id = 10 WHERE id = 5;
+-- doesn't match PK, but no error.
+INSERT INTO fktable VALUES (0, 20);
+
+ROLLBACK;
+
-- try additional syntax
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE;
-- illegal options
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE INITIALLY DEFERRED;
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NO INHERIT;
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT VALID;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORCED;
+CREATE TEMP TABLE fktable2 (fk int references pktable ENFORCED NOT ENFORCED);
-- test order of firing of FK triggers when several RI-induced changes need to
-- be made to the same row. This was broken by subtransaction-related
@@ -1184,11 +1241,14 @@ ALTER TABLE fk_partitioned_fk DROP COLUMN fdrop1;
CREATE TABLE fk_partitioned_fk_1 (fdrop1 int, fdrop2 int, a int, fdrop3 int, b int);
ALTER TABLE fk_partitioned_fk_1 DROP COLUMN fdrop1, DROP COLUMN fdrop2, DROP COLUMN fdrop3;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FROM (0,0) TO (1000,1000);
-ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk;
+ALTER TABLE fk_partitioned_fk ADD CONSTRAINT fk_partitioned_fk_a_b_fkey FOREIGN KEY (a, b)
+ REFERENCES fk_notpartitioned_pk NOT ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
+ALTER TABLE fk_partitioned_fk_2 ADD CONSTRAINT fk_partitioned_fk_a_b_fkey FOREIGN KEY (a, b)
+ REFERENCES fk_notpartitioned_pk NOT ENFORCED;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
-
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
CREATE TABLE fk_partitioned_fk_3 (fdrop1 int, fdrop2 int, fdrop3 int, fdrop4 int, b int, a int)
PARTITION BY HASH (a);
ALTER TABLE fk_partitioned_fk_3 DROP COLUMN fdrop1, DROP COLUMN fdrop2,
@@ -1234,6 +1294,27 @@ UPDATE fk_notpartitioned_pk SET b = 1502 WHERE a = 1500;
UPDATE fk_notpartitioned_pk SET b = 2504 WHERE a = 2500;
-- check psql behavior
\d fk_notpartitioned_pk
+
+-- Check the exsting FK trigger
+CREATE TEMP TABLE tmp_trg_info AS SELECT tgtype, tgrelid::regclass, tgconstraint
+FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+SELECT count(1) FROM tmp_trg_info;
+
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+-- No triggers
+SELECT count(1) FROM pg_trigger
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass);
+
+-- Changing it back to ENFORCED will recreate the necessary triggers.
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+
+-- Should be exactly the same number of triggers found as before
+SELECT COUNT(1) FROM pg_trigger pgt JOIN tmp_trg_info tt
+ON (pgt.tgtype = tt.tgtype AND pgt.tgrelid = tt.tgrelid AND pgt.tgconstraint = tt.tgconstraint);
+
ALTER TABLE fk_partitioned_fk DROP CONSTRAINT fk_partitioned_fk_a_b_fkey;
-- done.
DROP TABLE fk_notpartitioned_pk, fk_partitioned_fk;
@@ -1441,6 +1522,25 @@ ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN
\d fk_partitioned_fk_2
DROP TABLE fk_partitioned_fk_2;
+CREATE TABLE fk_partitioned_fk_2 (b int, a int,
+ CONSTRAINT fk_part_con FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
+-- fail -- cannot merge constraints with different enforceability.
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- If the constraint is modified to match the enforceability of the parent, it will work.
+BEGIN;
+-- change child constraint
+ALTER TABLE fk_partitioned_fk_2 ALTER CONSTRAINT fk_part_con ENFORCED;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+\d fk_partitioned_fk_2
+ROLLBACK;
+BEGIN;
+-- or change parent constraint
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+\d fk_partitioned_fk_2
+ROLLBACK;
+DROP TABLE fk_partitioned_fk_2;
+
CREATE TABLE fk_partitioned_fk_4 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE) PARTITION BY RANGE (b, a);
CREATE TABLE fk_partitioned_fk_4_1 PARTITION OF fk_partitioned_fk_4 FOR VALUES FROM (1,1) TO (100,100);
CREATE TABLE fk_partitioned_fk_4_2 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE SET NULL);
diff --git a/src/test/regress/sql/inherit.sql b/src/test/regress/sql/inherit.sql
index 941189761fd..5f0b2617464 100644
--- a/src/test/regress/sql/inherit.sql
+++ b/src/test/regress/sql/inherit.sql
@@ -481,6 +481,13 @@ alter table p1 add constraint inh_check_constraint5 check (f1 < 10) not enforced
alter table p1 add constraint inh_check_constraint6 check (f1 < 10) not enforced;
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 invalid state of the child constraint will be ignored here.
+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;
+
create table p1_c2(f1 int constraint inh_check_constraint4 check (f1 < 10)) inherits(p1);
-- but reverse is not allowed
--
2.43.5
v20-0006-Merge-the-parent-and-child-constraints-with-diff.patchapplication/octet-stream; name=v20-0006-Merge-the-parent-and-child-constraints-with-diff.patchDownload
From f228d8219530005ff3b50b32bb98d0b15c0cc623 Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 10 Feb 2025 10:59:28 +0530
Subject: [PATCH v20 6/6] Merge the parent and child constraints with differing
enforcibility.
If an ENFORCED parent constraint is attached to a NOT ENFORCED child
constraint, the child constraint will be made ENFORCED, with
validation applied if the parent constraint is validated as well.
Otherwise, a new ENFORCED constraint (with validation, if the parent
constraint is validated) would need to be created on the child table,
which would be unnecessary if a similar constraint already exists and
can be attached.
On the other hand, having a NOT ENFORCED parent constraint with an
ENFORCED child constraint does not cause any issues, and no changes
are required.
----
NOTE: This patch is intended to reduce the diff noise from the main
patch and is not meant to be committed separately. It should be
squashed with the main patch that adds ENFORCED/NOT ENFORCED.
----
---
src/backend/commands/tablecmds.c | 180 +++++++++++++++++++---
src/test/regress/expected/foreign_key.out | 108 +++++++------
src/test/regress/sql/foreign_key.sql | 54 ++++---
3 files changed, 248 insertions(+), 94 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 045eb1a4b28..ba6ce9f79b2 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -11540,23 +11540,6 @@ tryAttachPartitionForeignKey(List **wqueue,
if (!HeapTupleIsValid(partcontup))
elog(ERROR, "cache lookup failed for constraint %u", fk->conoid);
partConstr = (Form_pg_constraint) GETSTRUCT(partcontup);
-
- /*
- * An error should be raised if the constraint enforceability is different.
- * Returning false without raising an error, as we do for other attributes,
- * could lead to a duplicate constraint with the same enforceability as the
- * parent. While this may be acceptable, it may not be ideal. Therefore,
- * it's better to raise an error and allow the user to correct the
- * enforceability before proceeding.
- */
- if (partConstr->conenforced != parentConstr->conenforced)
- ereport(ERROR,
- (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
- errmsg("constraint \"%s\" enforceability conflicts with constraint \"%s\" on relation \"%s\"",
- NameStr(parentConstr->conname),
- NameStr(partConstr->conname),
- RelationGetRelationName(partition))));
-
if (OidIsValid(partConstr->conparentid) ||
partConstr->condeferrable != parentConstr->condeferrable ||
partConstr->condeferred != parentConstr->condeferred ||
@@ -11604,6 +11587,8 @@ AttachPartitionForeignKey(List **wqueue,
Oid partConstrFrelid;
Oid partConstrRelid;
bool parentConstrIsEnforced;
+ bool partConstrIsEnforced;
+ bool partConstrParentIsSet;
/* Fetch the parent constraint tuple */
parentConstrTup = SearchSysCache1(CONSTROID,
@@ -11619,13 +11604,47 @@ AttachPartitionForeignKey(List **wqueue,
if (!HeapTupleIsValid(partcontup))
elog(ERROR, "cache lookup failed for constraint %u", partConstrOid);
partConstr = (Form_pg_constraint) GETSTRUCT(partcontup);
+ partConstrIsEnforced = partConstr->conenforced;
partConstrFrelid = partConstr->confrelid;
partConstrRelid = partConstr->conrelid;
+ /*
+ * The case where the parent constraint is NOT ENFORCED and the child
+ * constraint is ENFORCED is acceptable because the not enforced parent
+ * constraint lacks triggers, eliminating any redundancy issues with the
+ * enforced child constraint. In this scenario, the child constraint
+ * remains enforced, and its trigger is retained, ensuring that
+ * referential integrity checks for the child continue as before, even
+ * with the parent constraint not enforced. The relationship between the
+ * two constraints is preserved by setting the parent constraint, which
+ * allows us to locate the child constraint. This becomes important if the
+ * parent constraint is later changed to enforced, at which point the
+ * necessary trigger will be created for the parent, and any redundancy
+ * from these triggers will be appropriately handled.
+ */
+ if (!parentConstrIsEnforced && partConstrIsEnforced)
+ {
+ ReleaseSysCache(partcontup);
+ ReleaseSysCache(parentConstrTup);
+
+ ConstraintSetParentConstraint(partConstrOid, parentConstrOid,
+ RelationGetRelid(partition));
+ CommandCounterIncrement();
+
+ return;
+ }
+
/*
* If the referenced table is partitioned, then the partition we're
* attaching now has extra pg_constraint rows and action triggers that are
* no longer needed. Remove those.
+ *
+ * Note that this must be done beforehand, particularly in situations
+ * where we might decide to change the constraint to an ENFORCED state
+ * which will create the required triggers and add the child constraint to
+ * the validation queue. To avoid generating unnecessary triggers and
+ * adding them to the validation queue, it is crucial to eliminate any
+ * redundant constraints beforehand.
*/
if (get_rel_relkind(partConstrFrelid) == RELKIND_PARTITIONED_TABLE)
{
@@ -11644,6 +11663,53 @@ AttachPartitionForeignKey(List **wqueue,
*/
queueValidation = parentConstr->convalidated && !partConstr->convalidated;
+ /*
+ * The case where the parent constraint is ENFORCED and the child
+ * constraint is NOT ENFORCED is not acceptable, as it would violate
+ * referential integrity. In such cases, the child constraint will first
+ * be enforced before merging it with the enforced parent constraint.
+ * Subsequently, removing action triggers, setting up constraint triggers,
+ * and handling check triggers for the parent will be managed in the usual
+ * manner, similar to how two enforced constraints are merged.
+ */
+ if (parentConstrIsEnforced && !partConstrIsEnforced)
+ {
+ ATAlterConstraint *cmdcon = makeNode(ATAlterConstraint);
+ Relation conrel;
+
+ cmdcon->conname = NameStr(partConstr->conname);
+ cmdcon->deferrable = partConstr->condeferrable;
+ cmdcon->initdeferred = partConstr->condeferred;
+ cmdcon->alterEnforceability = true;
+ cmdcon->is_enforced = true;
+
+ conrel = table_open(ConstraintRelationId, RowExclusiveLock);
+
+ ATExecAlterConstrEnforceability(wqueue, cmdcon, conrel, trigrel,
+ partConstr->conrelid,
+ partConstr->confrelid,
+ partcontup, AccessExclusiveLock,
+ InvalidOid, InvalidOid, InvalidOid,
+ InvalidOid);
+
+ table_close(conrel, RowExclusiveLock);
+
+ CommandCounterIncrement();
+
+ /*
+ * No further validation is needed, as changing the constraint to
+ * enforced will implicitly trigger the same validation.
+ */
+ queueValidation = false;
+ }
+
+ /*
+ * The constraint parent shouldn't be set beforehand, or if it's already
+ * set, it should be the specified parent.
+ */
+ partConstrParentIsSet = OidIsValid(partConstr->conparentid);
+ Assert(!partConstrParentIsSet || partConstr->conparentid == parentConstrOid);
+
ReleaseSysCache(partcontup);
ReleaseSysCache(parentConstrTup);
@@ -11656,8 +11722,10 @@ AttachPartitionForeignKey(List **wqueue,
DropForeignKeyConstraintTriggers(trigrel, partConstrOid, partConstrFrelid,
partConstrRelid);
- ConstraintSetParentConstraint(partConstrOid, parentConstrOid,
- RelationGetRelid(partition));
+ /* Skip if the parent is already set */
+ if (!partConstrParentIsSet)
+ ConstraintSetParentConstraint(partConstrOid, parentConstrOid,
+ RelationGetRelid(partition));
/*
* Like the constraint, attach partition's "check" triggers to the
@@ -12257,6 +12325,17 @@ ATExecAlterConstrEnforceability(List **wqueue, ATAlterConstraint *cmdcon,
/* Drop all the triggers */
DropForeignKeyConstraintTriggers(tgrel, conoid, InvalidOid, InvalidOid);
+
+ /*
+ * If the referenced table is partitioned, the child constraint we're
+ * changing to NOT ENFORCED may have additional pg_constraint rows and
+ * action triggers that remain untouched while this child constraint
+ * is attached to the NOT ENFORCED parent. These must now be removed.
+ * For more details, see AttachPartitionForeignKey().
+ */
+ if (OidIsValid(currcon->conparentid) &&
+ get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE)
+ RemoveInheritedConstraint(conrel, tgrel, currcon->conrelid, conoid);
}
else if (changed) /* Create triggers */
{
@@ -12582,13 +12661,40 @@ AlterConstrEnforceabilityRecurse(List **wqueue, ATAlterConstraint *cmdcon,
true, NULL, 1, &pkey);
while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
- ATExecAlterConstrEnforceability(wqueue, cmdcon, conrel, tgrel, fkrelid,
- pkrelid, childtup, lockmode,
- ReferencedParentDelTrigger,
- ReferencedParentUpdTrigger,
- ReferencingParentInsTrigger,
- ReferencingParentUpdTrigger);
+ {
+ Form_pg_constraint childcon;
+ childcon = (Form_pg_constraint) GETSTRUCT(childtup);
+
+ /*
+ * When the parent constraint is modified to be ENFORCED, and the
+ * child constraint is attached to the parent constraint (which is
+ * already ENFORCED), some constraints and action triggers on the
+ * child table may become redundant and need to be removed.
+ */
+ if (cmdcon->is_enforced && childcon->conenforced)
+ {
+ if (currcon->confrelid == pkrelid)
+ {
+ Relation rel = table_open(childcon->conrelid, lockmode);
+
+ AttachPartitionForeignKey(wqueue, rel, childcon->oid,
+ conoid,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger,
+ tgrel);
+
+ table_close(rel, NoLock);
+ }
+ }
+ else
+ ATExecAlterConstrEnforceability(wqueue, cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, childtup, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
+ }
systable_endscan(pscan);
}
@@ -20762,7 +20868,9 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
{
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
+ HeapTuple parentContup;
Form_pg_constraint conform;
+ Oid parentConstrIsEnforced;
contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
if (!HeapTupleIsValid(contup))
@@ -20781,12 +20889,34 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
continue;
}
+ /* Get the enforcibility of the parent constraint */
+ parentContup = SearchSysCache1(CONSTROID,
+ ObjectIdGetDatum(conform->conparentid));
+ if (!HeapTupleIsValid(parentContup))
+ elog(ERROR, "cache lookup failed for constraint %u",
+ conform->conparentid);
+ parentConstrIsEnforced =
+ ((Form_pg_constraint) GETSTRUCT(parentContup))->conenforced;
+ ReleaseSysCache(parentContup);
+
/*
* The constraint on this table must be marked no longer a child of
* the parent's constraint, as do its check triggers.
*/
ConstraintSetParentConstraint(fk->conoid, InvalidOid, InvalidOid);
+ /*
+ * Unsetting the parent is sufficient when the parent constraint is
+ * NOT ENFORCED and the child constraint is ENFORCED, as we link them
+ * by setting the constraint parent, while leaving the rest unchanged.
+ * For more details, see AttachPartitionForeignKey().
+ */
+ if (!parentConstrIsEnforced && fk->conenforced)
+ {
+ ReleaseSysCache(contup);
+ continue;
+ }
+
/*
* Also, look up the partition's "check" triggers corresponding to the
* ENFORCED constraint being detached and detach them from the parent
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 960239889ac..f36ad5bf6de 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -1670,8 +1670,6 @@ ALTER TABLE fk_partitioned_fk ADD CONSTRAINT fk_partitioned_fk_a_b_fkey FOREIGN
REFERENCES fk_notpartitioned_pk NOT ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
-ALTER TABLE fk_partitioned_fk_2 ADD CONSTRAINT fk_partitioned_fk_a_b_fkey FOREIGN KEY (a, b)
- REFERENCES fk_notpartitioned_pk NOT ENFORCED;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
CREATE TABLE fk_partitioned_fk_3 (fdrop1 int, fdrop2 int, fdrop3 int, fdrop4 int, b int, a int)
@@ -1680,8 +1678,65 @@ ALTER TABLE fk_partitioned_fk_3 DROP COLUMN fdrop1, DROP COLUMN fdrop2,
DROP COLUMN fdrop3, DROP COLUMN fdrop4;
CREATE TABLE fk_partitioned_fk_3_0 PARTITION OF fk_partitioned_fk_3 FOR VALUES WITH (MODULUS 5, REMAINDER 0);
CREATE TABLE fk_partitioned_fk_3_1 PARTITION OF fk_partitioned_fk_3 FOR VALUES WITH (MODULUS 5, REMAINDER 1);
+-- Merge the not-enforced parent constraint with the enforced and not-enforced child constraints
+ALTER TABLE fk_partitioned_fk_3_0 ADD CONSTRAINT fk_partitioned_fk_3_0_a_b_fkey FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ENFORCED;
+ALTER TABLE fk_partitioned_fk_3_1 ADD CONSTRAINT fk_partitioned_fk_3_1_a_b_fkey FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk_3 ADD CONSTRAINT fk_partitioned_fk_3_a_b_fkey FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+-- Merge the non-enforced parent constraint with both the enforced and
+-- non-enforced child constraints, where the referenced table is partitioned.
+CREATE TABLE fk_partitioned_pk (a int, b int, PRIMARY KEY (a, b)) PARTITION BY RANGE (a, b);
+CREATE TABLE fk_partitioned_pk_1 PARTITION OF fk_partitioned_pk FOR VALUES FROM (0,0) TO (1000,1000);
+-- Merge the enforced parent constraint with the enforced and not-enforced child constraints.
+ALTER TABLE fk_partitioned_fk_3_0 ADD CONSTRAINT fk_partitioned_fk_3_0_a_b_fkey1 FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk ENFORCED;
+ALTER TABLE fk_partitioned_fk_3_1 ADD CONSTRAINT fk_partitioned_fk_3_1_a_b_fkey1 FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk_3 ADD CONSTRAINT fk_partitioned_fk_3_a_b_fkey1 FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk ENFORCED;
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+ conname | conenforced | convalidated | conrelid | confrelid
+---------------------------------+-------------+--------------+-----------------------+----------------------
+ fk_partitioned_fk_3_0_a_b_fkey | t | t | fk_partitioned_fk_3_0 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey | f | f | fk_partitioned_fk_3_1 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_a_b_fkey | f | f | fk_partitioned_fk_3 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_0_a_b_fkey1 | t | t | fk_partitioned_fk_3_0 | fk_partitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey1 | t | t | fk_partitioned_fk_3_1 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey1 | t | t | fk_partitioned_fk_3 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey2 | t | t | fk_partitioned_fk_3 | fk_partitioned_pk_1
+(7 rows)
+
+ALTER TABLE fk_partitioned_fk_3 DETACH PARTITION fk_partitioned_fk_3_0;
+ALTER TABLE fk_partitioned_fk_3 ATTACH PARTITION fk_partitioned_fk_3_0 FOR VALUES WITH (MODULUS 5, REMAINDER 0);
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+ conname | conenforced | convalidated | conrelid | confrelid
+---------------------------------+-------------+--------------+-----------------------+----------------------
+ fk_partitioned_fk_3_0_a_b_fkey | t | t | fk_partitioned_fk_3_0 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey | f | f | fk_partitioned_fk_3_1 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_a_b_fkey | f | f | fk_partitioned_fk_3 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_0_a_b_fkey1 | t | t | fk_partitioned_fk_3_0 | fk_partitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey1 | t | t | fk_partitioned_fk_3_1 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey1 | t | t | fk_partitioned_fk_3 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey2 | t | t | fk_partitioned_fk_3 | fk_partitioned_pk_1
+(7 rows)
+
+ALTER TABLE fk_partitioned_fk_3 ALTER CONSTRAINT fk_partitioned_fk_3_a_b_fkey1 NOT ENFORCED;
+-- Merging an enforced parent constraint (validated) with a not-enforced child
+-- constraint will implicitly change the child constraint to enforced and apply
+-- the validation as well.
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_3
FOR VALUES FROM (2000,2000) TO (3000,3000);
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+ conname | conenforced | convalidated | conrelid | confrelid
+---------------------------------+-------------+--------------+-----------------------+----------------------
+ fk_partitioned_fk_3_0_a_b_fkey | t | t | fk_partitioned_fk_3_0 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey | t | t | fk_partitioned_fk_3_1 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_a_b_fkey | t | t | fk_partitioned_fk_3 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_0_a_b_fkey1 | f | f | fk_partitioned_fk_3_0 | fk_partitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey1 | f | f | fk_partitioned_fk_3_1 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey1 | f | f | fk_partitioned_fk_3 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey2 | f | f | fk_partitioned_fk_3 | fk_partitioned_pk_1
+(7 rows)
+
-- Creating a foreign key with ONLY on a partitioned table referencing
-- a non-partitioned table fails.
ALTER TABLE ONLY fk_partitioned_fk ADD FOREIGN KEY (a, b)
@@ -1702,16 +1757,16 @@ INSERT INTO fk_partitioned_fk_2 (a,b) VALUES (1500, 1501);
ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
DETAIL: Key (a, b)=(1500, 1501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (2500, 2502);
-ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_3_1_a_b_fkey"
DETAIL: Key (a, b)=(2500, 2502) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk_3 (a,b) VALUES (2500, 2502);
-ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_3_1_a_b_fkey"
DETAIL: Key (a, b)=(2500, 2502) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (2501, 2503);
-ERROR: insert or update on table "fk_partitioned_fk_3_0" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_0" violates foreign key constraint "fk_partitioned_fk_3_0_a_b_fkey"
DETAIL: Key (a, b)=(2501, 2503) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk_3 (a,b) VALUES (2501, 2503);
-ERROR: insert or update on table "fk_partitioned_fk_3_0" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_0" violates foreign key constraint "fk_partitioned_fk_3_0_a_b_fkey"
DETAIL: Key (a, b)=(2501, 2503) is not present in table "fk_notpartitioned_pk".
-- but if we insert the values that make them valid, then they work
INSERT INTO fk_notpartitioned_pk VALUES (500, 501), (1500, 1501),
@@ -1722,7 +1777,7 @@ INSERT INTO fk_partitioned_fk (a,b) VALUES (2500, 2502);
INSERT INTO fk_partitioned_fk (a,b) VALUES (2501, 2503);
-- this update fails because there is no referenced row
UPDATE fk_partitioned_fk SET a = a + 1 WHERE a = 2501;
-ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_3_1_a_b_fkey"
DETAIL: Key (a, b)=(2502, 2503) is not present in table "fk_notpartitioned_pk".
-- but we can fix it thusly:
INSERT INTO fk_notpartitioned_pk (a,b) VALUES (2502, 2503);
@@ -1865,8 +1920,6 @@ WHERE conrelid::regclass::text like 'fk_partitioned_fk%' ORDER BY oid::regclass:
DROP TABLE fk_partitioned_fk, fk_notpartitioned_pk;
-- NOT VALID foreign key on a non-partitioned table referencing a partitioned table
-CREATE TABLE fk_partitioned_pk (a int, b int, PRIMARY KEY (a, b)) PARTITION BY RANGE (a, b);
-CREATE TABLE fk_partitioned_pk_1 PARTITION OF fk_partitioned_pk FOR VALUES FROM (0,0) TO (1000,1000);
CREATE TABLE fk_notpartitioned_fk (b int, a int);
ALTER TABLE fk_notpartitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk NOT VALID;
-- Constraint will be invalid.
@@ -2077,43 +2130,6 @@ Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
Foreign-key constraints:
TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
-DROP TABLE fk_partitioned_fk_2;
-CREATE TABLE fk_partitioned_fk_2 (b int, a int,
- CONSTRAINT fk_part_con FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
--- fail -- cannot merge constraints with different enforceability.
-ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
-ERROR: constraint "fk_partitioned_fk_a_b_fkey" enforceability conflicts with constraint "fk_part_con" on relation "fk_partitioned_fk_2"
--- If the constraint is modified to match the enforceability of the parent, it will work.
-BEGIN;
--- change child constraint
-ALTER TABLE fk_partitioned_fk_2 ALTER CONSTRAINT fk_part_con ENFORCED;
-ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
-\d fk_partitioned_fk_2
- Table "public.fk_partitioned_fk_2"
- Column | Type | Collation | Nullable | Default
---------+---------+-----------+----------+---------
- b | integer | | |
- a | integer | | |
-Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
-Foreign-key constraints:
- TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
-
-ROLLBACK;
-BEGIN;
--- or change parent constraint
-ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
-ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
-\d fk_partitioned_fk_2
- Table "public.fk_partitioned_fk_2"
- Column | Type | Collation | Nullable | Default
---------+---------+-----------+----------+---------
- b | integer | | |
- a | integer | | |
-Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
-Foreign-key constraints:
- TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED
-
-ROLLBACK;
DROP TABLE fk_partitioned_fk_2;
CREATE TABLE fk_partitioned_fk_4 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE) PARTITION BY RANGE (b, a);
CREATE TABLE fk_partitioned_fk_4_1 PARTITION OF fk_partitioned_fk_4 FOR VALUES FROM (1,1) TO (100,100);
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index 8db546b30ee..06e2726f5fb 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -1245,8 +1245,6 @@ ALTER TABLE fk_partitioned_fk ADD CONSTRAINT fk_partitioned_fk_a_b_fkey FOREIGN
REFERENCES fk_notpartitioned_pk NOT ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
-ALTER TABLE fk_partitioned_fk_2 ADD CONSTRAINT fk_partitioned_fk_a_b_fkey FOREIGN KEY (a, b)
- REFERENCES fk_notpartitioned_pk NOT ENFORCED;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
CREATE TABLE fk_partitioned_fk_3 (fdrop1 int, fdrop2 int, fdrop3 int, fdrop4 int, b int, a int)
@@ -1255,9 +1253,40 @@ ALTER TABLE fk_partitioned_fk_3 DROP COLUMN fdrop1, DROP COLUMN fdrop2,
DROP COLUMN fdrop3, DROP COLUMN fdrop4;
CREATE TABLE fk_partitioned_fk_3_0 PARTITION OF fk_partitioned_fk_3 FOR VALUES WITH (MODULUS 5, REMAINDER 0);
CREATE TABLE fk_partitioned_fk_3_1 PARTITION OF fk_partitioned_fk_3 FOR VALUES WITH (MODULUS 5, REMAINDER 1);
+-- Merge the not-enforced parent constraint with the enforced and not-enforced child constraints
+ALTER TABLE fk_partitioned_fk_3_0 ADD CONSTRAINT fk_partitioned_fk_3_0_a_b_fkey FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ENFORCED;
+ALTER TABLE fk_partitioned_fk_3_1 ADD CONSTRAINT fk_partitioned_fk_3_1_a_b_fkey FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk_3 ADD CONSTRAINT fk_partitioned_fk_3_a_b_fkey FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+
+-- Merge the non-enforced parent constraint with both the enforced and
+-- non-enforced child constraints, where the referenced table is partitioned.
+CREATE TABLE fk_partitioned_pk (a int, b int, PRIMARY KEY (a, b)) PARTITION BY RANGE (a, b);
+CREATE TABLE fk_partitioned_pk_1 PARTITION OF fk_partitioned_pk FOR VALUES FROM (0,0) TO (1000,1000);
+-- Merge the enforced parent constraint with the enforced and not-enforced child constraints.
+ALTER TABLE fk_partitioned_fk_3_0 ADD CONSTRAINT fk_partitioned_fk_3_0_a_b_fkey1 FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk ENFORCED;
+ALTER TABLE fk_partitioned_fk_3_1 ADD CONSTRAINT fk_partitioned_fk_3_1_a_b_fkey1 FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk_3 ADD CONSTRAINT fk_partitioned_fk_3_a_b_fkey1 FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk ENFORCED;
+
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+
+ALTER TABLE fk_partitioned_fk_3 DETACH PARTITION fk_partitioned_fk_3_0;
+ALTER TABLE fk_partitioned_fk_3 ATTACH PARTITION fk_partitioned_fk_3_0 FOR VALUES WITH (MODULUS 5, REMAINDER 0);
+
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+
+ALTER TABLE fk_partitioned_fk_3 ALTER CONSTRAINT fk_partitioned_fk_3_a_b_fkey1 NOT ENFORCED;
+
+-- Merging an enforced parent constraint (validated) with a not-enforced child
+-- constraint will implicitly change the child constraint to enforced and apply
+-- the validation as well.
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_3
FOR VALUES FROM (2000,2000) TO (3000,3000);
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+
-- Creating a foreign key with ONLY on a partitioned table referencing
-- a non-partitioned table fails.
ALTER TABLE ONLY fk_partitioned_fk ADD FOREIGN KEY (a, b)
@@ -1384,8 +1413,6 @@ WHERE conrelid::regclass::text like 'fk_partitioned_fk%' ORDER BY oid::regclass:
DROP TABLE fk_partitioned_fk, fk_notpartitioned_pk;
-- NOT VALID foreign key on a non-partitioned table referencing a partitioned table
-CREATE TABLE fk_partitioned_pk (a int, b int, PRIMARY KEY (a, b)) PARTITION BY RANGE (a, b);
-CREATE TABLE fk_partitioned_pk_1 PARTITION OF fk_partitioned_pk FOR VALUES FROM (0,0) TO (1000,1000);
CREATE TABLE fk_notpartitioned_fk (b int, a int);
ALTER TABLE fk_notpartitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk NOT VALID;
@@ -1522,25 +1549,6 @@ ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN
\d fk_partitioned_fk_2
DROP TABLE fk_partitioned_fk_2;
-CREATE TABLE fk_partitioned_fk_2 (b int, a int,
- CONSTRAINT fk_part_con FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
--- fail -- cannot merge constraints with different enforceability.
-ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
--- If the constraint is modified to match the enforceability of the parent, it will work.
-BEGIN;
--- change child constraint
-ALTER TABLE fk_partitioned_fk_2 ALTER CONSTRAINT fk_part_con ENFORCED;
-ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
-\d fk_partitioned_fk_2
-ROLLBACK;
-BEGIN;
--- or change parent constraint
-ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
-ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
-\d fk_partitioned_fk_2
-ROLLBACK;
-DROP TABLE fk_partitioned_fk_2;
-
CREATE TABLE fk_partitioned_fk_4 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE) PARTITION BY RANGE (b, a);
CREATE TABLE fk_partitioned_fk_4_1 PARTITION OF fk_partitioned_fk_4 FOR VALUES FROM (1,1) TO (100,100);
CREATE TABLE fk_partitioned_fk_4_2 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE SET NULL);
--
2.43.5
On Thu, Mar 27, 2025 at 6:25 PM Amul Sul <sulamul@gmail.com> wrote:
I am not sure how to make such tests stable enough since the trigger
name involves OIDs. In count check, I tried adjusting the join
condition to ensure that I get the exact same type of constraint
w.r.t. trigger relation and the constraint.
There are tests which mask variable parts of EXPLAIN output. Can we
use similar trick to mask OIDs from the trigger names?
--
Best Wishes,
Ashutosh Bapat
On Fri, Mar 28, 2025 at 3:34 PM Ashutosh Bapat
<ashutosh.bapat.oss@gmail.com> wrote:
On Thu, Mar 27, 2025 at 6:25 PM Amul Sul <sulamul@gmail.com> wrote:
I am not sure how to make such tests stable enough since the trigger
name involves OIDs. In count check, I tried adjusting the join
condition to ensure that I get the exact same type of constraint
w.r.t. trigger relation and the constraint.There are tests which mask variable parts of EXPLAIN output. Can we
use similar trick to mask OIDs from the trigger names?
Okay, tried it in the attached version. Please check if it looks good.
Regards,
Amul
Attachments:
v21-0004-Remove-hastriggers-flag-check-before-fetching-FK.patchapplication/octet-stream; name=v21-0004-Remove-hastriggers-flag-check-before-fetching-FK.patchDownload
From 6b745c43a63540b7b27c538076db18216103368f Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Thu, 27 Mar 2025 16:15:05 +0530
Subject: [PATCH v21 4/6] Remove hastriggers flag check before fetching FK
constraints.
With NOT ENFORCED, foreign key constraints will be created without
triggers. Therefore, the criteria for fetching foreign keys based on
the presence of triggers no longer apply.
----
NOTE: This patch is intended to reduce the diff noise from the main
patch and is not meant to be committed separately. It should be
squashed with the main patch that adds ENFORCED/NOT ENFORCED.
----
---
src/backend/utils/cache/relcache.c | 5 -
src/bin/pg_dump/pg_dump.c | 8 +-
src/bin/psql/describe.c | 226 ++++++++++++++---------------
3 files changed, 108 insertions(+), 131 deletions(-)
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 9f54a9e72b7..e6721056536 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4666,11 +4666,6 @@ RelationGetFKeyList(Relation relation)
if (relation->rd_fkeyvalid)
return relation->rd_fkeylist;
- /* Fast path: non-partitioned tables without triggers can't have FKs */
- if (!relation->rd_rel->relhastriggers &&
- relation->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
- return NIL;
-
/*
* We build the list we intend to return (in the caller's context) while
* doing the scan. After successfully completing the scan, we copy that
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index e41e645f649..dc9797aa251 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -8016,13 +8016,7 @@ getConstraints(Archive *fout, TableInfo tblinfo[], int numTables)
{
TableInfo *tinfo = &tblinfo[i];
- /*
- * For partitioned tables, foreign keys have no triggers so they must
- * be included anyway in case some foreign keys are defined.
- */
- if ((!tinfo->hastriggers &&
- tinfo->relkind != RELKIND_PARTITIONED_TABLE) ||
- !(tinfo->dobj.dump & DUMP_COMPONENT_DEFINITION))
+ if (!(tinfo->dobj.dump & DUMP_COMPONENT_DEFINITION))
continue;
/* OK, we need info for this table */
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index bf565afcc4e..e4a4c5071dd 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2550,136 +2550,124 @@ describeOneTableDetails(const char *schemaname,
PQclear(result);
}
- /*
- * Print foreign-key constraints (there are none if no triggers,
- * except if the table is partitioned, in which case the triggers
- * appear in the partitions)
- */
- if (tableinfo.hastriggers ||
- tableinfo.relkind == RELKIND_PARTITIONED_TABLE)
+ /* Print foreign-key constraints */
+ if (pset.sversion >= 120000 &&
+ (tableinfo.ispartition || tableinfo.relkind == RELKIND_PARTITIONED_TABLE))
{
- if (pset.sversion >= 120000 &&
- (tableinfo.ispartition || tableinfo.relkind == RELKIND_PARTITIONED_TABLE))
+ /*
+ * Put the constraints defined in this table first, followed
+ * by the constraints defined in ancestor partitioned tables.
+ */
+ printfPQExpBuffer(&buf,
+ "SELECT conrelid = '%s'::pg_catalog.regclass AS sametable,\n"
+ " conname,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef,\n"
+ " conrelid::pg_catalog.regclass AS ontable\n"
+ " FROM pg_catalog.pg_constraint,\n"
+ " pg_catalog.pg_partition_ancestors('%s')\n"
+ " WHERE conrelid = relid AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
+ "ORDER BY sametable DESC, conname;",
+ oid, oid);
+ }
+ else
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT true as sametable, conname,\n"
+ " pg_catalog.pg_get_constraintdef(r.oid, true) as condef,\n"
+ " conrelid::pg_catalog.regclass AS ontable\n"
+ "FROM pg_catalog.pg_constraint r\n"
+ "WHERE r.conrelid = '%s' AND r.contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n",
+ oid);
+
+ if (pset.sversion >= 120000)
+ appendPQExpBufferStr(&buf, " AND conparentid = 0\n");
+ appendPQExpBufferStr(&buf, "ORDER BY conname");
+ }
+
+ result = PSQLexec(buf.data);
+ if (!result)
+ goto error_return;
+ else
+ tuples = PQntuples(result);
+
+ if (tuples > 0)
+ {
+ int i_sametable = PQfnumber(result, "sametable"),
+ i_conname = PQfnumber(result, "conname"),
+ i_condef = PQfnumber(result, "condef"),
+ i_ontable = PQfnumber(result, "ontable");
+
+ printTableAddFooter(&cont, _("Foreign-key constraints:"));
+ for (i = 0; i < tuples; i++)
{
/*
- * Put the constraints defined in this table first, followed
- * by the constraints defined in ancestor partitioned tables.
+ * Print untranslated constraint name and definition. Use
+ * a "TABLE tab" prefix when the constraint is defined in
+ * a parent partitioned table.
*/
- printfPQExpBuffer(&buf,
- "SELECT conrelid = '%s'::pg_catalog.regclass AS sametable,\n"
- " conname,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef,\n"
- " conrelid::pg_catalog.regclass AS ontable\n"
- " FROM pg_catalog.pg_constraint,\n"
- " pg_catalog.pg_partition_ancestors('%s')\n"
- " WHERE conrelid = relid AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
- "ORDER BY sametable DESC, conname;",
- oid, oid);
- }
- else
- {
- printfPQExpBuffer(&buf,
- "SELECT true as sametable, conname,\n"
- " pg_catalog.pg_get_constraintdef(r.oid, true) as condef,\n"
- " conrelid::pg_catalog.regclass AS ontable\n"
- "FROM pg_catalog.pg_constraint r\n"
- "WHERE r.conrelid = '%s' AND r.contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n",
- oid);
-
- if (pset.sversion >= 120000)
- appendPQExpBufferStr(&buf, " AND conparentid = 0\n");
- appendPQExpBufferStr(&buf, "ORDER BY conname");
- }
-
- result = PSQLexec(buf.data);
- if (!result)
- goto error_return;
- else
- tuples = PQntuples(result);
-
- if (tuples > 0)
- {
- int i_sametable = PQfnumber(result, "sametable"),
- i_conname = PQfnumber(result, "conname"),
- i_condef = PQfnumber(result, "condef"),
- i_ontable = PQfnumber(result, "ontable");
-
- printTableAddFooter(&cont, _("Foreign-key constraints:"));
- for (i = 0; i < tuples; i++)
- {
- /*
- * Print untranslated constraint name and definition. Use
- * a "TABLE tab" prefix when the constraint is defined in
- * a parent partitioned table.
- */
- if (strcmp(PQgetvalue(result, i, i_sametable), "f") == 0)
- printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
- PQgetvalue(result, i, i_ontable),
- PQgetvalue(result, i, i_conname),
- PQgetvalue(result, i, i_condef));
- else
- printfPQExpBuffer(&buf, " \"%s\" %s",
- PQgetvalue(result, i, i_conname),
- PQgetvalue(result, i, i_condef));
-
- printTableAddFooter(&cont, buf.data);
- }
- }
- PQclear(result);
- }
-
- /* print incoming foreign-key references */
- if (tableinfo.hastriggers ||
- tableinfo.relkind == RELKIND_PARTITIONED_TABLE)
- {
- if (pset.sversion >= 120000)
- {
- printfPQExpBuffer(&buf,
- "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
- " FROM pg_catalog.pg_constraint c\n"
- " WHERE confrelid IN (SELECT pg_catalog.pg_partition_ancestors('%s')\n"
- " UNION ALL VALUES ('%s'::pg_catalog.regclass))\n"
- " AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
- "ORDER BY conname;",
- oid, oid);
- }
- else
- {
- printfPQExpBuffer(&buf,
- "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
- " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
- " FROM pg_catalog.pg_constraint\n"
- " WHERE confrelid = %s AND contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n"
- "ORDER BY conname;",
- oid);
- }
-
- result = PSQLexec(buf.data);
- if (!result)
- goto error_return;
- else
- tuples = PQntuples(result);
-
- if (tuples > 0)
- {
- int i_conname = PQfnumber(result, "conname"),
- i_ontable = PQfnumber(result, "ontable"),
- i_condef = PQfnumber(result, "condef");
-
- printTableAddFooter(&cont, _("Referenced by:"));
- for (i = 0; i < tuples; i++)
- {
+ if (strcmp(PQgetvalue(result, i, i_sametable), "f") == 0)
printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
PQgetvalue(result, i, i_ontable),
PQgetvalue(result, i, i_conname),
PQgetvalue(result, i, i_condef));
+ else
+ printfPQExpBuffer(&buf, " \"%s\" %s",
+ PQgetvalue(result, i, i_conname),
+ PQgetvalue(result, i, i_condef));
- printTableAddFooter(&cont, buf.data);
- }
+ printTableAddFooter(&cont, buf.data);
}
- PQclear(result);
}
+ PQclear(result);
+
+ /* print incoming foreign-key references */
+ if (pset.sversion >= 120000)
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
+ " FROM pg_catalog.pg_constraint c\n"
+ " WHERE confrelid IN (SELECT pg_catalog.pg_partition_ancestors('%s')\n"
+ " UNION ALL VALUES ('%s'::pg_catalog.regclass))\n"
+ " AND contype = " CppAsString2(CONSTRAINT_FOREIGN) " AND conparentid = 0\n"
+ "ORDER BY conname;",
+ oid, oid);
+ }
+ else
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n"
+ " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n"
+ " FROM pg_catalog.pg_constraint\n"
+ " WHERE confrelid = %s AND contype = " CppAsString2(CONSTRAINT_FOREIGN) "\n"
+ "ORDER BY conname;",
+ oid);
+ }
+
+ result = PSQLexec(buf.data);
+ if (!result)
+ goto error_return;
+ else
+ tuples = PQntuples(result);
+
+ if (tuples > 0)
+ {
+ int i_conname = PQfnumber(result, "conname"),
+ i_ontable = PQfnumber(result, "ontable"),
+ i_condef = PQfnumber(result, "condef");
+
+ printTableAddFooter(&cont, _("Referenced by:"));
+ for (i = 0; i < tuples; i++)
+ {
+ printfPQExpBuffer(&buf, " TABLE \"%s\" CONSTRAINT \"%s\" %s",
+ PQgetvalue(result, i, i_ontable),
+ PQgetvalue(result, i, i_conname),
+ PQgetvalue(result, i, i_condef));
+
+ printTableAddFooter(&cont, buf.data);
+ }
+ }
+ PQclear(result);
/* print any row-level policies */
if (pset.sversion >= 90500)
--
2.43.5
v21-0005-Add-support-for-NOT-ENFORCED-in-foreign-key-cons.patchapplication/octet-stream; name=v21-0005-Add-support-for-NOT-ENFORCED-in-foreign-key-cons.patchDownload
From 9ec521f886a7cd2830b6cce3b22f8affffa503ab Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Fri, 28 Mar 2025 13:25:55 +0530
Subject: [PATCH v21 5/6] Add support for NOT ENFORCED in foreign key
constraints.
Typically, when a foreign key (FK) constraint is created on a table,
action and check triggers are added to maintain data integrity. With
this patch, if a constraint is marked as NOT ENFORCED, integrity
checks are no longer required, making these triggers unnecessary.
Consequently, when creating a NOT ENFORCED FK constraint, triggers
will not be created, and the constraint will be marked as NOT VALID.
Similarly, if an existing FK constraint is changed to NOT ENFORCED,
the associated triggers will be dropped, and the constraint will also
be marked as NOT VALID. Conversely, if a NOT ENFORCED FK constraint is
changed to ENFORCED, the necessary triggers will be created, and the
will be changed to VALID by performing necessary validation.
----
NOTE: In this patch, the tryAttachPartitionForeignKey() function will
not merge the constraint if the enforcibility differs. This will be
addressed in the next patch.
----
---
doc/src/sgml/catalogs.sgml | 2 +-
doc/src/sgml/ref/alter_table.sgml | 5 +-
doc/src/sgml/ref/create_table.sgml | 2 +-
src/backend/catalog/pg_constraint.c | 5 +-
src/backend/catalog/sql_features.txt | 2 +-
src/backend/commands/tablecmds.c | 471 ++++++++++++++++++----
src/backend/parser/gram.y | 11 +-
src/backend/parser/parse_utilcmd.c | 14 +-
src/backend/utils/cache/relcache.c | 1 +
src/include/nodes/parsenodes.h | 2 +
src/include/utils/rel.h | 3 +
src/test/regress/expected/constraints.out | 8 +-
src/test/regress/expected/foreign_key.out | 194 ++++++++-
src/test/regress/expected/inherit.out | 81 ++--
src/test/regress/sql/foreign_key.sql | 119 +++++-
src/test/regress/sql/inherit.sql | 7 +
16 files changed, 779 insertions(+), 148 deletions(-)
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index fb050635551..014bb815665 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2620,7 +2620,7 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l
</para>
<para>
Is the constraint enforced?
- Currently, can be false only for CHECK constraints
+ Currently, can be false only for foreign keys and CHECK constraints
</para></entry>
</row>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 11d1bc7dbe1..ece438f0075 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -58,7 +58,7 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable>
ALTER [ COLUMN ] <replaceable class="parameter">column_name</replaceable> SET COMPRESSION <replaceable class="parameter">compression_method</replaceable>
ADD <replaceable class="parameter">table_constraint</replaceable> [ NOT VALID ]
ADD <replaceable class="parameter">table_constraint_using_index</replaceable>
- ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+ ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]
ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> [ INHERIT | NO INHERIT ]
VALIDATE CONSTRAINT <replaceable class="parameter">constraint_name</replaceable>
DROP CONSTRAINT [ IF EXISTS ] <replaceable class="parameter">constraint_name</replaceable> [ RESTRICT | CASCADE ]
@@ -589,7 +589,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
This form validates a foreign key or check constraint that was
previously created as <literal>NOT VALID</literal>, by scanning the
table to ensure there are no rows for which the constraint is not
- satisfied. Nothing happens if the constraint is already marked valid.
+ satisfied. If the constraint is not enforced, an error is thrown.
+ Nothing happens if the constraint is already marked valid.
(See <xref linkend="sql-altertable-notes"/> below for an explanation
of the usefulness of this command.)
</para>
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index e5c034d724e..4a41b2f5530 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -1409,7 +1409,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</para>
<para>
- This is currently only supported for <literal>CHECK</literal>
+ This is currently only supported for foreign key and <literal>CHECK</literal>
constraints.
</para>
</listitem>
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index ac80652baf2..0467e7442ff 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -100,8 +100,9 @@ CreateConstraintEntry(const char *constraintName,
ObjectAddresses *addrs_auto;
ObjectAddresses *addrs_normal;
- /* Only CHECK constraint can be not enforced */
- Assert(isEnforced || constraintType == CONSTRAINT_CHECK);
+ /* Only CHECK or FOREIGN KEY constraint can be not enforced */
+ Assert(isEnforced || constraintType == CONSTRAINT_CHECK ||
+ constraintType == CONSTRAINT_FOREIGN);
/* NOT ENFORCED constraint must be NOT VALID */
Assert(isEnforced || !isValidated);
diff --git a/src/backend/catalog/sql_features.txt b/src/backend/catalog/sql_features.txt
index 2f250d2c57b..ebe85337c28 100644
--- a/src/backend/catalog/sql_features.txt
+++ b/src/backend/catalog/sql_features.txt
@@ -281,7 +281,7 @@ F461 Named character sets NO
F471 Scalar subquery values YES
F481 Expanded NULL predicate YES
F491 Constraint management YES
-F492 Optional table constraint enforcement NO check constraints only
+F492 Optional table constraint enforcement YES except not-null constraints
F501 Features and conformance views YES
F501 Features and conformance views 01 SQL_FEATURES view YES
F501 Features and conformance views 02 SQL_SIZING view YES
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 10624353b0a..6089765249a 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -395,6 +395,14 @@ static ObjectAddress ATExecAlterConstraint(List **wqueue, Relation rel,
static bool ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon, Relation conrel,
Relation tgrel, Relation rel, HeapTuple contuple,
bool recurse, LOCKMODE lockmode);
+static bool ATExecAlterConstrEnforceability(List **wqueue, ATAlterConstraint *cmdcon,
+ Relation conrel, Relation tgrel,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, LOCKMODE lockmode,
+ Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger);
static bool ATExecAlterConstrDeferrability(List **wqueue, ATAlterConstraint *cmdcon,
Relation conrel, Relation tgrel, Relation rel,
HeapTuple contuple, bool recurse,
@@ -405,6 +413,14 @@ static bool ATExecAlterConstrInheritability(List **wqueue, ATAlterConstraint *cm
static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
bool deferrable, bool initdeferred,
List **otherrelids);
+static void AlterConstrEnforceabilityRecurse(List **wqueue, ATAlterConstraint *cmdcon,
+ Relation conrel, Relation tgrel,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, LOCKMODE lockmode,
+ Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger);
static void AlterConstrDeferrabilityRecurse(List **wqueue, ATAlterConstraint *cmdcon,
Relation conrel, Relation tgrel, Relation rel,
HeapTuple contuple, bool recurse,
@@ -10610,7 +10626,7 @@ addFkConstraint(addFkConstraintSides fkside,
CONSTRAINT_FOREIGN,
fkconstraint->deferrable,
fkconstraint->initdeferred,
- true, /* Is Enforced */
+ fkconstraint->is_enforced,
fkconstraint->initially_valid,
parentConstr,
RelationGetRelid(rel),
@@ -10728,21 +10744,23 @@ addFkRecurseReferenced(Constraint *fkconstraint, Relation rel,
Oid parentDelTrigger, Oid parentUpdTrigger,
bool with_period)
{
- Oid deleteTriggerOid,
- updateTriggerOid;
+ Oid deleteTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
Assert(CheckRelationLockedByMe(pkrel, ShareRowExclusiveLock, true));
Assert(CheckRelationLockedByMe(rel, ShareRowExclusiveLock, true));
/*
- * Create the action triggers that enforce the constraint.
+ * Create action triggers to enforce the constraint, or skip them if the
+ * constraint is NOT ENFORCED.
*/
- createForeignKeyActionTriggers(RelationGetRelid(rel),
- RelationGetRelid(pkrel),
- fkconstraint,
- parentConstr, indexOid,
- parentDelTrigger, parentUpdTrigger,
- &deleteTriggerOid, &updateTriggerOid);
+ if (fkconstraint->is_enforced)
+ createForeignKeyActionTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
+ fkconstraint,
+ parentConstr, indexOid,
+ parentDelTrigger, parentUpdTrigger,
+ &deleteTriggerOid, &updateTriggerOid);
/*
* If the referenced table is partitioned, recurse on ourselves to handle
@@ -10863,8 +10881,8 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
Oid parentInsTrigger, Oid parentUpdTrigger,
bool with_period)
{
- Oid insertTriggerOid,
- updateTriggerOid;
+ Oid insertTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
Assert(OidIsValid(parentConstr));
Assert(CheckRelationLockedByMe(rel, ShareRowExclusiveLock, true));
@@ -10876,29 +10894,32 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
errmsg("foreign key constraints are not supported on foreign tables")));
/*
- * Add the check triggers to it and, if necessary, schedule it to be
- * checked in Phase 3.
+ * Add check triggers if the constraint is ENFORCED, and if needed,
+ * schedule them to be checked in Phase 3.
*
* If the relation is partitioned, drill down to do it to its partitions.
*/
- createForeignKeyCheckTriggers(RelationGetRelid(rel),
- RelationGetRelid(pkrel),
- fkconstraint,
- parentConstr,
- indexOid,
- parentInsTrigger, parentUpdTrigger,
- &insertTriggerOid, &updateTriggerOid);
+ if (fkconstraint->is_enforced)
+ createForeignKeyCheckTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
+ fkconstraint,
+ parentConstr,
+ indexOid,
+ parentInsTrigger, parentUpdTrigger,
+ &insertTriggerOid, &updateTriggerOid);
if (rel->rd_rel->relkind == RELKIND_RELATION)
{
/*
* Tell Phase 3 to check that the constraint is satisfied by existing
- * rows. We can skip this during table creation, when requested
- * explicitly by specifying NOT VALID in an ADD FOREIGN KEY command,
- * and when we're recreating a constraint following a SET DATA TYPE
- * operation that did not impugn its validity.
+ * rows. We can skip this during table creation, when constraint is
+ * specified as NOT ENFORCED, or when requested explicitly by
+ * specifying NOT VALID in an ADD FOREIGN KEY command, and when we're
+ * recreating a constraint following a SET DATA TYPE operation that
+ * did not impugn its validity.
*/
- if (wqueue && !old_check_ok && !fkconstraint->skip_validation)
+ if (wqueue && !old_check_ok && !fkconstraint->skip_validation &&
+ fkconstraint->is_enforced)
{
NewConstraint *newcon;
AlteredTableInfo *tab;
@@ -11129,8 +11150,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
AttrNumber confdelsetcols[INDEX_MAX_KEYS];
Constraint *fkconstraint;
ObjectAddress address;
- Oid deleteTriggerOid,
- updateTriggerOid;
+ Oid deleteTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(constrOid));
if (!HeapTupleIsValid(tuple))
@@ -11190,8 +11211,9 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
fkconstraint->fk_del_set_cols = NIL;
fkconstraint->old_conpfeqop = NIL;
fkconstraint->old_pktable_oid = InvalidOid;
+ fkconstraint->is_enforced = constrForm->conenforced;
fkconstraint->skip_validation = false;
- fkconstraint->initially_valid = true;
+ fkconstraint->initially_valid = constrForm->convalidated;
/* set up colnames that are used to generate the constraint name */
for (int i = 0; i < numfks; i++)
@@ -11219,9 +11241,10 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
* parent OIDs for similar triggers that will be created on the
* partition in addFkRecurseReferenced().
*/
- GetForeignKeyActionTriggers(trigrel, constrOid,
- constrForm->confrelid, constrForm->conrelid,
- &deleteTriggerOid, &updateTriggerOid);
+ if (constrForm->conenforced)
+ GetForeignKeyActionTriggers(trigrel, constrOid,
+ constrForm->confrelid, constrForm->conrelid,
+ &deleteTriggerOid, &updateTriggerOid);
/* Add this constraint ... */
address = addFkConstraint(addFkReferencedSide,
@@ -11354,8 +11377,8 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
Oid indexOid;
ObjectAddress address;
ListCell *lc;
- Oid insertTriggerOid,
- updateTriggerOid;
+ Oid insertTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
bool with_period;
tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(parentConstrOid));
@@ -11387,17 +11410,18 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
mapped_conkey[i] = attmap->attnums[conkey[i] - 1];
/*
- * Get the "check" triggers belonging to the constraint to pass as
- * parent OIDs for similar triggers that will be created on the
- * partition in addFkRecurseReferencing(). They are also passed to
- * tryAttachPartitionForeignKey() below to simply assign as parents to
- * the partition's existing "check" triggers, that is, if the
- * corresponding constraints is deemed attachable to the parent
- * constraint.
+ * Get the "check" triggers belonging to the constraint, if it is
+ * ENFORCED, to pass as parent OIDs for similar triggers that will be
+ * created on the partition in addFkRecurseReferencing(). They are
+ * also passed to tryAttachPartitionForeignKey() below to simply
+ * assign as parents to the partition's existing "check" triggers,
+ * that is, if the corresponding constraints is deemed attachable to
+ * the parent constraint.
*/
- GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
- constrForm->confrelid, constrForm->conrelid,
- &insertTriggerOid, &updateTriggerOid);
+ if (constrForm->conenforced)
+ GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
+ constrForm->confrelid, constrForm->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
/*
* Before creating a new constraint, see whether any existing FKs are
@@ -11450,6 +11474,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
fkconstraint->fk_del_set_cols = NIL;
fkconstraint->old_conpfeqop = NIL;
fkconstraint->old_pktable_oid = InvalidOid;
+ fkconstraint->is_enforced = constrForm->conenforced;
fkconstraint->skip_validation = false;
fkconstraint->initially_valid = constrForm->convalidated;
for (int i = 0; i < numfks; i++)
@@ -11564,6 +11589,23 @@ tryAttachPartitionForeignKey(List **wqueue,
if (!HeapTupleIsValid(partcontup))
elog(ERROR, "cache lookup failed for constraint %u", fk->conoid);
partConstr = (Form_pg_constraint) GETSTRUCT(partcontup);
+
+ /*
+ * An error should be raised if the constraint enforceability is different.
+ * Returning false without raising an error, as we do for other attributes,
+ * could lead to a duplicate constraint with the same enforceability as the
+ * parent. While this may be acceptable, it may not be ideal. Therefore,
+ * it's better to raise an error and allow the user to correct the
+ * enforceability before proceeding.
+ */
+ if (partConstr->conenforced != parentConstr->conenforced)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("constraint \"%s\" enforceability conflicts with constraint \"%s\" on relation \"%s\"",
+ NameStr(parentConstr->conname),
+ NameStr(partConstr->conname),
+ RelationGetRelationName(partition))));
+
if (OidIsValid(partConstr->conparentid) ||
partConstr->condeferrable != parentConstr->condeferrable ||
partConstr->condeferred != parentConstr->condeferred ||
@@ -11610,8 +11652,7 @@ AttachPartitionForeignKey(List **wqueue,
bool queueValidation;
Oid partConstrFrelid;
Oid partConstrRelid;
- Oid insertTriggerOid,
- updateTriggerOid;
+ bool parentConstrIsEnforced;
/* Fetch the parent constraint tuple */
parentConstrTup = SearchSysCache1(CONSTROID,
@@ -11619,6 +11660,7 @@ AttachPartitionForeignKey(List **wqueue,
if (!HeapTupleIsValid(parentConstrTup))
elog(ERROR, "cache lookup failed for constraint %u", parentConstrOid);
parentConstr = (Form_pg_constraint) GETSTRUCT(parentConstrTup);
+ parentConstrIsEnforced = parentConstr->conenforced;
/* Fetch the child constraint tuple */
partcontup = SearchSysCache1(CONSTROID,
@@ -11668,17 +11710,24 @@ AttachPartitionForeignKey(List **wqueue,
/*
* Like the constraint, attach partition's "check" triggers to the
- * corresponding parent triggers.
+ * corresponding parent triggers if the constraint is ENFORCED. NOT
+ * ENFORCED constraints do not have these triggers.
*/
- GetForeignKeyCheckTriggers(trigrel,
- partConstrOid, partConstrFrelid, partConstrRelid,
- &insertTriggerOid, &updateTriggerOid);
- Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
- TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
- RelationGetRelid(partition));
- Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
- TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
- RelationGetRelid(partition));
+ if (parentConstrIsEnforced)
+ {
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
+ GetForeignKeyCheckTriggers(trigrel,
+ partConstrOid, partConstrFrelid, partConstrRelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
+ RelationGetRelid(partition));
+ Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
+ RelationGetRelid(partition));
+ }
/*
* We updated this pg_constraint row above to set its parent; validating
@@ -11792,6 +11841,10 @@ RemoveInheritedConstraint(Relation conrel, Relation trigrel, Oid conoid,
*
* The subroutine for tryAttachPartitionForeignKey handles the deletion of
* action triggers for the foreign key constraint.
+ *
+ * If valid confrelid and conrelid values are not provided, the respective
+ * trigger check will be skipped, and the trigger will be considered for
+ * removal.
*/
static void
DropForeignKeyConstraintTriggers(Relation trigrel, Oid conoid, Oid confrelid,
@@ -11812,10 +11865,27 @@ DropForeignKeyConstraintTriggers(Relation trigrel, Oid conoid, Oid confrelid,
Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
ObjectAddress trigger;
- if (trgform->tgconstrrelid != conrelid)
+ /* Invalid if trigger is not for a referential integrity constraint */
+ if (!OidIsValid(trgform->tgconstrrelid))
continue;
- if (trgform->tgrelid != confrelid)
+ if (OidIsValid(conrelid) && trgform->tgconstrrelid != conrelid)
continue;
+ if (OidIsValid(confrelid) && trgform->tgrelid != confrelid)
+ continue;
+
+ /* We should be droping trigger related to foreign key constraint */
+ Assert(trgform->tgfoid == F_RI_FKEY_CHECK_INS ||
+ trgform->tgfoid == F_RI_FKEY_CHECK_UPD ||
+ trgform->tgfoid == F_RI_FKEY_CASCADE_DEL ||
+ trgform->tgfoid == F_RI_FKEY_CASCADE_UPD ||
+ trgform->tgfoid == F_RI_FKEY_RESTRICT_DEL ||
+ trgform->tgfoid == F_RI_FKEY_RESTRICT_UPD ||
+ trgform->tgfoid == F_RI_FKEY_SETNULL_DEL ||
+ trgform->tgfoid == F_RI_FKEY_SETNULL_UPD ||
+ trgform->tgfoid == F_RI_FKEY_SETDEFAULT_DEL ||
+ trgform->tgfoid == F_RI_FKEY_SETDEFAULT_UPD ||
+ trgform->tgfoid == F_RI_FKEY_NOACTION_DEL ||
+ trgform->tgfoid == F_RI_FKEY_NOACTION_UPD);
/*
* The constraint is originally set up to contain this trigger as an
@@ -12028,6 +12098,11 @@ ATExecAlterConstraint(List **wqueue, Relation rel, ATAlterConstraint *cmdcon,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("constraint \"%s\" of relation \"%s\" is not a foreign key constraint",
cmdcon->conname, RelationGetRelationName(rel))));
+ if (cmdcon->alterEnforceability && currcon->contype != CONSTRAINT_FOREIGN)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot alter enforceability of constraint \"%s\" of relation \"%s\"",
+ cmdcon->conname, RelationGetRelationName(rel))));
if (cmdcon->alterInheritability &&
currcon->contype != CONSTRAINT_NOTNULL)
ereport(ERROR,
@@ -12107,7 +12182,7 @@ ATExecAlterConstraint(List **wqueue, Relation rel, ATAlterConstraint *cmdcon,
/*
* A subroutine of ATExecAlterConstraint that calls the respective routines for
- * altering constraint attributes.
+ * altering constraint's enforceability, deferrability or inheritability.
*/
static bool
ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon,
@@ -12115,16 +12190,35 @@ ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon,
HeapTuple contuple, bool recurse,
LOCKMODE lockmode)
{
+ Form_pg_constraint currcon;
bool changed = false;
List *otherrelids = NIL;
+ currcon = (Form_pg_constraint) GETSTRUCT(contuple);
+
/*
- * Do the catalog work for the deferrability change, recurse if necessary.
+ * Do the catalog work for the enforceability or deferrability change,
+ * recurse if necessary.
+ *
+ * Note that even if deferrability is requested to be altered along with
+ * enforceability, we don't need to explicitly update multiple entries in
+ * pg_trigger related to deferrability.
+ *
+ * Modifying enforceability involves either creating or dropping the
+ * trigger, during which the deferrability setting will be adjusted
+ * automatically.
*/
- if (cmdcon->alterDeferrability &&
- ATExecAlterConstrDeferrability(wqueue, cmdcon, conrel, tgrel, rel,
- contuple, recurse, &otherrelids,
- lockmode))
+ if (cmdcon->alterEnforceability &&
+ ATExecAlterConstrEnforceability(wqueue, cmdcon, conrel, tgrel,
+ currcon->conrelid, currcon->confrelid,
+ contuple, lockmode, InvalidOid,
+ InvalidOid, InvalidOid, InvalidOid))
+ changed = true;
+
+ else if (cmdcon->alterDeferrability &&
+ ATExecAlterConstrDeferrability(wqueue, cmdcon, conrel, tgrel, rel,
+ contuple, recurse, &otherrelids,
+ lockmode))
{
/*
* AlterConstrUpdateConstraintEntry already invalidated relcache for
@@ -12149,6 +12243,151 @@ ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon,
return changed;
}
+/*
+ * Returns true if the constraint's enforceability is altered.
+ *
+ * Depending on whether the constraint is being set to ENFORCED or NOT
+ * ENFORCED, it creates or drops the trigger accordingly.
+ *
+ * Note that we must recurse even when trying to change a 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. 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.
+ */
+static bool
+ATExecAlterConstrEnforceability(List **wqueue, ATAlterConstraint *cmdcon,
+ Relation conrel, Relation tgrel,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, LOCKMODE lockmode,
+ Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger)
+{
+ Form_pg_constraint currcon;
+ Oid conoid;
+ Relation rel;
+ bool changed = false;
+
+ /* Since this function recurses, it could be driven to stack overflow */
+ check_stack_depth();
+
+ Assert(cmdcon->alterEnforceability);
+
+ currcon = (Form_pg_constraint) GETSTRUCT(contuple);
+ conoid = currcon->oid;
+
+ /* Should be foreign key constraint */
+ Assert(currcon->contype == CONSTRAINT_FOREIGN);
+
+ rel = table_open(currcon->conrelid, lockmode);
+
+ if (currcon->conenforced != cmdcon->is_enforced)
+ {
+ AlterConstrUpdateConstraintEntry(cmdcon, conrel, contuple);
+ changed = true;
+ }
+
+ /* Drop triggers */
+ if (!cmdcon->is_enforced)
+ {
+ /*
+ * When setting a constraint to NOT ENFORCED, the constraint triggers
+ * need to be dropped. Therefore, we must process the child relations
+ * first, followed by the parent, to account for dependencies.
+ */
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE)
+ AlterConstrEnforceabilityRecurse(wqueue, cmdcon, conrel, tgrel,
+ fkrelid, pkrelid, contuple,
+ lockmode, InvalidOid, InvalidOid,
+ InvalidOid, InvalidOid);
+
+ /* Drop all the triggers */
+ DropForeignKeyConstraintTriggers(tgrel, conoid, InvalidOid, InvalidOid);
+ }
+ else if (changed) /* Create triggers */
+ {
+ Oid ReferencedDelTriggerOid = InvalidOid,
+ ReferencedUpdTriggerOid = InvalidOid,
+ ReferencingInsTriggerOid = InvalidOid,
+ ReferencingUpdTriggerOid = InvalidOid;
+
+ /* Prepare the minimal information required for trigger creation. */
+ Constraint *fkconstraint = makeNode(Constraint);
+
+ fkconstraint->conname = pstrdup(NameStr(currcon->conname));
+ fkconstraint->fk_matchtype = currcon->confmatchtype;
+ fkconstraint->fk_upd_action = currcon->confupdtype;
+ fkconstraint->fk_del_action = currcon->confdeltype;
+
+ /* Create referenced triggers */
+ if (currcon->conrelid == fkrelid)
+ createForeignKeyActionTriggers(currcon->conrelid,
+ currcon->confrelid,
+ fkconstraint,
+ conoid,
+ currcon->conindid,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ &ReferencedDelTriggerOid,
+ &ReferencedUpdTriggerOid);
+
+ /* Create referencing triggers */
+ if (currcon->confrelid == pkrelid)
+ createForeignKeyCheckTriggers(currcon->conrelid,
+ pkrelid,
+ fkconstraint,
+ conoid,
+ currcon->conindid,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger,
+ &ReferencingInsTriggerOid,
+ &ReferencingUpdTriggerOid);
+
+ /*
+ * Tell Phase 3 to check that the constraint is satisfied by existing
+ * rows.
+ */
+ if (rel->rd_rel->relkind == RELKIND_RELATION)
+ {
+ AlteredTableInfo *tab;
+ NewConstraint *newcon;
+
+ newcon = (NewConstraint *) palloc0(sizeof(NewConstraint));
+ newcon->name = fkconstraint->conname;
+ newcon->contype = CONSTR_FOREIGN;
+ newcon->refrelid = currcon->confrelid;
+ newcon->refindid = currcon->conindid;
+ newcon->conid = currcon->oid;
+ newcon->qual = (Node *) fkconstraint;
+
+ /* Find or create work queue entry for this table */
+ tab = ATGetQueueEntry(wqueue, rel);
+ tab->constraints = lappend(tab->constraints, newcon);
+ }
+
+ /*
+ * If the table at either end of the constraint is partitioned, we
+ * need to recurse and create triggers for each constraint that is a
+ * child of this one.
+ */
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
+ get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE)
+ AlterConstrEnforceabilityRecurse(wqueue, cmdcon, conrel, tgrel,
+ fkrelid, pkrelid, contuple,
+ lockmode, ReferencedDelTriggerOid,
+ ReferencedUpdTriggerOid,
+ ReferencingInsTriggerOid,
+ ReferencingUpdTriggerOid);
+ }
+
+ table_close(rel, NoLock);
+
+ return changed;
+}
+
/*
* Returns true if the constraint's deferrability is altered.
*
@@ -12353,6 +12592,55 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel,
systable_endscan(tgscan);
}
+/*
+ * Invokes ATExecAlterConstrEnforceability for each constraint that is a child of
+ * the specified constraint.
+ *
+ * Note that this doesn't handle recursion the normal way, viz. by scanning the
+ * list of child relations and recursing; instead it uses the conparentid
+ * relationships. This may need to be reconsidered.
+ *
+ * The arguments to this function have the same meaning as the arguments to
+ * ATExecAlterConstrEnforceability.
+ */
+static void
+AlterConstrEnforceabilityRecurse(List **wqueue, ATAlterConstraint *cmdcon,
+ Relation conrel, Relation tgrel,
+ const Oid fkrelid, const Oid pkrelid,
+ HeapTuple contuple, LOCKMODE lockmode,
+ Oid ReferencedParentDelTrigger,
+ Oid ReferencedParentUpdTrigger,
+ Oid ReferencingParentInsTrigger,
+ Oid ReferencingParentUpdTrigger)
+{
+ Form_pg_constraint currcon;
+ Oid conoid;
+ ScanKeyData pkey;
+ SysScanDesc pscan;
+ HeapTuple childtup;
+
+ currcon = (Form_pg_constraint) GETSTRUCT(contuple);
+ conoid = currcon->oid;
+
+ ScanKeyInit(&pkey,
+ Anum_pg_constraint_conparentid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+
+ pscan = systable_beginscan(conrel, ConstraintParentIndexId,
+ true, NULL, 1, &pkey);
+
+ while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
+ ATExecAlterConstrEnforceability(wqueue, cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, childtup, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
+
+ systable_endscan(pscan);
+}
+
/*
* Invokes ATExecAlterConstrDeferrability for each constraint that is a child of
* the specified constraint.
@@ -12413,11 +12701,25 @@ AlterConstrUpdateConstraintEntry(ATAlterConstraint *cmdcon, Relation conrel,
HeapTuple copyTuple;
Form_pg_constraint copy_con;
- Assert(cmdcon->alterDeferrability || cmdcon->alterInheritability);
+ Assert(cmdcon->alterEnforceability || cmdcon->alterDeferrability ||
+ cmdcon->alterInheritability);
copyTuple = heap_copytuple(contuple);
copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
+ if (cmdcon->alterEnforceability)
+ {
+ copy_con->conenforced = cmdcon->is_enforced;
+
+ /*
+ * 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.
+ */
+ copy_con->convalidated = cmdcon->is_enforced;
+ }
if (cmdcon->alterDeferrability)
{
copy_con->condeferrable = cmdcon->deferrable;
@@ -17137,9 +17439,9 @@ MergeConstraintsIntoExisting(Relation child_rel, Relation parent_rel)
NameStr(child_con->conname), RelationGetRelationName(child_rel))));
/*
- * A non-enforced child constraint cannot be merged with an
- * enforced parent constraint. However, the reverse is allowed,
- * where the child constraint is enforced.
+ * A NOT ENFORCED child constraint cannot be merged with an
+ * ENFORCED parent constraint. However, the reverse is allowed,
+ * where the child constraint is ENFORCED.
*/
if (parent_con->conenforced && !child_con->conenforced)
ereport(ERROR,
@@ -20510,8 +20812,6 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
Form_pg_constraint conform;
- Oid insertTriggerOid,
- updateTriggerOid;
contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
if (!HeapTupleIsValid(contup))
@@ -20538,17 +20838,25 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
/*
* Also, look up the partition's "check" triggers corresponding to the
- * constraint being detached and detach them from the parent triggers.
+ * ENFORCED constraint being detached and detach them from the parent
+ * triggers. NOT ENFORCED constraints do not have these triggers;
+ * therefore, this step is not needed.
*/
- GetForeignKeyCheckTriggers(trigrel,
- fk->conoid, fk->confrelid, fk->conrelid,
- &insertTriggerOid, &updateTriggerOid);
- Assert(OidIsValid(insertTriggerOid));
- TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
- RelationGetRelid(partRel));
- Assert(OidIsValid(updateTriggerOid));
- TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
- RelationGetRelid(partRel));
+ if (fk->conenforced)
+ {
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ Assert(OidIsValid(updateTriggerOid));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ }
/*
* Lastly, create the action triggers on the referenced table, using
@@ -20588,8 +20896,9 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
fkconstraint->conname = pstrdup(NameStr(conform->conname));
fkconstraint->deferrable = conform->condeferrable;
fkconstraint->initdeferred = conform->condeferred;
+ fkconstraint->is_enforced = conform->conenforced;
fkconstraint->skip_validation = true;
- fkconstraint->initially_valid = true;
+ fkconstraint->initially_valid = conform->convalidated;
/* a few irrelevant fields omitted here */
fkconstraint->pktable = NULL;
fkconstraint->fk_attrs = NIL;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 0fc502a3a40..27257ec5dc1 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -2662,6 +2662,8 @@ alter_table_cmd:
n->subtype = AT_AlterConstraint;
n->def = (Node *) c;
c->conname = $3;
+ if ($4 & (CAS_NOT_ENFORCED | CAS_ENFORCED))
+ c->alterEnforceability = true;
if ($4 & (CAS_DEFERRABLE | CAS_NOT_DEFERRABLE |
CAS_INITIALLY_DEFERRED | CAS_INITIALLY_IMMEDIATE))
c->alterDeferrability = true;
@@ -2670,7 +2672,10 @@ alter_table_cmd:
processCASbits($4, @4, "FOREIGN KEY",
&c->deferrable,
&c->initdeferred,
- NULL, NULL, &c->noinherit, yyscanner);
+ &c->is_enforced,
+ NULL,
+ &c->noinherit,
+ yyscanner);
$$ = (Node *) n;
}
/* ALTER TABLE <name> ALTER CONSTRAINT INHERIT */
@@ -4334,8 +4339,8 @@ ConstraintElem:
n->fk_del_set_cols = ($11)->deleteAction->cols;
processCASbits($12, @12, "FOREIGN KEY",
&n->deferrable, &n->initdeferred,
- NULL, &n->skip_validation, NULL,
- yyscanner);
+ &n->is_enforced, &n->skip_validation,
+ NULL, yyscanner);
n->initially_valid = !n->skip_validation;
$$ = (Node *) n;
}
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 9c1541e1fea..62015431fdf 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -2962,8 +2962,10 @@ transformFKConstraints(CreateStmtContext *cxt,
/*
* If CREATE TABLE or adding a column with NULL default, we can safely
- * skip validation of FK constraints, and nonetheless mark them valid.
- * (This will override any user-supplied NOT VALID flag.)
+ * skip validation of FK constraints, and mark them as valid based on the
+ * constraint enforcement flag, since NOT ENFORCED constraints must always
+ * be marked as NOT VALID. (This will override any user-supplied NOT VALID
+ * flag.)
*/
if (skipValidation)
{
@@ -2972,7 +2974,7 @@ transformFKConstraints(CreateStmtContext *cxt,
Constraint *constraint = (Constraint *) lfirst(fkclist);
constraint->skip_validation = true;
- constraint->initially_valid = true;
+ constraint->initially_valid = constraint->is_enforced;
}
}
@@ -3967,7 +3969,8 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
case CONSTR_ATTR_ENFORCED:
if (lastprimarycon == NULL ||
- lastprimarycon->contype != CONSTR_CHECK)
+ (lastprimarycon->contype != CONSTR_CHECK &&
+ lastprimarycon->contype != CONSTR_FOREIGN))
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("misplaced ENFORCED clause"),
@@ -3983,7 +3986,8 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList)
case CONSTR_ATTR_NOT_ENFORCED:
if (lastprimarycon == NULL ||
- lastprimarycon->contype != CONSTR_CHECK)
+ (lastprimarycon->contype != CONSTR_CHECK &&
+ lastprimarycon->contype != CONSTR_FOREIGN))
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("misplaced NOT ENFORCED clause"),
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index e6721056536..18a14ae186e 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4697,6 +4697,7 @@ RelationGetFKeyList(Relation relation)
info->conoid = constraint->oid;
info->conrelid = constraint->conrelid;
info->confrelid = constraint->confrelid;
+ info->conenforced = constraint->conenforced;
DeconstructFkConstraintRow(htup, &info->nkeys,
info->conkey,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index df331b1c0d9..00fefa9483a 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2495,6 +2495,8 @@ typedef struct ATAlterConstraint
{
NodeTag type;
char *conname; /* Constraint name */
+ bool alterEnforceability; /* changing enforceability properties? */
+ bool is_enforced; /* ENFORCED? */
bool alterDeferrability; /* changing deferrability properties? */
bool deferrable; /* DEFERRABLE? */
bool initdeferred; /* INITIALLY DEFERRED? */
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index d94fddd7cef..b552359915f 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -284,6 +284,9 @@ typedef struct ForeignKeyCacheInfo
/* number of columns in the foreign key */
int nkeys;
+ /* Is enforced ? */
+ bool conenforced;
+
/*
* these arrays each have nkeys valid entries:
*/
diff --git a/src/test/regress/expected/constraints.out b/src/test/regress/expected/constraints.out
index 4f39100fcdf..a719d2f74e9 100644
--- a/src/test/regress/expected/constraints.out
+++ b/src/test/regress/expected/constraints.out
@@ -745,13 +745,9 @@ ERROR: misplaced NOT ENFORCED clause
LINE 1: CREATE TABLE UNIQUE_NOTEN_TBL(i int UNIQUE NOT ENFORCED);
^
ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
-ERROR: FOREIGN KEY constraints cannot be marked ENFORCED
-LINE 1: ...TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED;
- ^
+ERROR: cannot alter enforceability of constraint "unique_tbl_i_key" of relation "unique_tbl"
ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORCED;
-ERROR: FOREIGN KEY constraints cannot be marked NOT ENFORCED
-LINE 1: ...ABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORC...
- ^
+ERROR: cannot alter enforceability of constraint "unique_tbl_i_key" of relation "unique_tbl"
DROP TABLE unique_tbl;
--
-- EXCLUDE constraints
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 7f678349a8e..db1e39d5fce 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -1,21 +1,49 @@
--
-- FOREIGN KEY
--
--- MATCH FULL
+-- NOT ENFORCED
--
-- First test, check and cascade
--
CREATE TABLE PKTABLE ( ptest1 int PRIMARY KEY, ptest2 text );
-CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE, ftest2 int );
--- Insert test data into PKTABLE
+CREATE TABLE FKTABLE ( ftest1 int CONSTRAINT fktable_ftest1_fkey REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE NOT ENFORCED,
+ ftest2 int );
+-- Inserting into the foreign key table will not result in an error, even if
+-- there is no matching key in the referenced table.
+INSERT INTO FKTABLE VALUES (1, 2);
+INSERT INTO FKTABLE VALUES (2, 3);
+-- Check FKTABLE
+SELECT * FROM FKTABLE;
+ ftest1 | ftest2
+--------+--------
+ 1 | 2
+ 2 | 3
+(2 rows)
+
+-- Reverting it back to ENFORCED will result in failure because constraint validation will be triggered,
+-- as it was previously in a valid state.
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_fkey"
+DETAIL: Key (ftest1)=(1) is not present in table "pktable".
+-- Insert referenced data that satisfies the constraint, then attempted to
+-- change it.
INSERT INTO PKTABLE VALUES (1, 'Test1');
INSERT INTO PKTABLE VALUES (2, 'Test2');
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+-- Any further inserts will fail due to the enforcement.
+INSERT INTO FKTABLE VALUES (3, 4);
+ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_fkey"
+DETAIL: Key (ftest1)=(3) is not present in table "pktable".
+--
+-- MATCH FULL
+--
+-- First test, check and cascade
+--
+-- Insert test data into PKTABLE
INSERT INTO PKTABLE VALUES (3, 'Test3');
INSERT INTO PKTABLE VALUES (4, 'Test4');
INSERT INTO PKTABLE VALUES (5, 'Test5');
-- Insert successful rows into FK TABLE
-INSERT INTO FKTABLE VALUES (1, 2);
-INSERT INTO FKTABLE VALUES (2, 3);
INSERT INTO FKTABLE VALUES (3, 4);
INSERT INTO FKTABLE VALUES (NULL, 1);
-- Insert a failed row into FK TABLE
@@ -351,6 +379,43 @@ INSERT INTO FKTABLE VALUES (1, NULL);
ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL;
ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_ftest1_ftest2_fkey"
DETAIL: MATCH FULL does not allow mixing of null and nonnull key values.
+-- Modifying other attributes of a constraint should not affect its enforceability, and vice versa
+ALTER TABLE FKTABLE ADD CONSTRAINT fk_con FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE NOT VALID NOT ENFORCED;
+ALTER TABLE FKTABLE ALTER CONSTRAINT fk_con DEFERRABLE INITIALLY DEFERRED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fk_con';
+ condeferrable | condeferred | conenforced | convalidated
+---------------+-------------+-------------+--------------
+ t | t | f | f
+(1 row)
+
+ALTER TABLE FKTABLE ALTER CONSTRAINT fk_con NOT ENFORCED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fk_con';
+ condeferrable | condeferred | conenforced | convalidated
+---------------+-------------+-------------+--------------
+ t | t | f | f
+(1 row)
+
+-- Enforceability also changes the validate state, as data validation will be
+-- performed during this transformation.
+ALTER TABLE FKTABLE ALTER CONSTRAINT fk_con ENFORCED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fk_con';
+ condeferrable | condeferred | conenforced | convalidated
+---------------+-------------+-------------+--------------
+ t | t | t | t
+(1 row)
+
+-- Can change enforceability and deferrability together
+ALTER TABLE FKTABLE ALTER CONSTRAINT fk_con NOT ENFORCED NOT DEFERRABLE;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fk_con';
+ condeferrable | condeferred | conenforced | convalidated
+---------------+-------------+-------------+--------------
+ f | f | f | f
+(1 row)
+
DROP TABLE FKTABLE;
DROP TABLE PKTABLE;
-- MATCH SIMPLE
@@ -1276,6 +1341,13 @@ INSERT INTO fktable VALUES (0, 20);
ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_fk_fkey"
DETAIL: Key (fk)=(20) is not present in table "pktable".
COMMIT;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT ENFORCED;
+BEGIN;
+-- doesn't match FK, but no error.
+UPDATE pktable SET id = 10 WHERE id = 5;
+-- doesn't match PK, but no error.
+INSERT INTO fktable VALUES (0, 20);
+ROLLBACK;
-- try additional syntax
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE;
-- illegal options
@@ -1289,6 +1361,14 @@ ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT VALID;
ERROR: FOREIGN KEY constraints cannot be marked NOT VALID
LINE 1: ...ER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT VALID;
^
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORCED;
+ERROR: conflicting constraint properties
+LINE 1: ...fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORC...
+ ^
+CREATE TEMP TABLE fktable2 (fk int references pktable ENFORCED NOT ENFORCED);
+ERROR: multiple ENFORCED/NOT ENFORCED clauses not allowed
+LINE 1: ...ABLE fktable2 (fk int references pktable ENFORCED NOT ENFORC...
+ ^
-- test order of firing of FK triggers when several RI-induced changes need to
-- be made to the same row. This was broken by subtransaction-related
-- changes in 8.0.
@@ -1586,10 +1666,14 @@ ALTER TABLE fk_partitioned_fk DROP COLUMN fdrop1;
CREATE TABLE fk_partitioned_fk_1 (fdrop1 int, fdrop2 int, a int, fdrop3 int, b int);
ALTER TABLE fk_partitioned_fk_1 DROP COLUMN fdrop1, DROP COLUMN fdrop2, DROP COLUMN fdrop3;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FROM (0,0) TO (1000,1000);
-ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk;
+ALTER TABLE fk_partitioned_fk ADD CONSTRAINT fk_partitioned_fk_a_b_fkey FOREIGN KEY (a, b)
+ REFERENCES fk_notpartitioned_pk NOT ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
+ALTER TABLE fk_partitioned_fk_2 ADD CONSTRAINT fk_partitioned_fk_a_b_fkey FOREIGN KEY (a, b)
+ REFERENCES fk_notpartitioned_pk NOT ENFORCED;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
CREATE TABLE fk_partitioned_fk_3 (fdrop1 int, fdrop2 int, fdrop3 int, fdrop4 int, b int, a int)
PARTITION BY HASH (a);
ALTER TABLE fk_partitioned_fk_3 DROP COLUMN fdrop1, DROP COLUMN fdrop2,
@@ -1665,6 +1749,67 @@ Indexes:
Referenced by:
TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b)
+-- Check the exsting FK trigger
+SELECT conname, tgrelid::regclass as tgrel, regexp_replace(tgname, '[0-9]+', 'N') as tgname, tgtype
+FROM pg_trigger t JOIN pg_constraint c ON (t.tgconstraint = c.oid)
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass)
+ORDER BY tgrelid, tgtype;
+ conname | tgrel | tgname | tgtype
+--------------------------------+-----------------------+--------------------------+--------
+ fk_partitioned_fk_a_b_fkey | fk_notpartitioned_pk | RI_ConstraintTrigger_a_N | 9
+ fk_partitioned_fk_a_b_fkey | fk_notpartitioned_pk | RI_ConstraintTrigger_a_N | 17
+ fk_partitioned_fk_a_b_fkey | fk_partitioned_fk | RI_ConstraintTrigger_c_N | 5
+ fk_partitioned_fk_a_b_fkey | fk_partitioned_fk | RI_ConstraintTrigger_c_N | 17
+ fk_partitioned_fk_a_b_fkey | fk_partitioned_fk_1 | RI_ConstraintTrigger_c_N | 5
+ fk_partitioned_fk_a_b_fkey | fk_partitioned_fk_1 | RI_ConstraintTrigger_c_N | 17
+ fk_partitioned_fk_a_b_fkey | fk_partitioned_fk_2 | RI_ConstraintTrigger_c_N | 5
+ fk_partitioned_fk_a_b_fkey | fk_partitioned_fk_2 | RI_ConstraintTrigger_c_N | 17
+ fk_partitioned_fk_3_a_b_fkey | fk_partitioned_fk_3 | RI_ConstraintTrigger_c_N | 5
+ fk_partitioned_fk_3_a_b_fkey | fk_partitioned_fk_3 | RI_ConstraintTrigger_c_N | 17
+ fk_partitioned_fk_3_0_a_b_fkey | fk_partitioned_fk_3_0 | RI_ConstraintTrigger_c_N | 5
+ fk_partitioned_fk_3_0_a_b_fkey | fk_partitioned_fk_3_0 | RI_ConstraintTrigger_c_N | 17
+ fk_partitioned_fk_3_1_a_b_fkey | fk_partitioned_fk_3_1 | RI_ConstraintTrigger_c_N | 5
+ fk_partitioned_fk_3_1_a_b_fkey | fk_partitioned_fk_3_1 | RI_ConstraintTrigger_c_N | 17
+(14 rows)
+
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+-- No triggers
+SELECT conname, tgrelid::regclass as tgrel, regexp_replace(tgname, '[0-9]+', 'N') as tgname, tgtype
+FROM pg_trigger t JOIN pg_constraint c ON (t.tgconstraint = c.oid)
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass)
+ORDER BY tgrelid, tgtype;
+ conname | tgrel | tgname | tgtype
+---------+-------+--------+--------
+(0 rows)
+
+-- Changing it back to ENFORCED will recreate the necessary triggers.
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+-- Should be exactly the same number of triggers found as before
+SELECT conname, tgrelid::regclass as tgrel, regexp_replace(tgname, '[0-9]+', 'N') as tgname, tgtype
+FROM pg_trigger t JOIN pg_constraint c ON (t.tgconstraint = c.oid)
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass)
+ORDER BY tgrelid, tgtype;
+ conname | tgrel | tgname | tgtype
+--------------------------------+-----------------------+--------------------------+--------
+ fk_partitioned_fk_a_b_fkey | fk_notpartitioned_pk | RI_ConstraintTrigger_a_N | 9
+ fk_partitioned_fk_a_b_fkey | fk_notpartitioned_pk | RI_ConstraintTrigger_a_N | 17
+ fk_partitioned_fk_a_b_fkey | fk_partitioned_fk | RI_ConstraintTrigger_c_N | 5
+ fk_partitioned_fk_a_b_fkey | fk_partitioned_fk | RI_ConstraintTrigger_c_N | 17
+ fk_partitioned_fk_a_b_fkey | fk_partitioned_fk_1 | RI_ConstraintTrigger_c_N | 5
+ fk_partitioned_fk_a_b_fkey | fk_partitioned_fk_1 | RI_ConstraintTrigger_c_N | 17
+ fk_partitioned_fk_a_b_fkey | fk_partitioned_fk_2 | RI_ConstraintTrigger_c_N | 5
+ fk_partitioned_fk_a_b_fkey | fk_partitioned_fk_2 | RI_ConstraintTrigger_c_N | 17
+ fk_partitioned_fk_3_a_b_fkey | fk_partitioned_fk_3 | RI_ConstraintTrigger_c_N | 5
+ fk_partitioned_fk_3_a_b_fkey | fk_partitioned_fk_3 | RI_ConstraintTrigger_c_N | 17
+ fk_partitioned_fk_3_0_a_b_fkey | fk_partitioned_fk_3_0 | RI_ConstraintTrigger_c_N | 5
+ fk_partitioned_fk_3_0_a_b_fkey | fk_partitioned_fk_3_0 | RI_ConstraintTrigger_c_N | 17
+ fk_partitioned_fk_3_1_a_b_fkey | fk_partitioned_fk_3_1 | RI_ConstraintTrigger_c_N | 5
+ fk_partitioned_fk_3_1_a_b_fkey | fk_partitioned_fk_3_1 | RI_ConstraintTrigger_c_N | 17
+(14 rows)
+
ALTER TABLE fk_partitioned_fk DROP CONSTRAINT fk_partitioned_fk_a_b_fkey;
-- done.
DROP TABLE fk_notpartitioned_pk, fk_partitioned_fk;
@@ -1962,6 +2107,43 @@ Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
Foreign-key constraints:
TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
+DROP TABLE fk_partitioned_fk_2;
+CREATE TABLE fk_partitioned_fk_2 (b int, a int,
+ CONSTRAINT fk_part_con FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
+-- fail -- cannot merge constraints with different enforceability.
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+ERROR: constraint "fk_partitioned_fk_a_b_fkey" enforceability conflicts with constraint "fk_part_con" on relation "fk_partitioned_fk_2"
+-- If the constraint is modified to match the enforceability of the parent, it will work.
+BEGIN;
+-- change child constraint
+ALTER TABLE fk_partitioned_fk_2 ALTER CONSTRAINT fk_part_con ENFORCED;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+\d fk_partitioned_fk_2
+ Table "public.fk_partitioned_fk_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ b | integer | | |
+ a | integer | | |
+Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
+Foreign-key constraints:
+ TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
+
+ROLLBACK;
+BEGIN;
+-- or change parent constraint
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+\d fk_partitioned_fk_2
+ Table "public.fk_partitioned_fk_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ b | integer | | |
+ a | integer | | |
+Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
+Foreign-key constraints:
+ TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED
+
+ROLLBACK;
DROP TABLE fk_partitioned_fk_2;
CREATE TABLE fk_partitioned_fk_4 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE) PARTITION BY RANGE (b, a);
CREATE TABLE fk_partitioned_fk_4_1 PARTITION OF fk_partitioned_fk_4 FOR VALUES FROM (1,1) TO (100,100);
diff --git a/src/test/regress/expected/inherit.out b/src/test/regress/expected/inherit.out
index 4d07d0bd79b..caab1164fdb 100644
--- a/src/test/regress/expected/inherit.out
+++ b/src/test/regress/expected/inherit.out
@@ -1332,6 +1332,13 @@ NOTICE: merging constraint "inh_check_constraint5" with inherited definition
alter table p1 add constraint inh_check_constraint6 check (f1 < 10) not enforced;
alter table p1_c1 add constraint inh_check_constraint6 check (f1 < 10) enforced;
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 invalid state of the child constraint will be ignored here.
+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
create table p1_c2(f1 int constraint inh_check_constraint4 check (f1 < 10)) inherits(p1);
NOTICE: merging column "f1" with inherited definition
NOTICE: merging constraint "inh_check_constraint4" with inherited definition
@@ -1356,39 +1363,47 @@ ERROR: constraint "inh_check_constraint6" conflicts with NOT ENFORCED constrain
select conrelid::regclass::text as relname, conname, conislocal, coninhcount, conenforced, convalidated
from pg_constraint where conname like 'inh\_check\_constraint%'
order by 1, 2;
- relname | conname | conislocal | coninhcount | conenforced | convalidated
----------+-----------------------+------------+-------------+-------------+--------------
- p1 | inh_check_constraint1 | t | 0 | t | t
- p1 | inh_check_constraint2 | t | 0 | t | t
- p1 | inh_check_constraint3 | t | 0 | f | f
- p1 | inh_check_constraint4 | t | 0 | f | f
- p1 | inh_check_constraint5 | t | 0 | f | f
- p1 | inh_check_constraint6 | t | 0 | f | f
- p1 | inh_check_constraint8 | t | 0 | t | t
- p1_c1 | inh_check_constraint1 | t | 1 | t | t
- 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
- p1_c1 | inh_check_constraint5 | t | 1 | t | t
- p1_c1 | inh_check_constraint6 | t | 1 | t | t
- p1_c1 | inh_check_constraint7 | t | 0 | f | f
- p1_c1 | inh_check_constraint8 | f | 1 | t | t
- p1_c2 | inh_check_constraint1 | f | 1 | t | t
- p1_c2 | inh_check_constraint2 | f | 1 | t | t
- p1_c2 | inh_check_constraint3 | f | 1 | f | f
- p1_c2 | inh_check_constraint4 | t | 1 | t | t
- p1_c2 | inh_check_constraint5 | f | 1 | f | f
- p1_c2 | inh_check_constraint6 | f | 1 | f | f
- p1_c2 | inh_check_constraint8 | f | 1 | t | t
- p1_c3 | inh_check_constraint1 | f | 2 | t | t
- p1_c3 | inh_check_constraint2 | f | 2 | t | t
- p1_c3 | inh_check_constraint3 | f | 2 | f | f
- p1_c3 | inh_check_constraint4 | f | 2 | f | f
- p1_c3 | inh_check_constraint5 | f | 2 | t | t
- p1_c3 | inh_check_constraint6 | f | 2 | t | t
- p1_c3 | inh_check_constraint7 | f | 1 | f | f
- p1_c3 | inh_check_constraint8 | f | 2 | t | t
-(30 rows)
+ relname | conname | conislocal | coninhcount | conenforced | convalidated
+---------+------------------------+------------+-------------+-------------+--------------
+ p1 | inh_check_constraint1 | t | 0 | t | t
+ p1 | inh_check_constraint10 | t | 0 | f | f
+ p1 | inh_check_constraint2 | t | 0 | t | t
+ p1 | inh_check_constraint3 | t | 0 | f | f
+ p1 | inh_check_constraint4 | t | 0 | f | f
+ p1 | inh_check_constraint5 | t | 0 | f | f
+ p1 | inh_check_constraint6 | t | 0 | f | f
+ 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_constraint2 | t | 1 | t | t
+ p1_c1 | inh_check_constraint3 | t | 1 | f | f
+ p1_c1 | inh_check_constraint4 | t | 1 | f | f
+ p1_c1 | inh_check_constraint5 | t | 1 | t | t
+ p1_c1 | inh_check_constraint6 | t | 1 | t | t
+ p1_c1 | inh_check_constraint7 | t | 0 | f | f
+ p1_c1 | inh_check_constraint8 | f | 1 | t | t
+ p1_c1 | inh_check_constraint9 | t | 1 | t | f
+ p1_c2 | inh_check_constraint1 | f | 1 | t | t
+ p1_c2 | inh_check_constraint10 | f | 1 | f | f
+ p1_c2 | inh_check_constraint2 | f | 1 | t | t
+ p1_c2 | inh_check_constraint3 | f | 1 | f | f
+ p1_c2 | inh_check_constraint4 | t | 1 | t | t
+ p1_c2 | inh_check_constraint5 | f | 1 | f | f
+ p1_c2 | inh_check_constraint6 | f | 1 | f | f
+ p1_c2 | inh_check_constraint8 | f | 1 | t | t
+ p1_c2 | inh_check_constraint9 | f | 1 | f | f
+ p1_c3 | inh_check_constraint1 | f | 2 | t | t
+ p1_c3 | inh_check_constraint10 | f | 2 | t | t
+ p1_c3 | inh_check_constraint2 | f | 2 | t | t
+ p1_c3 | inh_check_constraint3 | f | 2 | f | f
+ p1_c3 | inh_check_constraint4 | f | 2 | f | f
+ p1_c3 | inh_check_constraint5 | f | 2 | t | t
+ p1_c3 | inh_check_constraint6 | f | 2 | t | t
+ p1_c3 | inh_check_constraint7 | f | 1 | f | f
+ p1_c3 | inh_check_constraint8 | f | 2 | t | t
+ p1_c3 | inh_check_constraint9 | f | 2 | t | t
+(38 rows)
drop table p1 cascade;
NOTICE: drop cascades to 3 other objects
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index 44945b0453a..05dd443106c 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -2,23 +2,46 @@
-- FOREIGN KEY
--
--- MATCH FULL
+-- NOT ENFORCED
--
-- First test, check and cascade
--
CREATE TABLE PKTABLE ( ptest1 int PRIMARY KEY, ptest2 text );
-CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE, ftest2 int );
+CREATE TABLE FKTABLE ( ftest1 int CONSTRAINT fktable_ftest1_fkey REFERENCES PKTABLE MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE NOT ENFORCED,
+ ftest2 int );
--- Insert test data into PKTABLE
+-- Inserting into the foreign key table will not result in an error, even if
+-- there is no matching key in the referenced table.
+INSERT INTO FKTABLE VALUES (1, 2);
+INSERT INTO FKTABLE VALUES (2, 3);
+
+-- Check FKTABLE
+SELECT * FROM FKTABLE;
+
+-- Reverting it back to ENFORCED will result in failure because constraint validation will be triggered,
+-- as it was previously in a valid state.
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+
+-- Insert referenced data that satisfies the constraint, then attempted to
+-- change it.
INSERT INTO PKTABLE VALUES (1, 'Test1');
INSERT INTO PKTABLE VALUES (2, 'Test2');
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_ftest1_fkey ENFORCED;
+
+-- Any further inserts will fail due to the enforcement.
+INSERT INTO FKTABLE VALUES (3, 4);
+
+--
+-- MATCH FULL
+--
+-- First test, check and cascade
+--
+-- Insert test data into PKTABLE
INSERT INTO PKTABLE VALUES (3, 'Test3');
INSERT INTO PKTABLE VALUES (4, 'Test4');
INSERT INTO PKTABLE VALUES (5, 'Test5');
-- Insert successful rows into FK TABLE
-INSERT INTO FKTABLE VALUES (1, 2);
-INSERT INTO FKTABLE VALUES (2, 3);
INSERT INTO FKTABLE VALUES (3, 4);
INSERT INTO FKTABLE VALUES (NULL, 1);
@@ -230,6 +253,27 @@ INSERT INTO FKTABLE VALUES (1, NULL);
ALTER TABLE FKTABLE ADD FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE MATCH FULL;
+-- Modifying other attributes of a constraint should not affect its enforceability, and vice versa
+ALTER TABLE FKTABLE ADD CONSTRAINT fk_con FOREIGN KEY(ftest1, ftest2) REFERENCES PKTABLE NOT VALID NOT ENFORCED;
+ALTER TABLE FKTABLE ALTER CONSTRAINT fk_con DEFERRABLE INITIALLY DEFERRED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fk_con';
+
+ALTER TABLE FKTABLE ALTER CONSTRAINT fk_con NOT ENFORCED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fk_con';
+
+-- Enforceability also changes the validate state, as data validation will be
+-- performed during this transformation.
+ALTER TABLE FKTABLE ALTER CONSTRAINT fk_con ENFORCED;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fk_con';
+
+-- Can change enforceability and deferrability together
+ALTER TABLE FKTABLE ALTER CONSTRAINT fk_con NOT ENFORCED NOT DEFERRABLE;
+SELECT condeferrable, condeferred, conenforced, convalidated
+FROM pg_constraint WHERE conname = 'fk_con';
+
DROP TABLE FKTABLE;
DROP TABLE PKTABLE;
@@ -968,12 +1012,25 @@ INSERT INTO fktable VALUES (0, 20);
COMMIT;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT ENFORCED;
+
+BEGIN;
+
+-- doesn't match FK, but no error.
+UPDATE pktable SET id = 10 WHERE id = 5;
+-- doesn't match PK, but no error.
+INSERT INTO fktable VALUES (0, 20);
+
+ROLLBACK;
+
-- try additional syntax
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE;
-- illegal options
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE INITIALLY DEFERRED;
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NO INHERIT;
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT VALID;
+ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORCED;
+CREATE TEMP TABLE fktable2 (fk int references pktable ENFORCED NOT ENFORCED);
-- test order of firing of FK triggers when several RI-induced changes need to
-- be made to the same row. This was broken by subtransaction-related
@@ -1184,11 +1241,14 @@ ALTER TABLE fk_partitioned_fk DROP COLUMN fdrop1;
CREATE TABLE fk_partitioned_fk_1 (fdrop1 int, fdrop2 int, a int, fdrop3 int, b int);
ALTER TABLE fk_partitioned_fk_1 DROP COLUMN fdrop1, DROP COLUMN fdrop2, DROP COLUMN fdrop3;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_1 FOR VALUES FROM (0,0) TO (1000,1000);
-ALTER TABLE fk_partitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk;
+ALTER TABLE fk_partitioned_fk ADD CONSTRAINT fk_partitioned_fk_a_b_fkey FOREIGN KEY (a, b)
+ REFERENCES fk_notpartitioned_pk NOT ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
+ALTER TABLE fk_partitioned_fk_2 ADD CONSTRAINT fk_partitioned_fk_a_b_fkey FOREIGN KEY (a, b)
+ REFERENCES fk_notpartitioned_pk NOT ENFORCED;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
-
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
CREATE TABLE fk_partitioned_fk_3 (fdrop1 int, fdrop2 int, fdrop3 int, fdrop4 int, b int, a int)
PARTITION BY HASH (a);
ALTER TABLE fk_partitioned_fk_3 DROP COLUMN fdrop1, DROP COLUMN fdrop2,
@@ -1234,6 +1294,32 @@ UPDATE fk_notpartitioned_pk SET b = 1502 WHERE a = 1500;
UPDATE fk_notpartitioned_pk SET b = 2504 WHERE a = 2500;
-- check psql behavior
\d fk_notpartitioned_pk
+
+-- Check the exsting FK trigger
+SELECT conname, tgrelid::regclass as tgrel, regexp_replace(tgname, '[0-9]+', 'N') as tgname, tgtype
+FROM pg_trigger t JOIN pg_constraint c ON (t.tgconstraint = c.oid)
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass)
+ORDER BY tgrelid, tgtype;
+
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+-- No triggers
+SELECT conname, tgrelid::regclass as tgrel, regexp_replace(tgname, '[0-9]+', 'N') as tgname, tgtype
+FROM pg_trigger t JOIN pg_constraint c ON (t.tgconstraint = c.oid)
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass)
+ORDER BY tgrelid, tgtype;
+
+-- Changing it back to ENFORCED will recreate the necessary triggers.
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
+
+-- Should be exactly the same number of triggers found as before
+SELECT conname, tgrelid::regclass as tgrel, regexp_replace(tgname, '[0-9]+', 'N') as tgname, tgtype
+FROM pg_trigger t JOIN pg_constraint c ON (t.tgconstraint = c.oid)
+WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass)
+ UNION ALL SELECT 'fk_notpartitioned_pk'::regclass)
+ORDER BY tgrelid, tgtype;
+
ALTER TABLE fk_partitioned_fk DROP CONSTRAINT fk_partitioned_fk_a_b_fkey;
-- done.
DROP TABLE fk_notpartitioned_pk, fk_partitioned_fk;
@@ -1441,6 +1527,25 @@ ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN
\d fk_partitioned_fk_2
DROP TABLE fk_partitioned_fk_2;
+CREATE TABLE fk_partitioned_fk_2 (b int, a int,
+ CONSTRAINT fk_part_con FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
+-- fail -- cannot merge constraints with different enforceability.
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+-- If the constraint is modified to match the enforceability of the parent, it will work.
+BEGIN;
+-- change child constraint
+ALTER TABLE fk_partitioned_fk_2 ALTER CONSTRAINT fk_part_con ENFORCED;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+\d fk_partitioned_fk_2
+ROLLBACK;
+BEGIN;
+-- or change parent constraint
+ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
+\d fk_partitioned_fk_2
+ROLLBACK;
+DROP TABLE fk_partitioned_fk_2;
+
CREATE TABLE fk_partitioned_fk_4 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE) PARTITION BY RANGE (b, a);
CREATE TABLE fk_partitioned_fk_4_1 PARTITION OF fk_partitioned_fk_4 FOR VALUES FROM (1,1) TO (100,100);
CREATE TABLE fk_partitioned_fk_4_2 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE SET NULL);
diff --git a/src/test/regress/sql/inherit.sql b/src/test/regress/sql/inherit.sql
index 941189761fd..5f0b2617464 100644
--- a/src/test/regress/sql/inherit.sql
+++ b/src/test/regress/sql/inherit.sql
@@ -481,6 +481,13 @@ alter table p1 add constraint inh_check_constraint5 check (f1 < 10) not enforced
alter table p1 add constraint inh_check_constraint6 check (f1 < 10) not enforced;
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 invalid state of the child constraint will be ignored here.
+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;
+
create table p1_c2(f1 int constraint inh_check_constraint4 check (f1 < 10)) inherits(p1);
-- but reverse is not allowed
--
2.43.5
v21-0006-Merge-the-parent-and-child-constraints-with-diff.patchapplication/octet-stream; name=v21-0006-Merge-the-parent-and-child-constraints-with-diff.patchDownload
From 0998de95720cb276ecdef140845e2423b5b0bf5a Mon Sep 17 00:00:00 2001
From: Amul Sul <amul.sul@enterprisedb.com>
Date: Mon, 10 Feb 2025 10:59:28 +0530
Subject: [PATCH v21 6/6] Merge the parent and child constraints with differing
enforcibility.
If an ENFORCED parent constraint is attached to a NOT ENFORCED child
constraint, the child constraint will be made ENFORCED, with
validation applied if the parent constraint is validated as well.
Otherwise, a new ENFORCED constraint (with validation, if the parent
constraint is validated) would need to be created on the child table,
which would be unnecessary if a similar constraint already exists and
can be attached.
On the other hand, having a NOT ENFORCED parent constraint with an
ENFORCED child constraint does not cause any issues, and no changes
are required.
----
NOTE: This patch is intended to reduce the diff noise from the main
patch and is not meant to be committed separately. It should be
squashed with the main patch that adds ENFORCED/NOT ENFORCED.
----
---
src/backend/commands/tablecmds.c | 180 +++++++++++++++++++---
src/test/regress/expected/foreign_key.out | 108 +++++++------
src/test/regress/sql/foreign_key.sql | 54 ++++---
3 files changed, 248 insertions(+), 94 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 6089765249a..17d632daf54 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -11589,23 +11589,6 @@ tryAttachPartitionForeignKey(List **wqueue,
if (!HeapTupleIsValid(partcontup))
elog(ERROR, "cache lookup failed for constraint %u", fk->conoid);
partConstr = (Form_pg_constraint) GETSTRUCT(partcontup);
-
- /*
- * An error should be raised if the constraint enforceability is different.
- * Returning false without raising an error, as we do for other attributes,
- * could lead to a duplicate constraint with the same enforceability as the
- * parent. While this may be acceptable, it may not be ideal. Therefore,
- * it's better to raise an error and allow the user to correct the
- * enforceability before proceeding.
- */
- if (partConstr->conenforced != parentConstr->conenforced)
- ereport(ERROR,
- (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
- errmsg("constraint \"%s\" enforceability conflicts with constraint \"%s\" on relation \"%s\"",
- NameStr(parentConstr->conname),
- NameStr(partConstr->conname),
- RelationGetRelationName(partition))));
-
if (OidIsValid(partConstr->conparentid) ||
partConstr->condeferrable != parentConstr->condeferrable ||
partConstr->condeferred != parentConstr->condeferred ||
@@ -11653,6 +11636,8 @@ AttachPartitionForeignKey(List **wqueue,
Oid partConstrFrelid;
Oid partConstrRelid;
bool parentConstrIsEnforced;
+ bool partConstrIsEnforced;
+ bool partConstrParentIsSet;
/* Fetch the parent constraint tuple */
parentConstrTup = SearchSysCache1(CONSTROID,
@@ -11668,13 +11653,47 @@ AttachPartitionForeignKey(List **wqueue,
if (!HeapTupleIsValid(partcontup))
elog(ERROR, "cache lookup failed for constraint %u", partConstrOid);
partConstr = (Form_pg_constraint) GETSTRUCT(partcontup);
+ partConstrIsEnforced = partConstr->conenforced;
partConstrFrelid = partConstr->confrelid;
partConstrRelid = partConstr->conrelid;
+ /*
+ * The case where the parent constraint is NOT ENFORCED and the child
+ * constraint is ENFORCED is acceptable because the not enforced parent
+ * constraint lacks triggers, eliminating any redundancy issues with the
+ * enforced child constraint. In this scenario, the child constraint
+ * remains enforced, and its trigger is retained, ensuring that
+ * referential integrity checks for the child continue as before, even
+ * with the parent constraint not enforced. The relationship between the
+ * two constraints is preserved by setting the parent constraint, which
+ * allows us to locate the child constraint. This becomes important if the
+ * parent constraint is later changed to enforced, at which point the
+ * necessary trigger will be created for the parent, and any redundancy
+ * from these triggers will be appropriately handled.
+ */
+ if (!parentConstrIsEnforced && partConstrIsEnforced)
+ {
+ ReleaseSysCache(partcontup);
+ ReleaseSysCache(parentConstrTup);
+
+ ConstraintSetParentConstraint(partConstrOid, parentConstrOid,
+ RelationGetRelid(partition));
+ CommandCounterIncrement();
+
+ return;
+ }
+
/*
* If the referenced table is partitioned, then the partition we're
* attaching now has extra pg_constraint rows and action triggers that are
* no longer needed. Remove those.
+ *
+ * Note that this must be done beforehand, particularly in situations
+ * where we might decide to change the constraint to an ENFORCED state
+ * which will create the required triggers and add the child constraint to
+ * the validation queue. To avoid generating unnecessary triggers and
+ * adding them to the validation queue, it is crucial to eliminate any
+ * redundant constraints beforehand.
*/
if (get_rel_relkind(partConstrFrelid) == RELKIND_PARTITIONED_TABLE)
{
@@ -11693,6 +11712,53 @@ AttachPartitionForeignKey(List **wqueue,
*/
queueValidation = parentConstr->convalidated && !partConstr->convalidated;
+ /*
+ * The case where the parent constraint is ENFORCED and the child
+ * constraint is NOT ENFORCED is not acceptable, as it would violate
+ * referential integrity. In such cases, the child constraint will first
+ * be enforced before merging it with the enforced parent constraint.
+ * Subsequently, removing action triggers, setting up constraint triggers,
+ * and handling check triggers for the parent will be managed in the usual
+ * manner, similar to how two enforced constraints are merged.
+ */
+ if (parentConstrIsEnforced && !partConstrIsEnforced)
+ {
+ ATAlterConstraint *cmdcon = makeNode(ATAlterConstraint);
+ Relation conrel;
+
+ cmdcon->conname = NameStr(partConstr->conname);
+ cmdcon->deferrable = partConstr->condeferrable;
+ cmdcon->initdeferred = partConstr->condeferred;
+ cmdcon->alterEnforceability = true;
+ cmdcon->is_enforced = true;
+
+ conrel = table_open(ConstraintRelationId, RowExclusiveLock);
+
+ ATExecAlterConstrEnforceability(wqueue, cmdcon, conrel, trigrel,
+ partConstr->conrelid,
+ partConstr->confrelid,
+ partcontup, AccessExclusiveLock,
+ InvalidOid, InvalidOid, InvalidOid,
+ InvalidOid);
+
+ table_close(conrel, RowExclusiveLock);
+
+ CommandCounterIncrement();
+
+ /*
+ * No further validation is needed, as changing the constraint to
+ * enforced will implicitly trigger the same validation.
+ */
+ queueValidation = false;
+ }
+
+ /*
+ * The constraint parent shouldn't be set beforehand, or if it's already
+ * set, it should be the specified parent.
+ */
+ partConstrParentIsSet = OidIsValid(partConstr->conparentid);
+ Assert(!partConstrParentIsSet || partConstr->conparentid == parentConstrOid);
+
ReleaseSysCache(partcontup);
ReleaseSysCache(parentConstrTup);
@@ -11705,8 +11771,10 @@ AttachPartitionForeignKey(List **wqueue,
DropForeignKeyConstraintTriggers(trigrel, partConstrOid, partConstrFrelid,
partConstrRelid);
- ConstraintSetParentConstraint(partConstrOid, parentConstrOid,
- RelationGetRelid(partition));
+ /* Skip if the parent is already set */
+ if (!partConstrParentIsSet)
+ ConstraintSetParentConstraint(partConstrOid, parentConstrOid,
+ RelationGetRelid(partition));
/*
* Like the constraint, attach partition's "check" triggers to the
@@ -12306,6 +12374,17 @@ ATExecAlterConstrEnforceability(List **wqueue, ATAlterConstraint *cmdcon,
/* Drop all the triggers */
DropForeignKeyConstraintTriggers(tgrel, conoid, InvalidOid, InvalidOid);
+
+ /*
+ * If the referenced table is partitioned, the child constraint we're
+ * changing to NOT ENFORCED may have additional pg_constraint rows and
+ * action triggers that remain untouched while this child constraint
+ * is attached to the NOT ENFORCED parent. These must now be removed.
+ * For more details, see AttachPartitionForeignKey().
+ */
+ if (OidIsValid(currcon->conparentid) &&
+ get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE)
+ RemoveInheritedConstraint(conrel, tgrel, currcon->conrelid, conoid);
}
else if (changed) /* Create triggers */
{
@@ -12631,13 +12710,40 @@ AlterConstrEnforceabilityRecurse(List **wqueue, ATAlterConstraint *cmdcon,
true, NULL, 1, &pkey);
while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
- ATExecAlterConstrEnforceability(wqueue, cmdcon, conrel, tgrel, fkrelid,
- pkrelid, childtup, lockmode,
- ReferencedParentDelTrigger,
- ReferencedParentUpdTrigger,
- ReferencingParentInsTrigger,
- ReferencingParentUpdTrigger);
+ {
+ Form_pg_constraint childcon;
+ childcon = (Form_pg_constraint) GETSTRUCT(childtup);
+
+ /*
+ * When the parent constraint is modified to be ENFORCED, and the
+ * child constraint is attached to the parent constraint (which is
+ * already ENFORCED), some constraints and action triggers on the
+ * child table may become redundant and need to be removed.
+ */
+ if (cmdcon->is_enforced && childcon->conenforced)
+ {
+ if (currcon->confrelid == pkrelid)
+ {
+ Relation rel = table_open(childcon->conrelid, lockmode);
+
+ AttachPartitionForeignKey(wqueue, rel, childcon->oid,
+ conoid,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger,
+ tgrel);
+
+ table_close(rel, NoLock);
+ }
+ }
+ else
+ ATExecAlterConstrEnforceability(wqueue, cmdcon, conrel, tgrel, fkrelid,
+ pkrelid, childtup, lockmode,
+ ReferencedParentDelTrigger,
+ ReferencedParentUpdTrigger,
+ ReferencingParentInsTrigger,
+ ReferencingParentUpdTrigger);
+ }
systable_endscan(pscan);
}
@@ -20811,7 +20917,9 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
{
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
+ HeapTuple parentContup;
Form_pg_constraint conform;
+ Oid parentConstrIsEnforced;
contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
if (!HeapTupleIsValid(contup))
@@ -20830,12 +20938,34 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
continue;
}
+ /* Get the enforcibility of the parent constraint */
+ parentContup = SearchSysCache1(CONSTROID,
+ ObjectIdGetDatum(conform->conparentid));
+ if (!HeapTupleIsValid(parentContup))
+ elog(ERROR, "cache lookup failed for constraint %u",
+ conform->conparentid);
+ parentConstrIsEnforced =
+ ((Form_pg_constraint) GETSTRUCT(parentContup))->conenforced;
+ ReleaseSysCache(parentContup);
+
/*
* The constraint on this table must be marked no longer a child of
* the parent's constraint, as do its check triggers.
*/
ConstraintSetParentConstraint(fk->conoid, InvalidOid, InvalidOid);
+ /*
+ * Unsetting the parent is sufficient when the parent constraint is
+ * NOT ENFORCED and the child constraint is ENFORCED, as we link them
+ * by setting the constraint parent, while leaving the rest unchanged.
+ * For more details, see AttachPartitionForeignKey().
+ */
+ if (!parentConstrIsEnforced && fk->conenforced)
+ {
+ ReleaseSysCache(contup);
+ continue;
+ }
+
/*
* Also, look up the partition's "check" triggers corresponding to the
* ENFORCED constraint being detached and detach them from the parent
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index db1e39d5fce..404c9c66bcd 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -1670,8 +1670,6 @@ ALTER TABLE fk_partitioned_fk ADD CONSTRAINT fk_partitioned_fk_a_b_fkey FOREIGN
REFERENCES fk_notpartitioned_pk NOT ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
-ALTER TABLE fk_partitioned_fk_2 ADD CONSTRAINT fk_partitioned_fk_a_b_fkey FOREIGN KEY (a, b)
- REFERENCES fk_notpartitioned_pk NOT ENFORCED;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
CREATE TABLE fk_partitioned_fk_3 (fdrop1 int, fdrop2 int, fdrop3 int, fdrop4 int, b int, a int)
@@ -1680,8 +1678,65 @@ ALTER TABLE fk_partitioned_fk_3 DROP COLUMN fdrop1, DROP COLUMN fdrop2,
DROP COLUMN fdrop3, DROP COLUMN fdrop4;
CREATE TABLE fk_partitioned_fk_3_0 PARTITION OF fk_partitioned_fk_3 FOR VALUES WITH (MODULUS 5, REMAINDER 0);
CREATE TABLE fk_partitioned_fk_3_1 PARTITION OF fk_partitioned_fk_3 FOR VALUES WITH (MODULUS 5, REMAINDER 1);
+-- Merge the not-enforced parent constraint with the enforced and not-enforced child constraints
+ALTER TABLE fk_partitioned_fk_3_0 ADD CONSTRAINT fk_partitioned_fk_3_0_a_b_fkey FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ENFORCED;
+ALTER TABLE fk_partitioned_fk_3_1 ADD CONSTRAINT fk_partitioned_fk_3_1_a_b_fkey FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk_3 ADD CONSTRAINT fk_partitioned_fk_3_a_b_fkey FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+-- Merge the non-enforced parent constraint with both the enforced and
+-- non-enforced child constraints, where the referenced table is partitioned.
+CREATE TABLE fk_partitioned_pk (a int, b int, PRIMARY KEY (a, b)) PARTITION BY RANGE (a, b);
+CREATE TABLE fk_partitioned_pk_1 PARTITION OF fk_partitioned_pk FOR VALUES FROM (0,0) TO (1000,1000);
+-- Merge the enforced parent constraint with the enforced and not-enforced child constraints.
+ALTER TABLE fk_partitioned_fk_3_0 ADD CONSTRAINT fk_partitioned_fk_3_0_a_b_fkey1 FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk ENFORCED;
+ALTER TABLE fk_partitioned_fk_3_1 ADD CONSTRAINT fk_partitioned_fk_3_1_a_b_fkey1 FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk_3 ADD CONSTRAINT fk_partitioned_fk_3_a_b_fkey1 FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk ENFORCED;
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+ conname | conenforced | convalidated | conrelid | confrelid
+---------------------------------+-------------+--------------+-----------------------+----------------------
+ fk_partitioned_fk_3_0_a_b_fkey | t | t | fk_partitioned_fk_3_0 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey | f | f | fk_partitioned_fk_3_1 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_a_b_fkey | f | f | fk_partitioned_fk_3 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_0_a_b_fkey1 | t | t | fk_partitioned_fk_3_0 | fk_partitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey1 | t | t | fk_partitioned_fk_3_1 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey1 | t | t | fk_partitioned_fk_3 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey2 | t | t | fk_partitioned_fk_3 | fk_partitioned_pk_1
+(7 rows)
+
+ALTER TABLE fk_partitioned_fk_3 DETACH PARTITION fk_partitioned_fk_3_0;
+ALTER TABLE fk_partitioned_fk_3 ATTACH PARTITION fk_partitioned_fk_3_0 FOR VALUES WITH (MODULUS 5, REMAINDER 0);
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+ conname | conenforced | convalidated | conrelid | confrelid
+---------------------------------+-------------+--------------+-----------------------+----------------------
+ fk_partitioned_fk_3_0_a_b_fkey | t | t | fk_partitioned_fk_3_0 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey | f | f | fk_partitioned_fk_3_1 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_a_b_fkey | f | f | fk_partitioned_fk_3 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_0_a_b_fkey1 | t | t | fk_partitioned_fk_3_0 | fk_partitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey1 | t | t | fk_partitioned_fk_3_1 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey1 | t | t | fk_partitioned_fk_3 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey2 | t | t | fk_partitioned_fk_3 | fk_partitioned_pk_1
+(7 rows)
+
+ALTER TABLE fk_partitioned_fk_3 ALTER CONSTRAINT fk_partitioned_fk_3_a_b_fkey1 NOT ENFORCED;
+-- Merging an enforced parent constraint (validated) with a not-enforced child
+-- constraint will implicitly change the child constraint to enforced and apply
+-- the validation as well.
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_3
FOR VALUES FROM (2000,2000) TO (3000,3000);
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+ conname | conenforced | convalidated | conrelid | confrelid
+---------------------------------+-------------+--------------+-----------------------+----------------------
+ fk_partitioned_fk_3_0_a_b_fkey | t | t | fk_partitioned_fk_3_0 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey | t | t | fk_partitioned_fk_3_1 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_a_b_fkey | t | t | fk_partitioned_fk_3 | fk_notpartitioned_pk
+ fk_partitioned_fk_3_0_a_b_fkey1 | f | f | fk_partitioned_fk_3_0 | fk_partitioned_pk
+ fk_partitioned_fk_3_1_a_b_fkey1 | f | f | fk_partitioned_fk_3_1 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey1 | f | f | fk_partitioned_fk_3 | fk_partitioned_pk
+ fk_partitioned_fk_3_a_b_fkey2 | f | f | fk_partitioned_fk_3 | fk_partitioned_pk_1
+(7 rows)
+
-- Creating a foreign key with ONLY on a partitioned table referencing
-- a non-partitioned table fails.
ALTER TABLE ONLY fk_partitioned_fk ADD FOREIGN KEY (a, b)
@@ -1702,16 +1757,16 @@ INSERT INTO fk_partitioned_fk_2 (a,b) VALUES (1500, 1501);
ERROR: insert or update on table "fk_partitioned_fk_2" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
DETAIL: Key (a, b)=(1500, 1501) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (2500, 2502);
-ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_3_1_a_b_fkey"
DETAIL: Key (a, b)=(2500, 2502) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk_3 (a,b) VALUES (2500, 2502);
-ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_3_1_a_b_fkey"
DETAIL: Key (a, b)=(2500, 2502) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk (a,b) VALUES (2501, 2503);
-ERROR: insert or update on table "fk_partitioned_fk_3_0" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_0" violates foreign key constraint "fk_partitioned_fk_3_0_a_b_fkey"
DETAIL: Key (a, b)=(2501, 2503) is not present in table "fk_notpartitioned_pk".
INSERT INTO fk_partitioned_fk_3 (a,b) VALUES (2501, 2503);
-ERROR: insert or update on table "fk_partitioned_fk_3_0" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_0" violates foreign key constraint "fk_partitioned_fk_3_0_a_b_fkey"
DETAIL: Key (a, b)=(2501, 2503) is not present in table "fk_notpartitioned_pk".
-- but if we insert the values that make them valid, then they work
INSERT INTO fk_notpartitioned_pk VALUES (500, 501), (1500, 1501),
@@ -1722,7 +1777,7 @@ INSERT INTO fk_partitioned_fk (a,b) VALUES (2500, 2502);
INSERT INTO fk_partitioned_fk (a,b) VALUES (2501, 2503);
-- this update fails because there is no referenced row
UPDATE fk_partitioned_fk SET a = a + 1 WHERE a = 2501;
-ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_a_b_fkey"
+ERROR: insert or update on table "fk_partitioned_fk_3_1" violates foreign key constraint "fk_partitioned_fk_3_1_a_b_fkey"
DETAIL: Key (a, b)=(2502, 2503) is not present in table "fk_notpartitioned_pk".
-- but we can fix it thusly:
INSERT INTO fk_notpartitioned_pk (a,b) VALUES (2502, 2503);
@@ -1895,8 +1950,6 @@ WHERE conrelid::regclass::text like 'fk_partitioned_fk%' ORDER BY oid::regclass:
DROP TABLE fk_partitioned_fk, fk_notpartitioned_pk;
-- NOT VALID foreign key on a non-partitioned table referencing a partitioned table
-CREATE TABLE fk_partitioned_pk (a int, b int, PRIMARY KEY (a, b)) PARTITION BY RANGE (a, b);
-CREATE TABLE fk_partitioned_pk_1 PARTITION OF fk_partitioned_pk FOR VALUES FROM (0,0) TO (1000,1000);
CREATE TABLE fk_notpartitioned_fk (b int, a int);
ALTER TABLE fk_notpartitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk NOT VALID;
-- Constraint will be invalid.
@@ -2107,43 +2160,6 @@ Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
Foreign-key constraints:
TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
-DROP TABLE fk_partitioned_fk_2;
-CREATE TABLE fk_partitioned_fk_2 (b int, a int,
- CONSTRAINT fk_part_con FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
--- fail -- cannot merge constraints with different enforceability.
-ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
-ERROR: constraint "fk_partitioned_fk_a_b_fkey" enforceability conflicts with constraint "fk_part_con" on relation "fk_partitioned_fk_2"
--- If the constraint is modified to match the enforceability of the parent, it will work.
-BEGIN;
--- change child constraint
-ALTER TABLE fk_partitioned_fk_2 ALTER CONSTRAINT fk_part_con ENFORCED;
-ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
-\d fk_partitioned_fk_2
- Table "public.fk_partitioned_fk_2"
- Column | Type | Collation | Nullable | Default
---------+---------+-----------+----------+---------
- b | integer | | |
- a | integer | | |
-Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
-Foreign-key constraints:
- TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE
-
-ROLLBACK;
-BEGIN;
--- or change parent constraint
-ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
-ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
-\d fk_partitioned_fk_2
- Table "public.fk_partitioned_fk_2"
- Column | Type | Collation | Nullable | Default
---------+---------+-----------+----------+---------
- b | integer | | |
- a | integer | | |
-Partition of: fk_partitioned_fk FOR VALUES IN (1500, 1502)
-Foreign-key constraints:
- TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED
-
-ROLLBACK;
DROP TABLE fk_partitioned_fk_2;
CREATE TABLE fk_partitioned_fk_4 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE) PARTITION BY RANGE (b, a);
CREATE TABLE fk_partitioned_fk_4_1 PARTITION OF fk_partitioned_fk_4 FOR VALUES FROM (1,1) TO (100,100);
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index 05dd443106c..91b25799a83 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -1245,8 +1245,6 @@ ALTER TABLE fk_partitioned_fk ADD CONSTRAINT fk_partitioned_fk_a_b_fkey FOREIGN
REFERENCES fk_notpartitioned_pk NOT ENFORCED;
CREATE TABLE fk_partitioned_fk_2 (b int, fdrop1 int, fdrop2 int, a int);
ALTER TABLE fk_partitioned_fk_2 DROP COLUMN fdrop1, DROP COLUMN fdrop2;
-ALTER TABLE fk_partitioned_fk_2 ADD CONSTRAINT fk_partitioned_fk_a_b_fkey FOREIGN KEY (a, b)
- REFERENCES fk_notpartitioned_pk NOT ENFORCED;
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES FROM (1000,1000) TO (2000,2000);
ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey ENFORCED;
CREATE TABLE fk_partitioned_fk_3 (fdrop1 int, fdrop2 int, fdrop3 int, fdrop4 int, b int, a int)
@@ -1255,9 +1253,40 @@ ALTER TABLE fk_partitioned_fk_3 DROP COLUMN fdrop1, DROP COLUMN fdrop2,
DROP COLUMN fdrop3, DROP COLUMN fdrop4;
CREATE TABLE fk_partitioned_fk_3_0 PARTITION OF fk_partitioned_fk_3 FOR VALUES WITH (MODULUS 5, REMAINDER 0);
CREATE TABLE fk_partitioned_fk_3_1 PARTITION OF fk_partitioned_fk_3 FOR VALUES WITH (MODULUS 5, REMAINDER 1);
+-- Merge the not-enforced parent constraint with the enforced and not-enforced child constraints
+ALTER TABLE fk_partitioned_fk_3_0 ADD CONSTRAINT fk_partitioned_fk_3_0_a_b_fkey FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ENFORCED;
+ALTER TABLE fk_partitioned_fk_3_1 ADD CONSTRAINT fk_partitioned_fk_3_1_a_b_fkey FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk_3 ADD CONSTRAINT fk_partitioned_fk_3_a_b_fkey FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk NOT ENFORCED;
+
+-- Merge the non-enforced parent constraint with both the enforced and
+-- non-enforced child constraints, where the referenced table is partitioned.
+CREATE TABLE fk_partitioned_pk (a int, b int, PRIMARY KEY (a, b)) PARTITION BY RANGE (a, b);
+CREATE TABLE fk_partitioned_pk_1 PARTITION OF fk_partitioned_pk FOR VALUES FROM (0,0) TO (1000,1000);
+-- Merge the enforced parent constraint with the enforced and not-enforced child constraints.
+ALTER TABLE fk_partitioned_fk_3_0 ADD CONSTRAINT fk_partitioned_fk_3_0_a_b_fkey1 FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk ENFORCED;
+ALTER TABLE fk_partitioned_fk_3_1 ADD CONSTRAINT fk_partitioned_fk_3_1_a_b_fkey1 FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk NOT ENFORCED;
+ALTER TABLE fk_partitioned_fk_3 ADD CONSTRAINT fk_partitioned_fk_3_a_b_fkey1 FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk ENFORCED;
+
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+
+ALTER TABLE fk_partitioned_fk_3 DETACH PARTITION fk_partitioned_fk_3_0;
+ALTER TABLE fk_partitioned_fk_3 ATTACH PARTITION fk_partitioned_fk_3_0 FOR VALUES WITH (MODULUS 5, REMAINDER 0);
+
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+
+ALTER TABLE fk_partitioned_fk_3 ALTER CONSTRAINT fk_partitioned_fk_3_a_b_fkey1 NOT ENFORCED;
+
+-- Merging an enforced parent constraint (validated) with a not-enforced child
+-- constraint will implicitly change the child constraint to enforced and apply
+-- the validation as well.
ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_3
FOR VALUES FROM (2000,2000) TO (3000,3000);
+SELECT conname, conenforced, convalidated, conrelid::regclass, confrelid::regclass
+FROM pg_constraint WHERE conrelid::regclass::text like 'fk_partitioned_fk_3%' ORDER BY oid;
+
-- Creating a foreign key with ONLY on a partitioned table referencing
-- a non-partitioned table fails.
ALTER TABLE ONLY fk_partitioned_fk ADD FOREIGN KEY (a, b)
@@ -1389,8 +1418,6 @@ WHERE conrelid::regclass::text like 'fk_partitioned_fk%' ORDER BY oid::regclass:
DROP TABLE fk_partitioned_fk, fk_notpartitioned_pk;
-- NOT VALID foreign key on a non-partitioned table referencing a partitioned table
-CREATE TABLE fk_partitioned_pk (a int, b int, PRIMARY KEY (a, b)) PARTITION BY RANGE (a, b);
-CREATE TABLE fk_partitioned_pk_1 PARTITION OF fk_partitioned_pk FOR VALUES FROM (0,0) TO (1000,1000);
CREATE TABLE fk_notpartitioned_fk (b int, a int);
ALTER TABLE fk_notpartitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk NOT VALID;
@@ -1527,25 +1554,6 @@ ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN
\d fk_partitioned_fk_2
DROP TABLE fk_partitioned_fk_2;
-CREATE TABLE fk_partitioned_fk_2 (b int, a int,
- CONSTRAINT fk_part_con FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk ON UPDATE CASCADE ON DELETE CASCADE NOT ENFORCED);
--- fail -- cannot merge constraints with different enforceability.
-ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
--- If the constraint is modified to match the enforceability of the parent, it will work.
-BEGIN;
--- change child constraint
-ALTER TABLE fk_partitioned_fk_2 ALTER CONSTRAINT fk_part_con ENFORCED;
-ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
-\d fk_partitioned_fk_2
-ROLLBACK;
-BEGIN;
--- or change parent constraint
-ALTER TABLE fk_partitioned_fk ALTER CONSTRAINT fk_partitioned_fk_a_b_fkey NOT ENFORCED;
-ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1500,1502);
-\d fk_partitioned_fk_2
-ROLLBACK;
-DROP TABLE fk_partitioned_fk_2;
-
CREATE TABLE fk_partitioned_fk_4 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE CASCADE ON DELETE CASCADE) PARTITION BY RANGE (b, a);
CREATE TABLE fk_partitioned_fk_4_1 PARTITION OF fk_partitioned_fk_4 FOR VALUES FROM (1,1) TO (100,100);
CREATE TABLE fk_partitioned_fk_4_2 (a int, b int, FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) ON UPDATE SET NULL);
--
2.43.5
On 28.03.25 14:27, Amul Sul wrote:
On Fri, Mar 28, 2025 at 3:34 PM Ashutosh Bapat
<ashutosh.bapat.oss@gmail.com> wrote:On Thu, Mar 27, 2025 at 6:25 PM Amul Sul <sulamul@gmail.com> wrote:
I am not sure how to make such tests stable enough since the trigger
name involves OIDs. In count check, I tried adjusting the join
condition to ensure that I get the exact same type of constraint
w.r.t. trigger relation and the constraint.There are tests which mask variable parts of EXPLAIN output. Can we
use similar trick to mask OIDs from the trigger names?Okay, tried it in the attached version. Please check if it looks good.
I have committed version 21 of the patches (without 0006).
The patch you posted failed the regression test foreign_key because in
the output of the queries that list the triggers, the conname output did
not match the expected output. I committed it so that the test output
matches the code behavior. But please double-check that that's what you
intended.
Also, something we hadn't looked at before, I think, I made
get_relation_foreign_keys() in src/backend/optimizer/util/plancat.c
ignore not-enforced foreign keys. That means, not-enforced foreign keys
will not be used for cost estimation. This is, I think, what we want,
as we discussed earlier. If we ever want an alternative mode where
not-enforced constraints are considered for cost-estimation, then we
could quite easily tweak this.
On Wed, Apr 2, 2025 at 6:02 PM Peter Eisentraut <peter@eisentraut.org> wrote:
On 28.03.25 14:27, Amul Sul wrote:
On Fri, Mar 28, 2025 at 3:34 PM Ashutosh Bapat
<ashutosh.bapat.oss@gmail.com> wrote:On Thu, Mar 27, 2025 at 6:25 PM Amul Sul <sulamul@gmail.com> wrote:
I am not sure how to make such tests stable enough since the trigger
name involves OIDs. In count check, I tried adjusting the join
condition to ensure that I get the exact same type of constraint
w.r.t. trigger relation and the constraint.There are tests which mask variable parts of EXPLAIN output. Can we
use similar trick to mask OIDs from the trigger names?Okay, tried it in the attached version. Please check if it looks good.
I have committed version 21 of the patches (without 0006).
The patch you posted failed the regression test foreign_key because in
the output of the queries that list the triggers, the conname output did
not match the expected output. I committed it so that the test output
matches the code behavior. But please double-check that that's what you
intended.
Interestingly, it's not failing for me, and what's even stranger is
that the version with the committed test works fine on my system as
well. :)
Also, something we hadn't looked at before, I think, I made
get_relation_foreign_keys() in src/backend/optimizer/util/plancat.c
ignore not-enforced foreign keys. That means, not-enforced foreign keys
will not be used for cost estimation. This is, I think, what we want,
as we discussed earlier. If we ever want an alternative mode where
not-enforced constraints are considered for cost-estimation, then we
could quite easily tweak this.
Yeah, it makes sense.
Thanks so much for the review, committing the patch, and all your guidance.
Regards,
Amul