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, &copyTuple->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

