problems with foreign keys on partitioned tables

Started by Amit Langoteabout 7 years ago17 messages
#1Amit Langote
Langote_Amit_f8@lab.ntt.co.jp
4 attachment(s)

Hi,

I noticed a couple of problems with foreign keys on partitioned tables.

1. Foreign keys of partitions stop working correctly after being detached
from the parent table

create table pk (a int primary key);
create table p (a int) partition by list (a);
create table p1 partition of p for values in (1) partition by list (a);
create table p11 partition of p1 for values in (1);
alter table p add foreign key (a) references pk (a);

-- these things work correctly
insert into p values (1);
ERROR: insert or update on table "p11" violates foreign key constraint
"p_a_fkey"
DETAIL: Key (a)=(1) is not present in table "pk".
insert into pk values (1);
insert into p values (1);
delete from pk where a = 1;
ERROR: update or delete on table "pk" violates foreign key constraint
"p_a_fkey" on table "p"
DETAIL: Key (a)=(1) is still referenced from table "p".

-- detach p1, which preserves the foreign key key
alter table p detach partition p1;
create table p12 partition of p1 for values in (2);

-- this part of the foreign key on p1 still works
insert into p1 values (2);
ERROR: insert or update on table "p12" violates foreign key constraint
"p_a_fkey"
DETAIL: Key (a)=(2) is not present in table "pk".

-- but this seems wrong
delete from pk where a = 1;
DELETE 1

-- because
select * from p1;
a
───
1
(1 row)

This happens because the action triggers defined on the PK relation (pk)
refers to p as the referencing relation. On detaching p1 from p, p1's
data is no longer accessible to that trigger. To fix this problem, we
need create action triggers on PK relation that refer to p1 when it's
detached (unless such triggers already exist which might be true in some
cases). Attached patch 0001 shows this approach.

2. Foreign keys of a partition cannot be dropped in some cases after
detaching it from the parent.

create table p (a int references pk) partition by list (a);
create table p1 partition of p for values in (1) partition by list (a);
create table p11 partition of p1 for values in (1);
alter table p detach partition p1;

-- p1's foreign key is no longer inherited, so should be able to drop it
alter table p1 drop constraint p_a_fkey ;
ERROR: constraint "p_a_fkey" of relation "p11" does not exist

This happens because by the time ATExecDropConstraint tries to recursively
drop the p11's inherited foreign key constraint (which is what normally
happens for inherited constraints), the latter has already been dropped by
dependency management. I think the foreign key inheritance related code
doesn't need to add dependencies for something that inheritance recursion
can take of and I can't think of any other reason to have such
dependencies around. I thought maybe they're needed for pg_dump to work
correctly, but apparently not so.

Interestingly, the above problem doesn't occur if the constraint is added
to partitions by inheritance recursion.

create table p (a int) partition by list (a);
create table p1 partition of p for values in (1) partition by list (a);
create table p11 partition of p1 for values in (1);
alter table p add foreign key (a) references pk (a);
alter table p detach partition p1;
alter table p1 drop constraint p_a_fkey ;
ALTER TABLE

Looking into it, that happens to work *accidentally*.

ATExecDropInherit() doesn't try to recurse, which prevents the error in
this case, because it finds that the constraint on p1 is marked NO INHERIT
(non-inheritable), which is incorrect. The value of p1's constraint's
connoinherit (in fact, other inheritance related properties too) is
incorrect, because ATAddForeignKeyConstraint doesn't bother to set them
correctly. This is what the inheritance properties of various copies of
'p_a_fkey' looks like in the catalog in this case:

-- case 1: foreign key added to partitions recursively
create table p (a int) partition by list (a);
create table p1 partition of p for values in (1) partition by list (a);
create table p11 partition of p1 for values in (1);
alter table p add foreign key (a) references pk (a);
select conname, conrelid::regclass, conislocal, coninhcount, connoinherit
from pg_constraint where conname like 'p%fkey%';
conname │ conrelid │ conislocal │ coninhcount │ connoinherit
──────────┼──────────┼────────────┼─────────────┼──────────────
p_a_fkey │ p │ t │ 0 │ t
p_a_fkey │ p1 │ t │ 0 │ t
p_a_fkey │ p11 │ t │ 0 │ t
(3 rows)

In this case, after detaching p1 from p, p1's foreign key's coninhcount
turns to -1, which is not good.

alter table p detach partition p1;
select conname, conrelid::regclass, conislocal, coninhcount, connoinherit
from pg_constraint where conname like 'p%fkey%';
conname │ conrelid │ conislocal │ coninhcount │ connoinherit
──────────┼──────────┼────────────┼─────────────┼──────────────
p_a_fkey │ p │ t │ 0 │ t
p_a_fkey │ p11 │ t │ 0 │ t
p_a_fkey │ p1 │ t │ -1 │ t
(3 rows)

-- case 2: foreign keys cloned to partitions after adding partitions
create table p (a int references pk) partition by list (a);
create table p1 partition of p for values in (1) partition by list (a);
create table p11 partition of p1 for values in (1);
select conname, conrelid::regclass, conislocal, coninhcount, connoinherit
from pg_constraint where conname like 'p%fkey%';
conname │ conrelid │ conislocal │ coninhcount │ connoinherit
──────────┼──────────┼────────────┼─────────────┼──────────────
p_a_fkey │ p │ t │ 0 │ t
p_a_fkey │ p1 │ f │ 1 │ f
p_a_fkey │ p11 │ f │ 1 │ f
(3 rows)

Anyway, I propose we fix this by first getting rid of dependencies for
foreign key constraints and instead rely on inheritance recursion for
dropping the inherited constraints. Before we can do that, we'll need to
consistently set the inheritance properties of foreign key constraints
correctly, that is, teach ATAddForeignKeyConstraint what
clone_fk_constraints already does correctly. See the attached patch 0002
for that.

I'm also attaching versions of 0001 and 0002 that can be applied to PG 11.

Thanks,
Amit

Attachments:

PG11-v1-0001-Ensure-PK-side-action-triggers-for-partitions-aft.patchtext/plain; charset=UTF-8; name=PG11-v1-0001-Ensure-PK-side-action-triggers-for-partitions-aft.patchDownload
From 86e18c2855ee9f8df19dd55dd024938ba2d41f78 Mon Sep 17 00:00:00 2001
From: amit <amitlangote09@gmail.com>
Date: Wed, 9 Jan 2019 10:00:11 +0900
Subject: [PATCH v1 1/2] Ensure PK-side action triggers for partitions after
 being detached

Detaching a partition from the parent whose foreign key(s) would've
been duplicated in the partition makes the foreign key(s) stop
working corretly.  That's because PK-side action trigger for the
foreign key would refer to the parent as the referencing relation
and after the partition is detached it's data is no longer accessible
via parent.  So while the check triggers that remain even after
being detached takes care of the one side of enforcing the foreign
key of the detached partition, lack of corresponding PK-side action
trigger to check detached partition's data means the other side
doesn't work.

To fix, add the action triggers on the PK relation that points to
the detached partition when detaching.
---
 src/backend/catalog/pg_constraint.c |  3 +-
 src/backend/commands/tablecmds.c    | 57 +++++++++++++++++++++++++++++++++++--
 src/include/commands/tablecmds.h    |  3 +-
 3 files changed, 58 insertions(+), 5 deletions(-)

diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index f4057a9f15..a379bec202 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -720,7 +720,8 @@ clone_fk_constraints(Relation pg_constraint, Relation parentRel,
 		fkconstraint->initdeferred = constrForm->condeferred;
 
 		createForeignKeyTriggers(partRel, constrForm->confrelid, fkconstraint,
-								 constrOid, constrForm->conindid, false);
+								 constrOid, constrForm->conindid, false,
+								 true);
 
 		if (cloned)
 		{
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 04b1098320..ae83deaf51 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -7718,7 +7718,7 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	 * though the constraints also exist below.
 	 */
 	createForeignKeyTriggers(rel, RelationGetRelid(pkrel), fkconstraint,
-							 constrOid, indexOid, !recursing);
+							 constrOid, indexOid, !recursing, true);
 
 	/*
 	 * Tell Phase 3 to check that the constraint is satisfied by existing
@@ -8827,7 +8827,8 @@ createForeignKeyCheckTriggers(Oid myRelOid, Oid refRelOid,
  */
 void
 createForeignKeyTriggers(Relation rel, Oid refRelOid, Constraint *fkconstraint,
-						 Oid constraintOid, Oid indexOid, bool create_action)
+						 Oid constraintOid, Oid indexOid, bool create_action,
+						 bool create_check)
 {
 	/*
 	 * For the referenced side, create action triggers, if requested.  (If the
@@ -8843,7 +8844,7 @@ createForeignKeyTriggers(Relation rel, Oid refRelOid, Constraint *fkconstraint,
 	 * For the referencing side, create the check triggers.  We only need
 	 * these on the partitions.
 	 */
-	if (rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
+	if (rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE && create_check)
 		createForeignKeyCheckTriggers(RelationGetRelid(rel), refRelOid,
 									  fkconstraint, constraintOid, indexOid);
 
@@ -14813,13 +14814,63 @@ ATExecDetachPartition(Relation rel, RangeVar *name)
 	{
 		ForeignKeyCacheInfo *fk = lfirst(cell);
 		HeapTuple	contup;
+		Form_pg_constraint conform;
+		Constraint *fkconstraint;
+		Relation	trigrel;
+		HeapTuple	trigtup;
+		HeapScanDesc scan;
+		ScanKeyData key[3];
 
 		contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
 		if (!contup)
 			elog(ERROR, "cache lookup failed for constraint %u", fk->conoid);
+		conform = (Form_pg_constraint) GETSTRUCT(contup);
 
 		ConstraintSetParentConstraint(fk->conoid, InvalidOid);
 
+		/*
+		 * We'll need to make the triggers on primary key relation to point
+		 * to this relation as the FK relation.  Currently, it points to
+		 * the parent from which this relation is being detached.
+		 */
+		fkconstraint = makeNode(Constraint);
+		fkconstraint->conname = pstrdup(NameStr(conform->conname));
+		fkconstraint->fk_upd_action = conform->confupdtype;
+		fkconstraint->fk_del_action = conform->confdeltype;
+		fkconstraint->deferrable = conform->condeferrable;
+		fkconstraint->initdeferred = conform->condeferred;
+
+		/* First check if such a trigger doesn't already exist. */
+		trigrel = heap_open(TriggerRelationId, AccessShareLock);
+		ScanKeyInit(&key[0],
+					Anum_pg_trigger_tgrelid,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(conform->confrelid));
+		ScanKeyInit(&key[1],
+					Anum_pg_trigger_tgconstrrelid,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(RelationGetRelid(partRel)));
+		ScanKeyInit(&key[2],
+					Anum_pg_trigger_tgconstraint,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(fk->conoid));
+		scan = heap_beginscan_catalog(trigrel, 3, key);
+		trigtup = heap_getnext(scan, ForwardScanDirection);
+		if (trigtup == NULL)
+		{
+			/*
+			 * Nope, we didn't find such a trigger, so create one.
+			 *
+			 * As we already got the check triggers, no need to recreate them,
+			 * so pass false for create_check.
+			 */
+			createForeignKeyTriggers(partRel, conform->confrelid, fkconstraint,
+									 fk->conoid, conform->conindid,
+									 true, false);
+		}
+		heap_endscan(scan);
+		heap_close(trigrel, AccessShareLock);
+
 		ReleaseSysCache(contup);
 	}
 	list_free_deep(fks);
diff --git a/src/include/commands/tablecmds.h b/src/include/commands/tablecmds.h
index 138de84e83..dd985a80b6 100644
--- a/src/include/commands/tablecmds.h
+++ b/src/include/commands/tablecmds.h
@@ -77,7 +77,8 @@ extern void check_of_type(HeapTuple typetuple);
 
 extern void createForeignKeyTriggers(Relation rel, Oid refRelOid,
 						 Constraint *fkconstraint, Oid constraintOid,
-						 Oid indexOid, bool create_action);
+						 Oid indexOid, bool create_action,
+						 bool create_check);
 
 extern void register_on_commit_action(Oid relid, OnCommitAction action);
 extern void remove_on_commit_action(Oid relid);
-- 
2.11.0

PG11-v1-0002-Do-not-track-foreign-key-inheritance-by-dependenc.patchtext/plain; charset=UTF-8; name=PG11-v1-0002-Do-not-track-foreign-key-inheritance-by-dependenc.patchDownload
From 669c0621d7970e04663117c43ba877ed53a804df Mon Sep 17 00:00:00 2001
From: amit <amitlangote09@gmail.com>
Date: Wed, 9 Jan 2019 18:52:16 +0900
Subject: [PATCH v1 2/2] Do not track foreign key inheritance by dependencies

Inheritance information maintained in pg_constraint is enough to
prevent a child constraint to be dropped and for them to be dropped
when the parent constraint is dropped.  So, do not create
dependencies between the parent foreign key constraint and its
children.

Also, fix ATAddForeignKeyConstraint to set inheritance information
correctly.
---
 src/backend/catalog/pg_constraint.c | 33 +++++++--------------
 src/backend/commands/indexcmds.c    | 18 ++++++++++++
 src/backend/commands/tablecmds.c    | 57 ++++++++++++++++++++++++++++---------
 3 files changed, 72 insertions(+), 36 deletions(-)

diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index a379bec202..4f0d40473d 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -482,8 +482,6 @@ clone_fk_constraints(Relation pg_constraint, Relation parentRel,
 		Constraint *fkconstraint;
 		bool		attach_it;
 		Oid			constrOid;
-		ObjectAddress parentAddr,
-					childAddr;
 		int			nelem;
 		ListCell   *cell;
 		int			i;
@@ -504,8 +502,6 @@ clone_fk_constraints(Relation pg_constraint, Relation parentRel,
 			continue;
 		}
 
-		ObjectAddressSet(parentAddr, ConstraintRelationId, parentConstrOid);
-
 		datum = fastgetattr(tuple, Anum_pg_constraint_conkey,
 							tupdesc, &isnull);
 		if (isnull)
@@ -708,9 +704,6 @@ clone_fk_constraints(Relation pg_constraint, Relation parentRel,
 								  1, false, true);
 		subclone = lappend_oid(subclone, constrOid);
 
-		ObjectAddressSet(childAddr, ConstraintRelationId, constrOid);
-		recordDependencyOn(&childAddr, &parentAddr, DEPENDENCY_INTERNAL_AUTO);
-
 		fkconstraint = makeNode(Constraint);
 		/* for now this is all we need */
 		fkconstraint->conname = pstrdup(NameStr(constrForm->conname));
@@ -1162,8 +1155,8 @@ AlterConstraintNamespaces(Oid ownerId, Oid oldNspId,
  * ConstraintSetParentConstraint
  *		Set a partition's constraint as child of its parent table's
  *
- * This updates the constraint's pg_constraint row to show it as inherited, and
- * add a dependency to the parent so that it cannot be removed on its own.
+ * This updates the constraint's pg_constraint row to change its
+ * inheritance properties.
  */
 void
 ConstraintSetParentConstraint(Oid childConstrId, Oid parentConstrId)
@@ -1172,8 +1165,6 @@ ConstraintSetParentConstraint(Oid childConstrId, Oid parentConstrId)
 	Form_pg_constraint constrForm;
 	HeapTuple	tuple,
 				newtup;
-	ObjectAddress depender;
-	ObjectAddress referenced;
 
 	constrRel = heap_open(ConstraintRelationId, RowExclusiveLock);
 	tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(childConstrId));
@@ -1185,25 +1176,23 @@ ConstraintSetParentConstraint(Oid childConstrId, Oid parentConstrId)
 	{
 		constrForm->conislocal = false;
 		constrForm->coninhcount++;
+		/*
+		 * An inherited foreign key constraint can never have more than one
+		 * parent, because inheriting foreign keys is only allowed for
+		 * partitioning where multiple inheritance is disallowed.
+		 */
+		Assert(constrForm->coninhcount == 1);
 		constrForm->conparentid = parentConstrId;
 
 		CatalogTupleUpdate(constrRel, &tuple->t_self, newtup);
-
-		ObjectAddressSet(referenced, ConstraintRelationId, parentConstrId);
-		ObjectAddressSet(depender, ConstraintRelationId, childConstrId);
-
-		recordDependencyOn(&depender, &referenced, DEPENDENCY_INTERNAL_AUTO);
 	}
 	else
 	{
 		constrForm->coninhcount--;
-		if (constrForm->coninhcount <= 0)
-			constrForm->conislocal = true;
+		/* See the above comment. */
+		Assert(constrForm->coninhcount == 0);
+		constrForm->conislocal = true;
 		constrForm->conparentid = InvalidOid;
-
-		deleteDependencyRecordsForClass(ConstraintRelationId, childConstrId,
-										ConstraintRelationId,
-										DEPENDENCY_INTERNAL_AUTO);
 		CatalogTupleUpdate(constrRel, &tuple->t_self, newtup);
 	}
 
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index 965b9f0d23..9e227796bc 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -974,9 +974,27 @@ DefineIndex(Oid relationId,
 						/* Attach index to parent and we're done. */
 						IndexSetParentIndex(cldidx, indexRelationId);
 						if (createdConstraintId != InvalidOid)
+						{
+							ObjectAddress depender;
+							ObjectAddress referenced;
+
 							ConstraintSetParentConstraint(cldConstrOid,
 														  createdConstraintId);
 
+							/*
+							 * Need to set an explicit dependency in this
+							 * case unlike other types of constraints where
+							 * the child constraint gets dropped due to
+							 * inheritance recursion.
+							 */
+							ObjectAddressSet(referenced, ConstraintRelationId,
+											 createdConstraintId);
+							ObjectAddressSet(depender, ConstraintRelationId,
+											 cldConstrOid);
+							recordDependencyOn(&depender, &referenced,
+											   DEPENDENCY_INTERNAL_AUTO);
+						}
+
 						if (!IndexIsValid(cldidx->rd_index))
 							invalidate_parent = true;
 
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index ae83deaf51..1a5da20bf3 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -7342,6 +7342,9 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	bool		old_check_ok;
 	ObjectAddress address;
 	ListCell   *old_pfeqop_item = list_head(fkconstraint->old_conpfeqop);
+	bool		conislocal = true;
+	int			coninhcount = 0;
+	bool		connoinherit = true;
 
 	/*
 	 * Grab ShareRowExclusiveLock on the pk table, so that someone doesn't
@@ -7680,6 +7683,26 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	/*
 	 * Record the FK constraint in pg_constraint.
 	 */
+
+	/*
+	 * Set the inheritance related properties correctly if it's being
+	 * recursively added for a partition.
+	 */
+	if (OidIsValid(parentConstr))
+	{
+		/* Foreign keys are inherited only for partitioning. */
+		Assert(rel->rd_rel->relispartition);
+		conislocal = false;
+		/* Partitions can have only one parent. */
+		coninhcount = 1;
+		/* Make sure that the constraint can be further inherited. */
+		connoinherit = false;
+	}
+
+	/* If partitioned table, constraint must be marked as inheritable. */
+	if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+		connoinherit = false;
+
 	constrOid = CreateConstraintEntry(fkconstraint->conname,
 									  RelationGetNamespace(rel),
 									  CONSTRAINT_FOREIGN,
@@ -7706,9 +7729,9 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 									  NULL, /* no check constraint */
 									  NULL,
 									  NULL,
-									  true, /* islocal */
-									  0,	/* inhcount */
-									  true, /* isnoinherit */
+									  conislocal,
+									  coninhcount,
+									  connoinherit,
 									  false);	/* is_internal */
 	ObjectAddressSet(address, ConstraintRelationId, constrOid);
 
@@ -7757,20 +7780,15 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 			Oid			partitionId = partdesc->oids[i];
 			Relation	partition = heap_open(partitionId, lockmode);
 			AlteredTableInfo *childtab;
-			ObjectAddress childAddr;
 
 			CheckTableNotInUse(partition, "ALTER TABLE");
 
 			/* Find or create work queue entry for this table */
 			childtab = ATGetQueueEntry(wqueue, partition);
 
-			childAddr =
-				ATAddForeignKeyConstraint(wqueue, childtab, partition,
-										  fkconstraint, constrOid,
-										  recurse, true, lockmode);
-
-			/* Record this constraint as dependent on the parent one */
-			recordDependencyOn(&childAddr, &address, DEPENDENCY_INTERNAL_AUTO);
+			ATAddForeignKeyConstraint(wqueue, childtab, partition,
+									  fkconstraint, constrOid,
+									  recurse, true, lockmode);
 
 			heap_close(partition, NoLock);
 		}
@@ -9024,9 +9042,13 @@ ATExecDropConstraint(Relation rel, const char *constrName,
 
 		con = (Form_pg_constraint) GETSTRUCT(copy_tuple);
 
-		/* Right now only CHECK constraints can be inherited */
-		if (con->contype != CONSTRAINT_CHECK)
-			elog(ERROR, "inherited constraint is not a CHECK constraint");
+		/*
+		 * Right now only CHECK constraints and foreign key constraints can
+		 * be inherited.
+		 */
+		if (con->contype != CONSTRAINT_CHECK &&
+			con->contype != CONSTRAINT_FOREIGN)
+			elog(ERROR, "inherited constraint is not a CHECK constraint or a foreign key constraint");
 
 		if (con->coninhcount <= 0)	/* shouldn't happen */
 			elog(ERROR, "relation %u has non-inherited constraint \"%s\"",
@@ -14826,6 +14848,13 @@ ATExecDetachPartition(Relation rel, RangeVar *name)
 			elog(ERROR, "cache lookup failed for constraint %u", fk->conoid);
 		conform = (Form_pg_constraint) GETSTRUCT(contup);
 
+		/* No need to touch the partition's own foreign keys. */
+		if (conform->conislocal)
+		{
+			ReleaseSysCache(contup);
+			continue;
+		}
+
 		ConstraintSetParentConstraint(fk->conoid, InvalidOid);
 
 		/*
-- 
2.11.0

v1-0001-Ensure-PK-side-action-triggers-for-partitions-aft.patchtext/plain; charset=UTF-8; name=v1-0001-Ensure-PK-side-action-triggers-for-partitions-aft.patchDownload
From f757e49a93aa5a43b673b973f900bdbf91bef8e4 Mon Sep 17 00:00:00 2001
From: amit <amitlangote09@gmail.com>
Date: Wed, 9 Jan 2019 10:00:11 +0900
Subject: [PATCH v1 1/2] Ensure PK-side action triggers for partitions after
 being detached

Detaching a partition from the parent whose foreign key(s) would've
been duplicated in the partition makes the foreign key(s) stop
working corretly.  That's because PK-side action trigger for the
foreign key would refer to the parent as the referencing relation
and after the partition is detached it's data is no longer accessible
via parent.  So while the check triggers that remain even after
being detached takes care of the one side of enforcing the foreign
key of the detached partition, lack of corresponding PK-side action
trigger to check detached partition's data means the other side
doesn't work.

To fix, add the action triggers on the PK relation that points to
the detached partition when detaching.
---
 src/backend/catalog/pg_constraint.c |  3 +-
 src/backend/commands/tablecmds.c    | 57 +++++++++++++++++++++++++++++++++++--
 src/include/commands/tablecmds.h    |  3 +-
 3 files changed, 58 insertions(+), 5 deletions(-)

diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index 3c960c9423..c88e923327 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -714,7 +714,8 @@ clone_fk_constraints(Relation pg_constraint, Relation parentRel,
 		fkconstraint->initdeferred = constrForm->condeferred;
 
 		createForeignKeyTriggers(partRel, constrForm->confrelid, fkconstraint,
-								 constrOid, constrForm->conindid, false);
+								 constrOid, constrForm->conindid, false,
+								 true);
 
 		if (cloned)
 		{
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c75c5808ba..a3eb39ffdd 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -7623,7 +7623,7 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	 * though the constraints also exist below.
 	 */
 	createForeignKeyTriggers(rel, RelationGetRelid(pkrel), fkconstraint,
-							 constrOid, indexOid, !recursing);
+							 constrOid, indexOid, !recursing, true);
 
 	/*
 	 * Tell Phase 3 to check that the constraint is satisfied by existing
@@ -8728,7 +8728,8 @@ createForeignKeyCheckTriggers(Oid myRelOid, Oid refRelOid,
  */
 void
 createForeignKeyTriggers(Relation rel, Oid refRelOid, Constraint *fkconstraint,
-						 Oid constraintOid, Oid indexOid, bool create_action)
+						 Oid constraintOid, Oid indexOid, bool create_action,
+						 bool create_check)
 {
 	/*
 	 * For the referenced side, create action triggers, if requested.  (If the
@@ -8744,7 +8745,7 @@ createForeignKeyTriggers(Relation rel, Oid refRelOid, Constraint *fkconstraint,
 	 * For the referencing side, create the check triggers.  We only need
 	 * these on the partitions.
 	 */
-	if (rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
+	if (rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE && create_check)
 		createForeignKeyCheckTriggers(RelationGetRelid(rel), refRelOid,
 									  fkconstraint, constraintOid, indexOid);
 
@@ -14675,13 +14676,63 @@ ATExecDetachPartition(Relation rel, RangeVar *name)
 	{
 		ForeignKeyCacheInfo *fk = lfirst(cell);
 		HeapTuple	contup;
+		Form_pg_constraint conform;
+		Constraint *fkconstraint;
+		Relation	trigrel;
+		HeapTuple	trigtup;
+		HeapScanDesc scan;
+		ScanKeyData key[3];
 
 		contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
 		if (!contup)
 			elog(ERROR, "cache lookup failed for constraint %u", fk->conoid);
+		conform = (Form_pg_constraint) GETSTRUCT(contup);
 
 		ConstraintSetParentConstraint(fk->conoid, InvalidOid);
 
+		/*
+		 * We'll need to make the triggers on primary key relation to point
+		 * to this relation as the FK relation.  Currently, it points to
+		 * the parent from which this relation is being detached.
+		 */
+		fkconstraint = makeNode(Constraint);
+		fkconstraint->conname = pstrdup(NameStr(conform->conname));
+		fkconstraint->fk_upd_action = conform->confupdtype;
+		fkconstraint->fk_del_action = conform->confdeltype;
+		fkconstraint->deferrable = conform->condeferrable;
+		fkconstraint->initdeferred = conform->condeferred;
+
+		/* First check if such a trigger doesn't already exist. */
+		trigrel = heap_open(TriggerRelationId, AccessShareLock);
+		ScanKeyInit(&key[0],
+					Anum_pg_trigger_tgrelid,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(conform->confrelid));
+		ScanKeyInit(&key[1],
+					Anum_pg_trigger_tgconstrrelid,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(RelationGetRelid(partRel)));
+		ScanKeyInit(&key[2],
+					Anum_pg_trigger_tgconstraint,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(fk->conoid));
+		scan = heap_beginscan_catalog(trigrel, 3, key);
+		trigtup = heap_getnext(scan, ForwardScanDirection);
+		if (trigtup == NULL)
+		{
+			/*
+			 * Nope, we didn't find such a trigger, so create one.
+			 *
+			 * As we already got the check triggers, no need to recreate them,
+			 * so pass false for create_check.
+			 */
+			createForeignKeyTriggers(partRel, conform->confrelid, fkconstraint,
+									 fk->conoid, conform->conindid,
+									 true, false);
+		}
+		heap_endscan(scan);
+		heap_close(trigrel, AccessShareLock);
+
 		ReleaseSysCache(contup);
 	}
 	list_free_deep(fks);
diff --git a/src/include/commands/tablecmds.h b/src/include/commands/tablecmds.h
index ec3bb90b01..6bebc68425 100644
--- a/src/include/commands/tablecmds.h
+++ b/src/include/commands/tablecmds.h
@@ -78,7 +78,8 @@ extern void check_of_type(HeapTuple typetuple);
 
 extern void createForeignKeyTriggers(Relation rel, Oid refRelOid,
 						 Constraint *fkconstraint, Oid constraintOid,
-						 Oid indexOid, bool create_action);
+						 Oid indexOid, bool create_action,
+						 bool create_check);
 
 extern void register_on_commit_action(Oid relid, OnCommitAction action);
 extern void remove_on_commit_action(Oid relid);
-- 
2.11.0

v1-0002-Do-not-track-foreign-key-inheritance-by-dependenc.patchtext/plain; charset=UTF-8; name=v1-0002-Do-not-track-foreign-key-inheritance-by-dependenc.patchDownload
From 92da65c2359db0bb6947a283a7996273d22a014a Mon Sep 17 00:00:00 2001
From: amit <amitlangote09@gmail.com>
Date: Wed, 9 Jan 2019 14:01:47 +0900
Subject: [PATCH v1 2/2] Do not track foreign key inheritance by dependencies

Inheritance information maintained in pg_constraint is enough to
prevent a child constraint to be dropped and for them to be dropped
when the parent constraint is dropped.  So, do not create
dependencies between the parent foreign key constraint and its
children.

Also, fix ATAddForeignKeyConstraint to set inheritance information
correctly.
---
 src/backend/catalog/pg_constraint.c | 33 +++++++--------------
 src/backend/commands/indexcmds.c    | 18 ++++++++++++
 src/backend/commands/tablecmds.c    | 57 ++++++++++++++++++++++++++++---------
 3 files changed, 72 insertions(+), 36 deletions(-)

diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index c88e923327..60e4ff5594 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -478,8 +478,6 @@ clone_fk_constraints(Relation pg_constraint, Relation parentRel,
 		Constraint *fkconstraint;
 		bool		attach_it;
 		Oid			constrOid;
-		ObjectAddress parentAddr,
-					childAddr;
 		int			nelem;
 		ListCell   *cell;
 		int			i;
@@ -500,8 +498,6 @@ clone_fk_constraints(Relation pg_constraint, Relation parentRel,
 			continue;
 		}
 
-		ObjectAddressSet(parentAddr, ConstraintRelationId, parentConstrOid);
-
 		datum = fastgetattr(tuple, Anum_pg_constraint_conkey,
 							tupdesc, &isnull);
 		if (isnull)
@@ -702,9 +698,6 @@ clone_fk_constraints(Relation pg_constraint, Relation parentRel,
 								  1, false, true);
 		subclone = lappend_oid(subclone, constrOid);
 
-		ObjectAddressSet(childAddr, ConstraintRelationId, constrOid);
-		recordDependencyOn(&childAddr, &parentAddr, DEPENDENCY_INTERNAL_AUTO);
-
 		fkconstraint = makeNode(Constraint);
 		/* for now this is all we need */
 		fkconstraint->conname = pstrdup(NameStr(constrForm->conname));
@@ -1156,8 +1149,8 @@ AlterConstraintNamespaces(Oid ownerId, Oid oldNspId,
  * ConstraintSetParentConstraint
  *		Set a partition's constraint as child of its parent table's
  *
- * This updates the constraint's pg_constraint row to show it as inherited, and
- * add a dependency to the parent so that it cannot be removed on its own.
+ * This updates the constraint's pg_constraint row to change its
+ * inheritance properties.
  */
 void
 ConstraintSetParentConstraint(Oid childConstrId, Oid parentConstrId)
@@ -1166,8 +1159,6 @@ ConstraintSetParentConstraint(Oid childConstrId, Oid parentConstrId)
 	Form_pg_constraint constrForm;
 	HeapTuple	tuple,
 				newtup;
-	ObjectAddress depender;
-	ObjectAddress referenced;
 
 	constrRel = heap_open(ConstraintRelationId, RowExclusiveLock);
 	tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(childConstrId));
@@ -1179,25 +1170,23 @@ ConstraintSetParentConstraint(Oid childConstrId, Oid parentConstrId)
 	{
 		constrForm->conislocal = false;
 		constrForm->coninhcount++;
+		/*
+		 * An inherited foreign key constraint can never have more than one
+		 * parent, because inheriting foreign keys is only allowed for
+		 * partitioning where multiple inheritance is disallowed.
+		 */
+		Assert(constrForm->coninhcount == 1);
 		constrForm->conparentid = parentConstrId;
 
 		CatalogTupleUpdate(constrRel, &tuple->t_self, newtup);
-
-		ObjectAddressSet(referenced, ConstraintRelationId, parentConstrId);
-		ObjectAddressSet(depender, ConstraintRelationId, childConstrId);
-
-		recordDependencyOn(&depender, &referenced, DEPENDENCY_INTERNAL_AUTO);
 	}
 	else
 	{
 		constrForm->coninhcount--;
-		if (constrForm->coninhcount <= 0)
-			constrForm->conislocal = true;
+		/* See the above comment. */
+		Assert(constrForm->coninhcount == 0);
+		constrForm->conislocal = true;
 		constrForm->conparentid = InvalidOid;
-
-		deleteDependencyRecordsForClass(ConstraintRelationId, childConstrId,
-										ConstraintRelationId,
-										DEPENDENCY_INTERNAL_AUTO);
 		CatalogTupleUpdate(constrRel, &tuple->t_self, newtup);
 	}
 
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index d263903622..d8430f6fe5 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -972,9 +972,27 @@ DefineIndex(Oid relationId,
 						/* Attach index to parent and we're done. */
 						IndexSetParentIndex(cldidx, indexRelationId);
 						if (createdConstraintId != InvalidOid)
+						{
+							ObjectAddress depender;
+							ObjectAddress referenced;
+
 							ConstraintSetParentConstraint(cldConstrOid,
 														  createdConstraintId);
 
+							/*
+							 * Need to set an explicit dependency in this
+							 * case unlike other types of constraints where
+							 * the child constraint gets dropped due to
+							 * inheritance recursion.
+							 */
+							ObjectAddressSet(referenced, ConstraintRelationId,
+											 createdConstraintId);
+							ObjectAddressSet(depender, ConstraintRelationId,
+											 cldConstrOid);
+							recordDependencyOn(&depender, &referenced,
+											   DEPENDENCY_INTERNAL_AUTO);
+						}
+
 						if (!cldidx->rd_index->indisvalid)
 							invalidate_parent = true;
 
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index a3eb39ffdd..8e2fb9b189 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -7248,6 +7248,9 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	bool		old_check_ok;
 	ObjectAddress address;
 	ListCell   *old_pfeqop_item = list_head(fkconstraint->old_conpfeqop);
+	bool		conislocal = true;
+	int			coninhcount = 0;
+	bool		connoinherit = true;
 
 	/*
 	 * Grab ShareRowExclusiveLock on the pk table, so that someone doesn't
@@ -7586,6 +7589,26 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	/*
 	 * Record the FK constraint in pg_constraint.
 	 */
+
+	/*
+	 * Set the inheritance related properties correctly if it's being
+	 * recursively added for a partition.
+	 */
+	if (OidIsValid(parentConstr))
+	{
+		/* Foreign keys are inherited only for partitioning. */
+		Assert(rel->rd_rel->relispartition);
+		conislocal = false;
+		/* Partitions can have only one parent. */
+		coninhcount = 1;
+		/* Make sure that the constraint can be further inherited. */
+		connoinherit = false;
+	}
+
+	/* If partitioned table, constraint must be marked as inheritable. */
+	if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+		connoinherit = false;
+
 	constrOid = CreateConstraintEntry(fkconstraint->conname,
 									  RelationGetNamespace(rel),
 									  CONSTRAINT_FOREIGN,
@@ -7611,9 +7634,9 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 									  NULL, /* no exclusion constraint */
 									  NULL, /* no check constraint */
 									  NULL,
-									  true, /* islocal */
-									  0,	/* inhcount */
-									  true, /* isnoinherit */
+									  conislocal,
+									  coninhcount,
+									  connoinherit,
 									  false);	/* is_internal */
 	ObjectAddressSet(address, ConstraintRelationId, constrOid);
 
@@ -7662,20 +7685,15 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 			Oid			partitionId = partdesc->oids[i];
 			Relation	partition = heap_open(partitionId, lockmode);
 			AlteredTableInfo *childtab;
-			ObjectAddress childAddr;
 
 			CheckTableNotInUse(partition, "ALTER TABLE");
 
 			/* Find or create work queue entry for this table */
 			childtab = ATGetQueueEntry(wqueue, partition);
 
-			childAddr =
-				ATAddForeignKeyConstraint(wqueue, childtab, partition,
-										  fkconstraint, constrOid,
-										  recurse, true, lockmode);
-
-			/* Record this constraint as dependent on the parent one */
-			recordDependencyOn(&childAddr, &address, DEPENDENCY_INTERNAL_AUTO);
+			ATAddForeignKeyConstraint(wqueue, childtab, partition,
+									  fkconstraint, constrOid,
+									  recurse, true, lockmode);
 
 			heap_close(partition, NoLock);
 		}
@@ -8925,9 +8943,13 @@ ATExecDropConstraint(Relation rel, const char *constrName,
 
 		con = (Form_pg_constraint) GETSTRUCT(copy_tuple);
 
-		/* Right now only CHECK constraints can be inherited */
-		if (con->contype != CONSTRAINT_CHECK)
-			elog(ERROR, "inherited constraint is not a CHECK constraint");
+		/*
+		 * Right now only CHECK constraints and foreign key constraints can
+		 * be inherited.
+		 */
+		if (con->contype != CONSTRAINT_CHECK &&
+			con->contype != CONSTRAINT_FOREIGN)
+			elog(ERROR, "inherited constraint is not a CHECK constraint or a foreign key constraint");
 
 		if (con->coninhcount <= 0)	/* shouldn't happen */
 			elog(ERROR, "relation %u has non-inherited constraint \"%s\"",
@@ -14688,6 +14710,13 @@ ATExecDetachPartition(Relation rel, RangeVar *name)
 			elog(ERROR, "cache lookup failed for constraint %u", fk->conoid);
 		conform = (Form_pg_constraint) GETSTRUCT(contup);
 
+		/* No need to touch the partition's own foreign keys. */
+		if (conform->conislocal)
+		{
+			ReleaseSysCache(contup);
+			continue;
+		}
+
 		ConstraintSetParentConstraint(fk->conoid, InvalidOid);
 
 		/*
-- 
2.11.0

#2Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: Amit Langote (#1)
Re: problems with foreign keys on partitioned tables

Hi Amit

On 2019-Jan-09, Amit Langote wrote:

I noticed a couple of problems with foreign keys on partitioned tables.

Ouch, thanks for reporting. I think 0001 needs a bit of a tweak in pg11
to avoid an ABI break -- I intend to study this one and try to push
early next week. I'm going to see about pushing 0002 shortly,
adding some of your tests.

--
�lvaro Herrera https://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#3Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: Amit Langote (#1)
Re: problems with foreign keys on partitioned tables

On 2019-Jan-09, Amit Langote wrote:

1. Foreign keys of partitions stop working correctly after being detached
from the parent table

This happens because the action triggers defined on the PK relation (pk)
refers to p as the referencing relation. On detaching p1 from p, p1's
data is no longer accessible to that trigger.

Ouch.

To fix this problem, we need create action triggers on PK relation
that refer to p1 when it's detached (unless such triggers already
exist which might be true in some cases). Attached patch 0001 shows
this approach.

Hmm, okay. I'm not in love with the idea that such triggers might
already exist -- seems unclean. We should remove redundant action
triggers when we attach a table as a partition, no?

--
�lvaro Herrera https://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#4Amit Langote
Langote_Amit_f8@lab.ntt.co.jp
In reply to: Alvaro Herrera (#3)
4 attachment(s)
Re: problems with foreign keys on partitioned tables

On 2019/01/18 7:54, Alvaro Herrera wrote:

On 2019-Jan-09, Amit Langote wrote:

1. Foreign keys of partitions stop working correctly after being detached
from the parent table

This happens because the action triggers defined on the PK relation (pk)
refers to p as the referencing relation. On detaching p1 from p, p1's
data is no longer accessible to that trigger.

Ouch.

To fix this problem, we need create action triggers on PK relation
that refer to p1 when it's detached (unless such triggers already
exist which might be true in some cases). Attached patch 0001 shows
this approach.

Hmm, okay. I'm not in love with the idea that such triggers might
already exist -- seems unclean. We should remove redundant action
triggers when we attach a table as a partition, no?

OK, I agree. I have updated the patch to make things work that way. With
the patch:

create table pk (a int primary key);
create table p (a int references pk) partition by list (a);

-- this query shows the action triggers on the referenced rel ('pk'), name
-- of the constraint that the trigger is part of and the foreign key rel
-- ('p', etc.)

select tgrelid::regclass as pkrel, c.conname as fkconname,
tgconstrrelid::regclass as fkrel from pg_trigger t, pg_constraint c where
tgrelid = 'pk'::regclass and tgconstraint = c.oid;
pkrel │ fkconname │ fkrel
───────┼───────────┼───────
pk │ p_a_fkey │ p
pk │ p_a_fkey │ p
(2 rows)

create table p1 (
a int references pk,
foreign key (a) references pk (a) ON UPDATE CASCADE ON DELETE CASCADE
DEFERRABLE,
foreign key (a) references pk (a) MATCH FULL ON UPDATE CASCADE ON
DELETE CASCADE
) partition by list (a);

-- p1_a_fkey on 'p1' is equivalent to p_a_fkey on 'p', but they're not
-- attached yet

select tgrelid::regclass as pkrel, conname as fkconname,
tgconstrrelid::regclass as fkrel from pg_trigger t, pg_constraint c where
tgrelid = 'pk'::regclass and tgconstraint = c.oid;
pkrel │ fkconname │ fkrel
───────┼────────────┼───────
pk │ p_a_fkey │ p
pk │ p_a_fkey │ p
pk │ p1_a_fkey │ p1
pk │ p1_a_fkey │ p1
pk │ p1_a_fkey1 │ p1
pk │ p1_a_fkey1 │ p1
pk │ p1_a_fkey2 │ p1
pk │ p1_a_fkey2 │ p1
(8 rows)

create table p11 (like p1, foreign key (a) references pk);

-- again, p11_a_fkey, p1_a_fkey, and p_a_fkey are equivalent

select tgrelid::regclass as pkrel, conname as fkconname,
tgconstrrelid::regclass as fkrel from pg_trigger t, pg_constraint c where
tgrelid = 'pk'::regclass and tgconstraint = c.oid;
pkrel │ fkconname │ fkrel
───────┼────────────┼───────
pk │ p_a_fkey │ p
pk │ p_a_fkey │ p
pk │ p1_a_fkey │ p1
pk │ p1_a_fkey │ p1
pk │ p1_a_fkey1 │ p1
pk │ p1_a_fkey1 │ p1
pk │ p1_a_fkey2 │ p1
pk │ p1_a_fkey2 │ p1
pk │ p11_a_fkey │ p11
pk │ p11_a_fkey │ p11
(10 rows)

alter table p1 attach partition p11 for values in (1);

-- p1_a_fkey and p11_a_fkey merged, so triggers for the latter dropped

select tgrelid::regclass as pkrel, conname as fkconname,
tgconstrrelid::regclass as fkrel from pg_trigger t, pg_constraint c where
tgrelid = 'pk'::regclass and tgconstraint = c.oid;
pkrel │ fkconname │ fkrel
───────┼────────────┼───────
pk │ p_a_fkey │ p
pk │ p_a_fkey │ p
pk │ p1_a_fkey │ p1
pk │ p1_a_fkey │ p1
pk │ p1_a_fkey1 │ p1
pk │ p1_a_fkey1 │ p1
pk │ p1_a_fkey2 │ p1
pk │ p1_a_fkey2 │ p1
(8 rows)

-- p_a_fkey and p1_a_fkey merged, so triggers for the latter dropped

alter table p attach partition p1 for values in (1);

select tgrelid::regclass as pkrel, conname as fkconname,
tgconstrrelid::regclass as fkrel from pg_trigger t, pg_constraint c where
tgrelid = 'pk'::regclass and tgconstraint = c.oid;
pkrel │ fkconname │ fkrel
───────┼────────────┼───────
pk │ p_a_fkey │ p
pk │ p_a_fkey │ p
pk │ p1_a_fkey1 │ p1
pk │ p1_a_fkey1 │ p1
pk │ p1_a_fkey2 │ p1
pk │ p1_a_fkey2 │ p1
(6 rows)

alter table p detach partition p1;

-- p1_a_fkey needs its own triggers again

select tgrelid::regclass as pkrel, conname as fkconname,
tgconstrrelid::regclass as fkrel from pg_trigger t, pg_constraint c where
tgrelid = 'pk'::regclass and tgconstraint = c.oid;
pkrel │ fkconname │ fkrel
───────┼────────────┼───────
pk │ p_a_fkey │ p
pk │ p_a_fkey │ p
pk │ p1_a_fkey1 │ p1
pk │ p1_a_fkey1 │ p1
pk │ p1_a_fkey2 │ p1
pk │ p1_a_fkey2 │ p1
pk │ p1_a_fkey │ p1
pk │ p1_a_fkey │ p1
(8 rows)

alter table p1 detach partition p11;

-- p11_a_fkey needs its own triggers again

select tgrelid::regclass as pkrel, conname as fkconname,
tgconstrrelid::regclass as fkrel from pg_trigger t, pg_constraint c where
tgrelid = 'pk'::regclass and tgconstraint = c.oid;
pkrel │ fkconname │ fkrel
───────┼────────────┼───────
pk │ p_a_fkey │ p
pk │ p_a_fkey │ p
pk │ p1_a_fkey1 │ p1
pk │ p1_a_fkey1 │ p1
pk │ p1_a_fkey2 │ p1
pk │ p1_a_fkey2 │ p1
pk │ p1_a_fkey │ p1
pk │ p1_a_fkey │ p1
pk │ p11_a_fkey │ p11
pk │ p11_a_fkey │ p11
pk │ p1_a_fkey1 │ p11
pk │ p1_a_fkey1 │ p11
pk │ p1_a_fkey2 │ p11
pk │ p1_a_fkey2 │ p11
(14 rows)

-- try again

alter table p1 attach partition p11 for values in (1);

select tgrelid::regclass as pkrel, conname as fkconname,
tgconstrrelid::regclass as fkrel from pg_trigger t, pg_constraint c where
tgrelid = 'pk'::regclass and tgconstraint = c.oid;
pkrel │ fkconname │ fkrel
───────┼────────────┼───────
pk │ p_a_fkey │ p
pk │ p_a_fkey │ p
pk │ p1_a_fkey1 │ p1
pk │ p1_a_fkey1 │ p1
pk │ p1_a_fkey2 │ p1
pk │ p1_a_fkey2 │ p1
pk │ p1_a_fkey │ p1
pk │ p1_a_fkey │ p1
(8 rows)

alter table p attach partition p1 for values in (1);

select tgrelid::regclass as pkrel, conname as fkconname,
tgconstrrelid::regclass as fkrel from pg_trigger t, pg_constraint c where
tgrelid = 'pk'::regclass and tgconstraint = c.oid;
pkrel │ fkconname │ fkrel
───────┼────────────┼───────
pk │ p_a_fkey │ p
pk │ p_a_fkey │ p
pk │ p1_a_fkey1 │ p1
pk │ p1_a_fkey1 │ p1
pk │ p1_a_fkey2 │ p1
pk │ p1_a_fkey2 │ p1
(6 rows)

By the way, I also noticed that there's duplicated code in
clone_fk_constraints() which 0001 gets rid of:

datum = fastgetattr(tuple, Anum_pg_constraint_conpfeqop,
tupdesc, &isnull);
if (isnull)
elog(ERROR, "null conpfeqop");
arr = DatumGetArrayTypeP(datum);
nelem = ARR_DIMS(arr)[0];
if (ARR_NDIM(arr) != 1 ||
nelem < 1 ||
nelem > INDEX_MAX_KEYS ||
ARR_HASNULL(arr) ||
ARR_ELEMTYPE(arr) != OIDOID)
elog(ERROR, "conpfeqop is not a 1-D OID array");
memcpy(conpfeqop, ARR_DATA_PTR(arr), nelem * sizeof(Oid));

- datum = fastgetattr(tuple, Anum_pg_constraint_conpfeqop,
- tupdesc, &isnull);
- if (isnull)
- elog(ERROR, "null conpfeqop");
- arr = DatumGetArrayTypeP(datum);
- nelem = ARR_DIMS(arr)[0];
- if (ARR_NDIM(arr) != 1 ||
- nelem < 1 ||
- nelem > INDEX_MAX_KEYS ||
- ARR_HASNULL(arr) ||
- ARR_ELEMTYPE(arr) != OIDOID)
- elog(ERROR, "conpfeqop is not a 1-D OID array");
- memcpy(conpfeqop, ARR_DATA_PTR(arr), nelem * sizeof(Oid));
-

I know you're working on a bug fix in the thread on pgsql-bugs which is
related to the patch 0002 here, but attaching it here anyway, because it
proposes to get rid of the needless dependencies which I didn't see
mentioned on the other thread. Also, updated 0001 needed it to be rebased.

Like the last time, I've also attached the patches that can be applied
PG11 branch.

Thanks,
Amit

Attachments:

PG11-v2-0001-Ensure-PK-side-action-triggers-for-partitions-aft.patchtext/plain; charset=UTF-8; name=PG11-v2-0001-Ensure-PK-side-action-triggers-for-partitions-aft.patchDownload
From 9c877b0cb1b8dbe7f6957b5b1256686f3690dee5 Mon Sep 17 00:00:00 2001
From: amit <amitlangote09@gmail.com>
Date: Fri, 18 Jan 2019 14:13:11 +0900
Subject: [PATCH v2 1/2] Ensure PK-side action triggers for partitions after
 being detached

Detaching a partition from the parent whose foreign key(s) would've
been duplicated in the partition makes the foreign key(s) stop
working corretly.  That's because PK-side action trigger for the
foreign key would refer to the parent as the referencing relation
and after the partition is detached it's data is no longer accessible
via parent.  So while the check triggers that remain even after
being detached takes care of the one side of enforcing the foreign
key of the detached partition, lack of corresponding PK-side action
trigger to check detached partition's data means the other side
doesn't work.

To fix, add the action triggers on the PK relation that point to
the detached partition when detaching.
---
 src/backend/catalog/pg_constraint.c | 74 +++++++++++++++++++++++++++++--------
 src/backend/commands/tablecmds.c    | 42 +++++++++++++++++++--
 src/include/commands/tablecmds.h    |  3 +-
 3 files changed, 98 insertions(+), 21 deletions(-)

diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index f4057a9f15..9df6540ac3 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -26,6 +26,7 @@
 #include "catalog/partition.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_operator.h"
+#include "catalog/pg_trigger.h"
 #include "catalog/pg_type.h"
 #include "commands/defrem.h"
 #include "commands/tablecmds.h"
@@ -551,20 +552,6 @@ clone_fk_constraints(Relation pg_constraint, Relation parentRel,
 			elog(ERROR, "conpfeqop is not a 1-D OID array");
 		memcpy(conpfeqop, ARR_DATA_PTR(arr), nelem * sizeof(Oid));
 
-		datum = fastgetattr(tuple, Anum_pg_constraint_conpfeqop,
-							tupdesc, &isnull);
-		if (isnull)
-			elog(ERROR, "null conpfeqop");
-		arr = DatumGetArrayTypeP(datum);
-		nelem = ARR_DIMS(arr)[0];
-		if (ARR_NDIM(arr) != 1 ||
-			nelem < 1 ||
-			nelem > INDEX_MAX_KEYS ||
-			ARR_HASNULL(arr) ||
-			ARR_ELEMTYPE(arr) != OIDOID)
-			elog(ERROR, "conpfeqop is not a 1-D OID array");
-		memcpy(conpfeqop, ARR_DATA_PTR(arr), nelem * sizeof(Oid));
-
 		datum = fastgetattr(tuple, Anum_pg_constraint_conppeqop,
 							tupdesc, &isnull);
 		if (isnull)
@@ -607,6 +594,10 @@ clone_fk_constraints(Relation pg_constraint, Relation parentRel,
 			ForeignKeyCacheInfo *fk = lfirst_node(ForeignKeyCacheInfo, cell);
 			Form_pg_constraint partConstr;
 			HeapTuple	partcontup;
+			Relation	trigrel;
+			HeapTuple	trigtup;
+			HeapScanDesc scan;
+			ScanKeyData key[3];
 
 			attach_it = true;
 
@@ -658,7 +649,57 @@ clone_fk_constraints(Relation pg_constraint, Relation parentRel,
 
 			ReleaseSysCache(partcontup);
 
-			/* looks good!  Attach this constraint */
+			/*
+			 * looks good!  Attach this constraint.  Drop the action triggers
+			 * on the PK side that refer to this partition rel as part of
+			 * this constraint, because they will be wasteful after attaching
+			 * this constraint to the parent constraint.  After being attached,
+			 * action triggers corresponding to the parent constraint can
+			 * indirectly refer to this partition's data by referencing the
+			 * parent relation.
+			 */
+			trigrel = heap_open(TriggerRelationId, AccessShareLock);
+			ScanKeyInit(&key[0],
+						Anum_pg_trigger_tgrelid,
+						BTEqualStrategyNumber, F_OIDEQ,
+						ObjectIdGetDatum(fk->confrelid));
+			ScanKeyInit(&key[1],
+						Anum_pg_trigger_tgconstrrelid,
+						BTEqualStrategyNumber, F_OIDEQ,
+						ObjectIdGetDatum(fk->conrelid));
+			ScanKeyInit(&key[2],
+						Anum_pg_trigger_tgconstraint,
+						BTEqualStrategyNumber, F_OIDEQ,
+						ObjectIdGetDatum(fk->conoid));
+			scan = heap_beginscan_catalog(trigrel, 3, key);
+
+			/*
+			 * There should be two tuples to be deleted corresponding to
+			 * the two action triggers that createForeignKeyActionTriggers
+			 * would have created.
+			 */
+			trigtup = heap_getnext(scan, ForwardScanDirection);
+			Assert(trigtup != NULL);
+			deleteDependencyRecordsForClass(TriggerRelationId,
+											HeapTupleGetOid(trigtup),
+											ConstraintRelationId,
+											DEPENDENCY_INTERNAL);
+			CatalogTupleDelete(trigrel, &trigtup->t_self);
+
+			trigtup = heap_getnext(scan, ForwardScanDirection);
+			Assert(trigtup != NULL);
+			deleteDependencyRecordsForClass(TriggerRelationId,
+											HeapTupleGetOid(trigtup),
+											ConstraintRelationId,
+											DEPENDENCY_INTERNAL);
+			CatalogTupleDelete(trigrel, &trigtup->t_self);
+
+			trigtup = heap_getnext(scan, ForwardScanDirection);
+			Assert(trigtup == NULL);
+
+			heap_endscan(scan);
+			heap_close(trigrel, AccessShareLock);
+
 			ConstraintSetParentConstraint(fk->conoid,
 										  HeapTupleGetOid(tuple));
 			CommandCounterIncrement();
@@ -720,7 +761,8 @@ clone_fk_constraints(Relation pg_constraint, Relation parentRel,
 		fkconstraint->initdeferred = constrForm->condeferred;
 
 		createForeignKeyTriggers(partRel, constrForm->confrelid, fkconstraint,
-								 constrOid, constrForm->conindid, false);
+								 constrOid, constrForm->conindid, false,
+								 true);
 
 		if (cloned)
 		{
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 71b2e3f134..d668a57ac2 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -7718,7 +7718,7 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	 * though the constraints also exist below.
 	 */
 	createForeignKeyTriggers(rel, RelationGetRelid(pkrel), fkconstraint,
-							 constrOid, indexOid, !recursing);
+							 constrOid, indexOid, !recursing, true);
 
 	/*
 	 * Tell Phase 3 to check that the constraint is satisfied by existing
@@ -8827,7 +8827,8 @@ createForeignKeyCheckTriggers(Oid myRelOid, Oid refRelOid,
  */
 void
 createForeignKeyTriggers(Relation rel, Oid refRelOid, Constraint *fkconstraint,
-						 Oid constraintOid, Oid indexOid, bool create_action)
+						 Oid constraintOid, Oid indexOid, bool create_action,
+						 bool create_check)
 {
 	/*
 	 * For the referenced side, create action triggers, if requested.  (If the
@@ -8843,7 +8844,7 @@ createForeignKeyTriggers(Relation rel, Oid refRelOid, Constraint *fkconstraint,
 	 * For the referencing side, create the check triggers.  We only need
 	 * these on the partitions.
 	 */
-	if (rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
+	if (rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE && create_check)
 		createForeignKeyCheckTriggers(RelationGetRelid(rel), refRelOid,
 									  fkconstraint, constraintOid, indexOid);
 
@@ -14887,19 +14888,52 @@ ATExecDetachPartition(Relation rel, RangeVar *name)
 	}
 	heap_close(classRel, RowExclusiveLock);
 
-	/* Detach foreign keys */
+	/* Detach any foreign keys that are inherited */
 	fks = copyObject(RelationGetFKeyList(partRel));
 	foreach(cell, fks)
 	{
 		ForeignKeyCacheInfo *fk = lfirst(cell);
 		HeapTuple	contup;
+		Form_pg_constraint conform;
+		Constraint *fkconstraint;
 
 		contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
 		if (!contup)
 			elog(ERROR, "cache lookup failed for constraint %u", fk->conoid);
+		conform = (Form_pg_constraint) GETSTRUCT(contup);
 
+		/* consider only the inherited foreign keys */
+		if (conform->contype != CONSTRAINT_FOREIGN ||
+			!OidIsValid(conform->conparentid))
+		{
+			ReleaseSysCache(contup);
+			continue;
+		}
+
+		/* unset conparentid and adjust conislocal, coninhcount, etc. */
 		ConstraintSetParentConstraint(fk->conoid, InvalidOid);
 
+		/*
+		 * We'll need to make the action triggers on primary key relation that
+		 * point to this relation as the FK relation.  We need to do this,
+		 * because no PK-side triggers exist for an inherited FK constraint.
+		 * Now that we've detached it from the parent, we'd our own.
+		 */
+		fkconstraint = makeNode(Constraint);
+		fkconstraint->conname = pstrdup(NameStr(conform->conname));
+		fkconstraint->fk_upd_action = conform->confupdtype;
+		fkconstraint->fk_del_action = conform->confdeltype;
+		fkconstraint->deferrable = conform->condeferrable;
+		fkconstraint->initdeferred = conform->condeferred;
+
+		/*
+		 * As we already got the check triggers, no need to recreate them,
+		 * so pass false for create_check.
+		 */
+		createForeignKeyTriggers(partRel, conform->confrelid, fkconstraint,
+								 fk->conoid, conform->conindid,
+								 true, false);
+
 		ReleaseSysCache(contup);
 	}
 	list_free_deep(fks);
diff --git a/src/include/commands/tablecmds.h b/src/include/commands/tablecmds.h
index 138de84e83..dd985a80b6 100644
--- a/src/include/commands/tablecmds.h
+++ b/src/include/commands/tablecmds.h
@@ -77,7 +77,8 @@ extern void check_of_type(HeapTuple typetuple);
 
 extern void createForeignKeyTriggers(Relation rel, Oid refRelOid,
 						 Constraint *fkconstraint, Oid constraintOid,
-						 Oid indexOid, bool create_action);
+						 Oid indexOid, bool create_action,
+						 bool create_check);
 
 extern void register_on_commit_action(Oid relid, OnCommitAction action);
 extern void remove_on_commit_action(Oid relid);
-- 
2.11.0

PG11-v2-0002-Do-not-track-foreign-key-inheritance-by-dependenc.patchtext/plain; charset=UTF-8; name=PG11-v2-0002-Do-not-track-foreign-key-inheritance-by-dependenc.patchDownload
From 61c03e3435eb10304361ca093be7687f56463b9f Mon Sep 17 00:00:00 2001
From: amit <amitlangote09@gmail.com>
Date: Fri, 18 Jan 2019 14:20:13 +0900
Subject: [PATCH v2 2/2] Do not track foreign key inheritance by dependencies

Inheritance information maintained in pg_constraint is enough to
prevent a child constraint to be dropped and for them to be dropped
when the parent constraint is dropped.  So, do not create
dependencies between the parent foreign key constraint and its
children.

Also, fix ATAddForeignKeyConstraint to set inheritance information
correctly.
---
 src/backend/catalog/pg_constraint.c | 33 ++++++++----------------
 src/backend/commands/indexcmds.c    | 17 +++++++++++++
 src/backend/commands/tablecmds.c    | 50 ++++++++++++++++++++++++++-----------
 3 files changed, 64 insertions(+), 36 deletions(-)

diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index 9df6540ac3..0048b3a436 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -483,8 +483,6 @@ clone_fk_constraints(Relation pg_constraint, Relation parentRel,
 		Constraint *fkconstraint;
 		bool		attach_it;
 		Oid			constrOid;
-		ObjectAddress parentAddr,
-					childAddr;
 		int			nelem;
 		ListCell   *cell;
 		int			i;
@@ -505,8 +503,6 @@ clone_fk_constraints(Relation pg_constraint, Relation parentRel,
 			continue;
 		}
 
-		ObjectAddressSet(parentAddr, ConstraintRelationId, parentConstrOid);
-
 		datum = fastgetattr(tuple, Anum_pg_constraint_conkey,
 							tupdesc, &isnull);
 		if (isnull)
@@ -749,9 +745,6 @@ clone_fk_constraints(Relation pg_constraint, Relation parentRel,
 								  1, false, true);
 		subclone = lappend_oid(subclone, constrOid);
 
-		ObjectAddressSet(childAddr, ConstraintRelationId, constrOid);
-		recordDependencyOn(&childAddr, &parentAddr, DEPENDENCY_INTERNAL_AUTO);
-
 		fkconstraint = makeNode(Constraint);
 		/* for now this is all we need */
 		fkconstraint->conname = pstrdup(NameStr(constrForm->conname));
@@ -1203,8 +1196,8 @@ AlterConstraintNamespaces(Oid ownerId, Oid oldNspId,
  * ConstraintSetParentConstraint
  *		Set a partition's constraint as child of its parent table's
  *
- * This updates the constraint's pg_constraint row to show it as inherited, and
- * add a dependency to the parent so that it cannot be removed on its own.
+ * This updates the constraint's pg_constraint row to change its
+ * inheritance properties.
  */
 void
 ConstraintSetParentConstraint(Oid childConstrId, Oid parentConstrId)
@@ -1213,8 +1206,6 @@ ConstraintSetParentConstraint(Oid childConstrId, Oid parentConstrId)
 	Form_pg_constraint constrForm;
 	HeapTuple	tuple,
 				newtup;
-	ObjectAddress depender;
-	ObjectAddress referenced;
 
 	constrRel = heap_open(ConstraintRelationId, RowExclusiveLock);
 	tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(childConstrId));
@@ -1226,25 +1217,23 @@ ConstraintSetParentConstraint(Oid childConstrId, Oid parentConstrId)
 	{
 		constrForm->conislocal = false;
 		constrForm->coninhcount++;
+		/*
+		 * An inherited foreign key constraint can never have more than one
+		 * parent, because inheriting foreign keys is only allowed for
+		 * partitioning where multiple inheritance is disallowed.
+		 */
+		Assert(constrForm->coninhcount == 1);
 		constrForm->conparentid = parentConstrId;
 
 		CatalogTupleUpdate(constrRel, &tuple->t_self, newtup);
-
-		ObjectAddressSet(referenced, ConstraintRelationId, parentConstrId);
-		ObjectAddressSet(depender, ConstraintRelationId, childConstrId);
-
-		recordDependencyOn(&depender, &referenced, DEPENDENCY_INTERNAL_AUTO);
 	}
 	else
 	{
 		constrForm->coninhcount--;
-		if (constrForm->coninhcount <= 0)
-			constrForm->conislocal = true;
+		/* See the above comment. */
+		Assert(constrForm->coninhcount == 0);
+		constrForm->conislocal = true;
 		constrForm->conparentid = InvalidOid;
-
-		deleteDependencyRecordsForClass(ConstraintRelationId, childConstrId,
-										ConstraintRelationId,
-										DEPENDENCY_INTERNAL_AUTO);
 		CatalogTupleUpdate(constrRel, &tuple->t_self, newtup);
 	}
 
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index fec5bc5dd6..1b497b5bd8 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -974,8 +974,25 @@ DefineIndex(Oid relationId,
 						/* Attach index to parent and we're done. */
 						IndexSetParentIndex(cldidx, indexRelationId);
 						if (createdConstraintId != InvalidOid)
+						{
+							ObjectAddress depender;
+							ObjectAddress referenced;
+
 							ConstraintSetParentConstraint(cldConstrOid,
 														  createdConstraintId);
+							/*
+							 * Need to set an explicit dependency in this
+							 * case unlike other types of constraints where
+							 * the child constraint gets dropped due to
+							 * inheritance recursion.
+							 */
+							ObjectAddressSet(referenced, ConstraintRelationId,
+											 createdConstraintId);
+							ObjectAddressSet(depender, ConstraintRelationId,
+											 cldConstrOid);
+							recordDependencyOn(&depender, &referenced,
+											   DEPENDENCY_INTERNAL_AUTO);
+						}
 
 						if (!IndexIsValid(cldidx->rd_index))
 							invalidate_parent = true;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d668a57ac2..d045b4eddb 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -7342,6 +7342,9 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	bool		old_check_ok;
 	ObjectAddress address;
 	ListCell   *old_pfeqop_item = list_head(fkconstraint->old_conpfeqop);
+	bool		conislocal = true;
+	int			coninhcount = 0;
+	bool		connoinherit = true;
 
 	/*
 	 * Grab ShareRowExclusiveLock on the pk table, so that someone doesn't
@@ -7680,6 +7683,26 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	/*
 	 * Record the FK constraint in pg_constraint.
 	 */
+
+	/*
+	 * Set the inheritance related properties correctly if it's being
+	 * recursively added for a partition.
+	 */
+	if (OidIsValid(parentConstr))
+	{
+		/* Foreign keys are inherited only for partitioning. */
+		Assert(rel->rd_rel->relispartition);
+		conislocal = false;
+		/* Partitions can have only one parent. */
+		coninhcount = 1;
+		/* Make sure that the constraint can be further inherited. */
+		connoinherit = false;
+	}
+
+	/* If partitioned table, constraint must be marked as inheritable. */
+	if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+		connoinherit = false;
+
 	constrOid = CreateConstraintEntry(fkconstraint->conname,
 									  RelationGetNamespace(rel),
 									  CONSTRAINT_FOREIGN,
@@ -7706,9 +7729,9 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 									  NULL, /* no check constraint */
 									  NULL,
 									  NULL,
-									  true, /* islocal */
-									  0,	/* inhcount */
-									  true, /* isnoinherit */
+									  conislocal,
+									  coninhcount,
+									  connoinherit,
 									  false);	/* is_internal */
 	ObjectAddressSet(address, ConstraintRelationId, constrOid);
 
@@ -7757,20 +7780,15 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 			Oid			partitionId = partdesc->oids[i];
 			Relation	partition = heap_open(partitionId, lockmode);
 			AlteredTableInfo *childtab;
-			ObjectAddress childAddr;
 
 			CheckTableNotInUse(partition, "ALTER TABLE");
 
 			/* Find or create work queue entry for this table */
 			childtab = ATGetQueueEntry(wqueue, partition);
 
-			childAddr =
-				ATAddForeignKeyConstraint(wqueue, childtab, partition,
-										  fkconstraint, constrOid,
-										  recurse, true, lockmode);
-
-			/* Record this constraint as dependent on the parent one */
-			recordDependencyOn(&childAddr, &address, DEPENDENCY_INTERNAL_AUTO);
+			ATAddForeignKeyConstraint(wqueue, childtab, partition,
+									  fkconstraint, constrOid,
+									  recurse, true, lockmode);
 
 			heap_close(partition, NoLock);
 		}
@@ -9024,9 +9042,13 @@ ATExecDropConstraint(Relation rel, const char *constrName,
 
 		con = (Form_pg_constraint) GETSTRUCT(copy_tuple);
 
-		/* Right now only CHECK constraints can be inherited */
-		if (con->contype != CONSTRAINT_CHECK)
-			elog(ERROR, "inherited constraint is not a CHECK constraint");
+		/*
+		 * Right now only CHECK constraints and foreign key constraints can
+		 * be inherited.
+		 */
+		if (con->contype != CONSTRAINT_CHECK &&
+			con->contype != CONSTRAINT_FOREIGN)
+			elog(ERROR, "inherited constraint is not a CHECK constraint or a foreign key constraint");
 
 		if (con->coninhcount <= 0)	/* shouldn't happen */
 			elog(ERROR, "relation %u has non-inherited constraint \"%s\"",
-- 
2.11.0

v2-0001-Ensure-PK-side-action-triggers-for-partitions-aft.patchtext/plain; charset=UTF-8; name=v2-0001-Ensure-PK-side-action-triggers-for-partitions-aft.patchDownload
From d168bfa988bb8923fb138ceb4026ac005a7f580a Mon Sep 17 00:00:00 2001
From: amit <amitlangote09@gmail.com>
Date: Wed, 9 Jan 2019 10:00:11 +0900
Subject: [PATCH v2 1/2] Ensure PK-side action triggers for partitions after
 being detached

Detaching a partition from the parent whose foreign key(s) would've
been duplicated in the partition makes the foreign key(s) stop
working corretly.  That's because PK-side action trigger for the
foreign key would refer to the parent as the referencing relation
and after the partition is detached it's data is no longer accessible
via parent.  So while the check triggers that remain even after
being detached takes care of the one side of enforcing the foreign
key of the detached partition, lack of corresponding PK-side action
trigger to check detached partition's data means the other side
doesn't work.

To fix, add the action triggers on the PK relation that point to
the detached partition when detaching.
---
 src/backend/catalog/pg_constraint.c | 74 +++++++++++++++++++++++++++++--------
 src/backend/commands/tablecmds.c    | 42 +++++++++++++++++++--
 src/include/commands/tablecmds.h    |  3 +-
 3 files changed, 98 insertions(+), 21 deletions(-)

diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index 3c960c9423..3cac851447 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -27,6 +27,7 @@
 #include "catalog/partition.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_operator.h"
+#include "catalog/pg_trigger.h"
 #include "catalog/pg_type.h"
 #include "commands/defrem.h"
 #include "commands/tablecmds.h"
@@ -547,20 +548,6 @@ clone_fk_constraints(Relation pg_constraint, Relation parentRel,
 			elog(ERROR, "conpfeqop is not a 1-D OID array");
 		memcpy(conpfeqop, ARR_DATA_PTR(arr), nelem * sizeof(Oid));
 
-		datum = fastgetattr(tuple, Anum_pg_constraint_conpfeqop,
-							tupdesc, &isnull);
-		if (isnull)
-			elog(ERROR, "null conpfeqop");
-		arr = DatumGetArrayTypeP(datum);
-		nelem = ARR_DIMS(arr)[0];
-		if (ARR_NDIM(arr) != 1 ||
-			nelem < 1 ||
-			nelem > INDEX_MAX_KEYS ||
-			ARR_HASNULL(arr) ||
-			ARR_ELEMTYPE(arr) != OIDOID)
-			elog(ERROR, "conpfeqop is not a 1-D OID array");
-		memcpy(conpfeqop, ARR_DATA_PTR(arr), nelem * sizeof(Oid));
-
 		datum = fastgetattr(tuple, Anum_pg_constraint_conppeqop,
 							tupdesc, &isnull);
 		if (isnull)
@@ -603,6 +590,11 @@ clone_fk_constraints(Relation pg_constraint, Relation parentRel,
 			ForeignKeyCacheInfo *fk = lfirst_node(ForeignKeyCacheInfo, cell);
 			Form_pg_constraint partConstr;
 			HeapTuple	partcontup;
+			Relation	trigrel;
+			HeapTuple	trigtup;
+			Form_pg_trigger trigform;
+			HeapScanDesc scan;
+			ScanKeyData key[3];
 
 			attach_it = true;
 
@@ -654,7 +646,56 @@ clone_fk_constraints(Relation pg_constraint, Relation parentRel,
 
 			ReleaseSysCache(partcontup);
 
-			/* looks good!  Attach this constraint */
+			/*
+			 * looks good!  Attach this constraint.  Drop the action triggers
+			 * on the PK side that refer to this partition rel as part of
+			 * this constraint, because they will be wasteful after attaching
+			 * this constraint to the parent constraint.  After being attached,
+			 * action triggers corresponding to the parent constraint can
+			 * indirectly refer to this partition's data by referencing the
+			 * parent relation.
+			 */
+			trigrel = heap_open(TriggerRelationId, AccessShareLock);
+			ScanKeyInit(&key[0],
+						Anum_pg_trigger_tgrelid,
+						BTEqualStrategyNumber, F_OIDEQ,
+						ObjectIdGetDatum(fk->confrelid));
+			ScanKeyInit(&key[1],
+						Anum_pg_trigger_tgconstrrelid,
+						BTEqualStrategyNumber, F_OIDEQ,
+						ObjectIdGetDatum(fk->conrelid));
+			ScanKeyInit(&key[2],
+						Anum_pg_trigger_tgconstraint,
+						BTEqualStrategyNumber, F_OIDEQ,
+						ObjectIdGetDatum(fk->conoid));
+			scan = heap_beginscan_catalog(trigrel, 3, key);
+
+			/*
+			 * There should be two tuples to be deleted corresponding to
+			 * the two action triggers that createForeignKeyActionTriggers
+			 * would have created.
+			 */
+			trigtup = heap_getnext(scan, ForwardScanDirection);
+			Assert(trigtup != NULL);
+			trigform = (Form_pg_trigger) GETSTRUCT(trigtup);
+			deleteDependencyRecordsForClass(TriggerRelationId, trigform->oid,
+											ConstraintRelationId,
+											DEPENDENCY_INTERNAL);
+			CatalogTupleDelete(trigrel, &trigtup->t_self);
+
+			trigtup = heap_getnext(scan, ForwardScanDirection);
+			Assert(trigtup != NULL);
+			trigform = (Form_pg_trigger) GETSTRUCT(trigtup);
+			deleteDependencyRecordsForClass(TriggerRelationId, trigform->oid,
+											ConstraintRelationId,
+											DEPENDENCY_INTERNAL);
+			CatalogTupleDelete(trigrel, &trigtup->t_self);
+
+			trigtup = heap_getnext(scan, ForwardScanDirection);
+			Assert(trigtup == NULL);
+			heap_endscan(scan);
+			heap_close(trigrel, AccessShareLock);
+
 			ConstraintSetParentConstraint(fk->conoid, constrForm->oid);
 			CommandCounterIncrement();
 			attach_it = true;
@@ -714,7 +755,8 @@ clone_fk_constraints(Relation pg_constraint, Relation parentRel,
 		fkconstraint->initdeferred = constrForm->condeferred;
 
 		createForeignKeyTriggers(partRel, constrForm->confrelid, fkconstraint,
-								 constrOid, constrForm->conindid, false);
+								 constrOid, constrForm->conindid, false,
+								 true);
 
 		if (cloned)
 		{
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d2781cbf19..80b46d3139 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -7623,7 +7623,7 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	 * though the constraints also exist below.
 	 */
 	createForeignKeyTriggers(rel, RelationGetRelid(pkrel), fkconstraint,
-							 constrOid, indexOid, !recursing);
+							 constrOid, indexOid, !recursing, true);
 
 	/*
 	 * Tell Phase 3 to check that the constraint is satisfied by existing
@@ -8728,7 +8728,8 @@ createForeignKeyCheckTriggers(Oid myRelOid, Oid refRelOid,
  */
 void
 createForeignKeyTriggers(Relation rel, Oid refRelOid, Constraint *fkconstraint,
-						 Oid constraintOid, Oid indexOid, bool create_action)
+						 Oid constraintOid, Oid indexOid, bool create_action,
+						 bool create_check)
 {
 	/*
 	 * For the referenced side, create action triggers, if requested.  (If the
@@ -8744,7 +8745,7 @@ createForeignKeyTriggers(Relation rel, Oid refRelOid, Constraint *fkconstraint,
 	 * For the referencing side, create the check triggers.  We only need
 	 * these on the partitions.
 	 */
-	if (rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
+	if (rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE && create_check)
 		createForeignKeyCheckTriggers(RelationGetRelid(rel), refRelOid,
 									  fkconstraint, constraintOid, indexOid);
 
@@ -14749,19 +14750,52 @@ ATExecDetachPartition(Relation rel, RangeVar *name)
 	}
 	heap_close(classRel, RowExclusiveLock);
 
-	/* Detach foreign keys */
+	/* Detach any foreign keys that are inherited */
 	fks = copyObject(RelationGetFKeyList(partRel));
 	foreach(cell, fks)
 	{
 		ForeignKeyCacheInfo *fk = lfirst(cell);
 		HeapTuple	contup;
+		Form_pg_constraint conform;
+		Constraint *fkconstraint;
 
 		contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
 		if (!contup)
 			elog(ERROR, "cache lookup failed for constraint %u", fk->conoid);
+		conform = (Form_pg_constraint) GETSTRUCT(contup);
 
+		/* consider only the inherited foreign keys */
+		if (conform->contype != CONSTRAINT_FOREIGN ||
+			!OidIsValid(conform->conparentid))
+		{
+			ReleaseSysCache(contup);
+			continue;
+		}
+
+		/* unset conparentid and adjust conislocal, coninhcount, etc. */
 		ConstraintSetParentConstraint(fk->conoid, InvalidOid);
 
+		/*
+		 * We'll need to make the action triggers on primary key relation that
+		 * point to this relation as the FK relation.  We need to do this,
+		 * because no PK-side triggers exist for an inherited FK constraint.
+		 * Now that we've detached it from the parent, we'd our own.
+		 */
+		fkconstraint = makeNode(Constraint);
+		fkconstraint->conname = pstrdup(NameStr(conform->conname));
+		fkconstraint->fk_upd_action = conform->confupdtype;
+		fkconstraint->fk_del_action = conform->confdeltype;
+		fkconstraint->deferrable = conform->condeferrable;
+		fkconstraint->initdeferred = conform->condeferred;
+
+		/*
+		 * As we already got the check triggers, no need to recreate them,
+		 * so pass false for create_check.
+		 */
+		createForeignKeyTriggers(partRel, conform->confrelid, fkconstraint,
+								 fk->conoid, conform->conindid,
+								 true, false);
+
 		ReleaseSysCache(contup);
 	}
 	list_free_deep(fks);
diff --git a/src/include/commands/tablecmds.h b/src/include/commands/tablecmds.h
index ec3bb90b01..6bebc68425 100644
--- a/src/include/commands/tablecmds.h
+++ b/src/include/commands/tablecmds.h
@@ -78,7 +78,8 @@ extern void check_of_type(HeapTuple typetuple);
 
 extern void createForeignKeyTriggers(Relation rel, Oid refRelOid,
 						 Constraint *fkconstraint, Oid constraintOid,
-						 Oid indexOid, bool create_action);
+						 Oid indexOid, bool create_action,
+						 bool create_check);
 
 extern void register_on_commit_action(Oid relid, OnCommitAction action);
 extern void remove_on_commit_action(Oid relid);
-- 
2.11.0

v2-0002-Do-not-track-foreign-key-inheritance-by-dependenc.patchtext/plain; charset=UTF-8; name=v2-0002-Do-not-track-foreign-key-inheritance-by-dependenc.patchDownload
From 3c2ec8fb45df5b807cb00fa9d2c742451372bda2 Mon Sep 17 00:00:00 2001
From: amit <amitlangote09@gmail.com>
Date: Wed, 9 Jan 2019 14:01:47 +0900
Subject: [PATCH v2 2/2] Do not track foreign key inheritance by dependencies

Inheritance information maintained in pg_constraint is enough to
prevent a child constraint to be dropped and for them to be dropped
when the parent constraint is dropped.  So, do not create
dependencies between the parent foreign key constraint and its
children.

Also, fix ATAddForeignKeyConstraint to set inheritance information
correctly.
---
 src/backend/catalog/pg_constraint.c | 33 ++++++++----------------
 src/backend/commands/indexcmds.c    | 18 +++++++++++++
 src/backend/commands/tablecmds.c    | 50 ++++++++++++++++++++++++++-----------
 3 files changed, 65 insertions(+), 36 deletions(-)

diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index 3cac851447..3571416c3d 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -479,8 +479,6 @@ clone_fk_constraints(Relation pg_constraint, Relation parentRel,
 		Constraint *fkconstraint;
 		bool		attach_it;
 		Oid			constrOid;
-		ObjectAddress parentAddr,
-					childAddr;
 		int			nelem;
 		ListCell   *cell;
 		int			i;
@@ -501,8 +499,6 @@ clone_fk_constraints(Relation pg_constraint, Relation parentRel,
 			continue;
 		}
 
-		ObjectAddressSet(parentAddr, ConstraintRelationId, parentConstrOid);
-
 		datum = fastgetattr(tuple, Anum_pg_constraint_conkey,
 							tupdesc, &isnull);
 		if (isnull)
@@ -743,9 +739,6 @@ clone_fk_constraints(Relation pg_constraint, Relation parentRel,
 								  1, false, true);
 		subclone = lappend_oid(subclone, constrOid);
 
-		ObjectAddressSet(childAddr, ConstraintRelationId, constrOid);
-		recordDependencyOn(&childAddr, &parentAddr, DEPENDENCY_INTERNAL_AUTO);
-
 		fkconstraint = makeNode(Constraint);
 		/* for now this is all we need */
 		fkconstraint->conname = pstrdup(NameStr(constrForm->conname));
@@ -1197,8 +1190,8 @@ AlterConstraintNamespaces(Oid ownerId, Oid oldNspId,
  * ConstraintSetParentConstraint
  *		Set a partition's constraint as child of its parent table's
  *
- * This updates the constraint's pg_constraint row to show it as inherited, and
- * add a dependency to the parent so that it cannot be removed on its own.
+ * This updates the constraint's pg_constraint row to change its
+ * inheritance properties.
  */
 void
 ConstraintSetParentConstraint(Oid childConstrId, Oid parentConstrId)
@@ -1207,8 +1200,6 @@ ConstraintSetParentConstraint(Oid childConstrId, Oid parentConstrId)
 	Form_pg_constraint constrForm;
 	HeapTuple	tuple,
 				newtup;
-	ObjectAddress depender;
-	ObjectAddress referenced;
 
 	constrRel = heap_open(ConstraintRelationId, RowExclusiveLock);
 	tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(childConstrId));
@@ -1220,25 +1211,23 @@ ConstraintSetParentConstraint(Oid childConstrId, Oid parentConstrId)
 	{
 		constrForm->conislocal = false;
 		constrForm->coninhcount++;
+		/*
+		 * An inherited foreign key constraint can never have more than one
+		 * parent, because inheriting foreign keys is only allowed for
+		 * partitioning where multiple inheritance is disallowed.
+		 */
+		Assert(constrForm->coninhcount == 1);
 		constrForm->conparentid = parentConstrId;
 
 		CatalogTupleUpdate(constrRel, &tuple->t_self, newtup);
-
-		ObjectAddressSet(referenced, ConstraintRelationId, parentConstrId);
-		ObjectAddressSet(depender, ConstraintRelationId, childConstrId);
-
-		recordDependencyOn(&depender, &referenced, DEPENDENCY_INTERNAL_AUTO);
 	}
 	else
 	{
 		constrForm->coninhcount--;
-		if (constrForm->coninhcount <= 0)
-			constrForm->conislocal = true;
+		/* See the above comment. */
+		Assert(constrForm->coninhcount == 0);
+		constrForm->conislocal = true;
 		constrForm->conparentid = InvalidOid;
-
-		deleteDependencyRecordsForClass(ConstraintRelationId, childConstrId,
-										ConstraintRelationId,
-										DEPENDENCY_INTERNAL_AUTO);
 		CatalogTupleUpdate(constrRel, &tuple->t_self, newtup);
 	}
 
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index 1959e8a82e..91e0b968ac 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -973,9 +973,27 @@ DefineIndex(Oid relationId,
 						/* Attach index to parent and we're done. */
 						IndexSetParentIndex(cldidx, indexRelationId);
 						if (createdConstraintId != InvalidOid)
+						{
+							ObjectAddress depender;
+							ObjectAddress referenced;
+
 							ConstraintSetParentConstraint(cldConstrOid,
 														  createdConstraintId);
 
+							/*
+							 * Need to set an explicit dependency in this
+							 * case unlike other types of constraints where
+							 * the child constraint gets dropped due to
+							 * inheritance recursion.
+							 */
+							ObjectAddressSet(referenced, ConstraintRelationId,
+											 createdConstraintId);
+							ObjectAddressSet(depender, ConstraintRelationId,
+											 cldConstrOid);
+							recordDependencyOn(&depender, &referenced,
+											   DEPENDENCY_INTERNAL_AUTO);
+						}
+
 						if (!cldidx->rd_index->indisvalid)
 							invalidate_parent = true;
 
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 80b46d3139..96aef0699c 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -7248,6 +7248,9 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	bool		old_check_ok;
 	ObjectAddress address;
 	ListCell   *old_pfeqop_item = list_head(fkconstraint->old_conpfeqop);
+	bool		conislocal = true;
+	int			coninhcount = 0;
+	bool		connoinherit = true;
 
 	/*
 	 * Grab ShareRowExclusiveLock on the pk table, so that someone doesn't
@@ -7586,6 +7589,26 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	/*
 	 * Record the FK constraint in pg_constraint.
 	 */
+
+	/*
+	 * Set the inheritance related properties correctly if it's being
+	 * recursively added for a partition.
+	 */
+	if (OidIsValid(parentConstr))
+	{
+		/* Foreign keys are inherited only for partitioning. */
+		Assert(rel->rd_rel->relispartition);
+		conislocal = false;
+		/* Partitions can have only one parent. */
+		coninhcount = 1;
+		/* Make sure that the constraint can be further inherited. */
+		connoinherit = false;
+	}
+
+	/* If partitioned table, constraint must be marked as inheritable. */
+	if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+		connoinherit = false;
+
 	constrOid = CreateConstraintEntry(fkconstraint->conname,
 									  RelationGetNamespace(rel),
 									  CONSTRAINT_FOREIGN,
@@ -7611,9 +7634,9 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 									  NULL, /* no exclusion constraint */
 									  NULL, /* no check constraint */
 									  NULL,
-									  true, /* islocal */
-									  0,	/* inhcount */
-									  true, /* isnoinherit */
+									  conislocal,
+									  coninhcount,
+									  connoinherit,
 									  false);	/* is_internal */
 	ObjectAddressSet(address, ConstraintRelationId, constrOid);
 
@@ -7662,20 +7685,15 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 			Oid			partitionId = partdesc->oids[i];
 			Relation	partition = heap_open(partitionId, lockmode);
 			AlteredTableInfo *childtab;
-			ObjectAddress childAddr;
 
 			CheckTableNotInUse(partition, "ALTER TABLE");
 
 			/* Find or create work queue entry for this table */
 			childtab = ATGetQueueEntry(wqueue, partition);
 
-			childAddr =
-				ATAddForeignKeyConstraint(wqueue, childtab, partition,
-										  fkconstraint, constrOid,
-										  recurse, true, lockmode);
-
-			/* Record this constraint as dependent on the parent one */
-			recordDependencyOn(&childAddr, &address, DEPENDENCY_INTERNAL_AUTO);
+			ATAddForeignKeyConstraint(wqueue, childtab, partition,
+									  fkconstraint, constrOid,
+									  recurse, true, lockmode);
 
 			heap_close(partition, NoLock);
 		}
@@ -8925,9 +8943,13 @@ ATExecDropConstraint(Relation rel, const char *constrName,
 
 		con = (Form_pg_constraint) GETSTRUCT(copy_tuple);
 
-		/* Right now only CHECK constraints can be inherited */
-		if (con->contype != CONSTRAINT_CHECK)
-			elog(ERROR, "inherited constraint is not a CHECK constraint");
+		/*
+		 * Right now only CHECK constraints and foreign key constraints can
+		 * be inherited.
+		 */
+		if (con->contype != CONSTRAINT_CHECK &&
+			con->contype != CONSTRAINT_FOREIGN)
+			elog(ERROR, "inherited constraint is not a CHECK constraint or a foreign key constraint");
 
 		if (con->coninhcount <= 0)	/* shouldn't happen */
 			elog(ERROR, "relation %u has non-inherited constraint \"%s\"",
-- 
2.11.0

#5Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: Amit Langote (#4)
2 attachment(s)
Re: problems with foreign keys on partitioned tables

On 2019-Jan-18, Amit Langote wrote:

OK, I agree. I have updated the patch to make things work that way.

Thanks, this is better. There were a few other things I didn't like, so
I updated it. Mostly, two things:

1. I didn't like a seqscan on pg_trigger, so I turned that into an
indexed scan on the constraint OID, and then the other two conditions
are checked in the returned tuples. Also, what's the point on
duplicating code and checking how many you deleted? Just delete them
all.

2. I didn't like the ABI break, and it wasn't necessary: you can just
call createForeignKeyActionTriggers directly. That's much simpler.

I also added tests. While running them, I noticed that my previous
commit was broken in terms of relcache invalidation. I don't really
know if this is a new problem with that commit, or an existing one. The
fix is 0001.

Haven't got around to your 0002 yet.

--
�lvaro Herrera https://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

Attachments:

0001-Add-missing-relcache-reset.patchtext/x-diff; charset=us-asciiDownload
From 2047f054facf3ffaff31e36591279b9bd76ca53c Mon Sep 17 00:00:00 2001
From: Alvaro Herrera <alvherre@alvh.no-ip.org>
Date: Fri, 18 Jan 2019 19:06:40 -0300
Subject: [PATCH 1/2] Add missing relcache reset

Adding a foreign key to a partitioned table requires applying a relcache
reset to each partition, so that the newly added FK is correctly
recorded in its entry on next rebuild.

With the previous coding of ATAddForeignKeyConstraint, this may not have
been necessary, but with the one after 0325d7a5957b, it is.

Noted while testing another bugfix.
---
 src/backend/commands/tablecmds.c | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 1a1ac698e5..5a27f64aa7 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -7700,6 +7700,12 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
 			childtab->constraints = lappend(childtab->constraints, newcon);
 
+			/*
+			 * Also invalidate the partition's relcache entry, so that its
+			 * FK list gets updated on next rebuild.
+			 */
+			CacheInvalidateRelcache(partition);
+
 			heap_close(partition, lockmode);
 		}
 	}
-- 
2.11.0

0002-Fix-action-triggers-for-FK-constraints-on-detached-p.patchtext/x-diff; charset=us-asciiDownload
From 42b96db12aafea22285893e1d96c63a7f684abfd Mon Sep 17 00:00:00 2001
From: Alvaro Herrera <alvherre@alvh.no-ip.org>
Date: Fri, 18 Jan 2019 19:10:29 -0300
Subject: [PATCH 2/2] Fix action triggers for FK constraints on detached
 partitions

---
 src/backend/commands/tablecmds.c     | 72 +++++++++++++++++++++++++++++++++++-
 src/test/regress/sql/foreign_key.sql | 20 +++++++++-
 2 files changed, 89 insertions(+), 3 deletions(-)

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 5a27f64aa7..02a55ed3d6 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -7858,6 +7858,10 @@ CloneFkReferencing(Relation pg_constraint, Relation parentRel,
 			ForeignKeyCacheInfo *fk = lfirst_node(ForeignKeyCacheInfo, cell);
 			Form_pg_constraint partConstr;
 			HeapTuple	partcontup;
+			Relation	trigrel;
+			HeapTuple	trigtup;
+			SysScanDesc scan;
+			ScanKeyData key;
 
 			attach_it = true;
 
@@ -7909,7 +7913,39 @@ CloneFkReferencing(Relation pg_constraint, Relation parentRel,
 
 			ReleaseSysCache(partcontup);
 
-			/* looks good!  Attach this constraint */
+			/*
+			 * Looks good!  Attach this constraint.  Note that the action
+			 * triggers are no longer needed, so remove them.  We identify
+			 * them because they have our constraint OID, as well as being
+			 * on the referenced rel.
+			 */
+			trigrel = heap_open(TriggerRelationId, RowExclusiveLock);
+			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);
+
+				if (trgform->tgconstrrelid != fk->conrelid)
+					continue;
+				if (trgform->tgrelid != fk->confrelid)
+					continue;
+
+				deleteDependencyRecordsForClass(TriggerRelationId,
+												trgform->oid,
+												ConstraintRelationId,
+												DEPENDENCY_INTERNAL);
+				CatalogTupleDelete(trigrel, &trigtup->t_self);
+			}
+
+			systable_endscan(scan);
+			heap_close(trigrel, RowExclusiveLock);
+
 			ConstraintSetParentConstraint(fk->conoid, parentConstrOid);
 			CommandCounterIncrement();
 			attach_it = true;
@@ -15080,19 +15116,51 @@ ATExecDetachPartition(Relation rel, RangeVar *name)
 	}
 	heap_close(classRel, RowExclusiveLock);
 
-	/* Detach foreign keys */
+	/*
+	 * Detach any foreign keys that are inherited.  This includes creating
+	 * additional action triggers.
+	 */
 	fks = copyObject(RelationGetFKeyList(partRel));
 	foreach(cell, fks)
 	{
 		ForeignKeyCacheInfo *fk = lfirst(cell);
 		HeapTuple	contup;
+		Form_pg_constraint conform;
+		Constraint *fkconstraint;
 
 		contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
 		if (!contup)
 			elog(ERROR, "cache lookup failed for constraint %u", fk->conoid);
+		conform = (Form_pg_constraint) GETSTRUCT(contup);
 
+		/* consider only the inherited foreign keys */
+		if (conform->contype != CONSTRAINT_FOREIGN ||
+			!OidIsValid(conform->conparentid))
+		{
+			ReleaseSysCache(contup);
+			continue;
+		}
+
+		/* unset conparentid and adjust conislocal, coninhcount, etc. */
 		ConstraintSetParentConstraint(fk->conoid, InvalidOid);
 
+		/*
+		 * 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->conname = pstrdup(NameStr(conform->conname));
+		fkconstraint->fk_upd_action = conform->confupdtype;
+		fkconstraint->fk_del_action = conform->confdeltype;
+		fkconstraint->deferrable = conform->condeferrable;
+		fkconstraint->initdeferred = conform->condeferred;
+
+		createForeignKeyActionTriggers(partRel, conform->confrelid,
+									   fkconstraint, fk->conoid,
+									   conform->conindid);
+		CommandCounterIncrement();
+
 		ReleaseSysCache(contup);
 	}
 	list_free_deep(fks);
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index c3a7cfcc01..e08b6a21b0 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -1375,6 +1375,24 @@ create table fkpart0.fk_part_56_5 partition of fkpart0.fk_part_56
 alter table fkpart0.fk_part_56 drop constraint fk_part_a_fkey;
 alter table fkpart0.fk_part_56_5 drop constraint fk_part_a_fkey;
 
+-- verify that attaching and detaching partitions maintains the right set of
+-- triggers
+create schema fkpart1;
+set search_path to fkpart1;
+  create table pkey (a int primary key);
+  create table fk_part (a int) partition by list (a);
+  create table fk_part_1 partition of fk_part for values in (1) partition by list (a);
+  create table fk_part_1_1 partition of fk_part_1 for values in (1);
+alter table fkpart1.fk_part add foreign key (a) references fkpart1.pkey;
+insert into fkpart1.fk_part values (1);		-- should fail
+insert into fkpart1.pkey values (1);
+insert into fkpart1.fk_part values (1);
+delete from fkpart1.pkey where a = 1;		-- should fail
+alter table fkpart1.fk_part detach partition fkpart1.fk_part_1;
+create table fkpart1.fk_part_1_2 partition of fkpart1.fk_part_1 for values in (2);
+insert into fkpart1.fk_part_1 values (2);	-- should fail
+delete from fkpart1.pkey where a = 1;
+
 \set VERBOSITY terse	\\ -- suppress cascade details
-drop schema fkpart0 cascade;
+drop schema fkpart0, fkpart1 cascade;
 \set VERBOSITY default
-- 
2.11.0

#6Amit Langote
amitlangote09@gmail.com
In reply to: Alvaro Herrera (#5)
Re: problems with foreign keys on partitioned tables

On Sat, Jan 19, 2019 at 7:16 AM Alvaro Herrera <alvherre@2ndquadrant.com> wrote:

Thanks, this is better. There were a few other things I didn't like, so
I updated it. Mostly, two things:

1. I didn't like a seqscan on pg_trigger, so I turned that into an
indexed scan on the constraint OID, and then the other two conditions
are checked in the returned tuples. Also, what's the point on
duplicating code and checking how many you deleted? Just delete them
all.

Yeah, I didn't quite like what that code looked like, but it didn't
occur to me that there's an index on tgconstraint.

It looks much better now.

2. I didn't like the ABI break, and it wasn't necessary: you can just
call createForeignKeyActionTriggers directly. That's much simpler.

OK.

I also added tests. While running them, I noticed that my previous
commit was broken in terms of relcache invalidation. I don't really
know if this is a new problem with that commit, or an existing one. The
fix is 0001.

Looks good.

Thanks,
Amit

#7Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: Amit Langote (#6)
Re: problems with foreign keys on partitioned tables

Pushed now, thanks.

--
�lvaro Herrera https://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#8Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: Amit Langote (#4)
Re: problems with foreign keys on partitioned tables

Hi Amit,

Will you please rebase 0002? Please add your proposed tests cases to
it, too.

Thanks,

--
�lvaro Herrera https://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#9Amit Langote
Langote_Amit_f8@lab.ntt.co.jp
In reply to: Alvaro Herrera (#8)
2 attachment(s)
Re: problems with foreign keys on partitioned tables

On 2019/01/22 8:30, Alvaro Herrera wrote:

Hi Amit,

Will you please rebase 0002? Please add your proposed tests cases to
it, too.

Done. See the attached patches for HEAD and PG 11.

Thanks,
Amit

Attachments:

0001-Do-not-track-foreign-key-inheritance-by-dependencies.patchtext/plain; charset=UTF-8; name=0001-Do-not-track-foreign-key-inheritance-by-dependencies.patchDownload
From 432c4551990d0da1c77b6b9523296b0a2a0a5119 Mon Sep 17 00:00:00 2001
From: amit <amitlangote09@gmail.com>
Date: Tue, 22 Jan 2019 13:20:54 +0900
Subject: [PATCH] Do not track foreign key inheritance by dependencies

Inheritance information maintained in pg_constraint is enough to
prevent a child constraint to be dropped and for them to be dropped
when the parent constraint is dropped.  So, do not create
dependencies between the parent foreign key constraint and its
children.

Also, fix ATAddForeignKeyConstraint to set connoniherit on the parent
constraint correctly.
---
 src/backend/catalog/pg_constraint.c       | 27 +++++++++++++--------------
 src/backend/commands/indexcmds.c          | 22 ++++++++++++++++++++--
 src/backend/commands/tablecmds.c          | 24 ++++++++++++------------
 src/test/regress/expected/foreign_key.out | 17 +++++++++++++++--
 src/test/regress/sql/foreign_key.sql      | 14 +++++++++++++-
 5 files changed, 73 insertions(+), 31 deletions(-)

diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index 446b54b9ff..855d57c65a 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -762,8 +762,8 @@ AlterConstraintNamespaces(Oid ownerId, Oid oldNspId,
  * ConstraintSetParentConstraint
  *		Set a partition's constraint as child of its parent table's
  *
- * This updates the constraint's pg_constraint row to show it as inherited, and
- * add a dependency to the parent so that it cannot be removed on its own.
+ * This updates the constraint's pg_constraint row to change its inheritance
+ * properties.
  */
 void
 ConstraintSetParentConstraint(Oid childConstrId, Oid parentConstrId)
@@ -772,8 +772,6 @@ ConstraintSetParentConstraint(Oid childConstrId, Oid parentConstrId)
 	Form_pg_constraint constrForm;
 	HeapTuple	tuple,
 				newtup;
-	ObjectAddress depender;
-	ObjectAddress referenced;
 
 	constrRel = table_open(ConstraintRelationId, RowExclusiveLock);
 	tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(childConstrId));
@@ -785,25 +783,26 @@ ConstraintSetParentConstraint(Oid childConstrId, Oid parentConstrId)
 	{
 		constrForm->conislocal = false;
 		constrForm->coninhcount++;
+
+		/*
+		 * An inherited foreign key constraint can never have more than one
+		 * parent, because inheriting foreign keys is only allowed for
+		 * partitioning where multiple inheritance is disallowed.
+		 */
+		Assert(constrForm->coninhcount == 1);
+
 		constrForm->conparentid = parentConstrId;
 
 		CatalogTupleUpdate(constrRel, &tuple->t_self, newtup);
-
-		ObjectAddressSet(referenced, ConstraintRelationId, parentConstrId);
-		ObjectAddressSet(depender, ConstraintRelationId, childConstrId);
-
-		recordDependencyOn(&depender, &referenced, DEPENDENCY_INTERNAL_AUTO);
 	}
 	else
 	{
 		constrForm->coninhcount--;
-		if (constrForm->coninhcount <= 0)
-			constrForm->conislocal = true;
+		/* See the above comment. */
+		Assert(constrForm->coninhcount == 0);
+		constrForm->conislocal = true;
 		constrForm->conparentid = InvalidOid;
 
-		deleteDependencyRecordsForClass(ConstraintRelationId, childConstrId,
-										ConstraintRelationId,
-										DEPENDENCY_INTERNAL_AUTO);
 		CatalogTupleUpdate(constrRel, &tuple->t_self, newtup);
 	}
 
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index 3edc94308e..6c8aa5d149 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -972,8 +972,26 @@ DefineIndex(Oid relationId,
 						/* Attach index to parent and we're done. */
 						IndexSetParentIndex(cldidx, indexRelationId);
 						if (createdConstraintId != InvalidOid)
-							ConstraintSetParentConstraint(cldConstrOid,
-														  createdConstraintId);
+						{
+							ObjectAddress depender;
+							ObjectAddress referenced;
+
+ 							ConstraintSetParentConstraint(cldConstrOid,
+ 														  createdConstraintId);
+ 
+							/*
+							 * Need to set an explicit dependency in this
+							 * case unlike other types of constraints where
+							 * the child constraint gets dropped due to
+							 * inheritance recursion.
+							 */
+							ObjectAddressSet(referenced, ConstraintRelationId,
+											 createdConstraintId);
+							ObjectAddressSet(depender, ConstraintRelationId,
+											 cldConstrOid);
+							recordDependencyOn(&depender, &referenced,
+											   DEPENDENCY_INTERNAL_AUTO);
+						}
 
 						if (!cldidx->rd_index->indisvalid)
 							invalidate_parent = true;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index fbd2d101c1..0040ac63d2 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -7251,6 +7251,9 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	bool		old_check_ok;
 	ObjectAddress address;
 	ListCell   *old_pfeqop_item = list_head(fkconstraint->old_conpfeqop);
+	/* Partitioned table's foreign key must always be inheritable. */
+	bool		connoinherit = (rel->rd_rel->relkind !=
+								RELKIND_PARTITIONED_TABLE);
 
 	/*
 	 * Grab ShareRowExclusiveLock on the pk table, so that someone doesn't
@@ -7616,7 +7619,7 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 									  NULL,
 									  true, /* islocal */
 									  0,	/* inhcount */
-									  true, /* isnoinherit */
+									  connoinherit,
 									  false);	/* is_internal */
 	ObjectAddressSet(address, ConstraintRelationId, constrOid);
 
@@ -7812,8 +7815,6 @@ CloneFkReferencing(Relation pg_constraint, Relation parentRel,
 		Constraint *fkconstraint;
 		bool		attach_it;
 		Oid			constrOid;
-		ObjectAddress parentAddr,
-					childAddr;
 		ListCell   *cell;
 		int			i;
 
@@ -7830,8 +7831,6 @@ CloneFkReferencing(Relation pg_constraint, Relation parentRel,
 			continue;
 		}
 
-		ObjectAddressSet(parentAddr, ConstraintRelationId, parentConstrOid);
-
 		DeconstructFkConstraintRow(tuple, &numfks, conkey, confkey,
 								   conpfeqop, conppeqop, conffeqop);
 		for (i = 0; i < numfks; i++)
@@ -7986,9 +7985,6 @@ CloneFkReferencing(Relation pg_constraint, Relation parentRel,
 								  1, false, true);
 		subclone = lappend_oid(subclone, constrOid);
 
-		ObjectAddressSet(childAddr, ConstraintRelationId, constrOid);
-		recordDependencyOn(&childAddr, &parentAddr, DEPENDENCY_INTERNAL_AUTO);
-
 		fkconstraint = makeNode(Constraint);
 		/* for now this is all we need */
 		fkconstraint->conname = pstrdup(NameStr(constrForm->conname));
@@ -9284,11 +9280,15 @@ ATExecDropConstraint(Relation rel, const char *constrName,
 
 		con = (Form_pg_constraint) GETSTRUCT(copy_tuple);
 
-		/* Right now only CHECK constraints can be inherited */
-		if (con->contype != CONSTRAINT_CHECK)
-			elog(ERROR, "inherited constraint is not a CHECK constraint");
+		/*
+		 * Only CHECK constraints and foreign key constraints can be
+		 * inherited.
+		*/
+		if (con->contype != CONSTRAINT_CHECK &&
+			con->contype != CONSTRAINT_FOREIGN)
+			elog(ERROR, "inherited constraint is not a CHECK constraint or a foreign key constraint");
 
-		if (con->coninhcount <= 0)	/* shouldn't happen */
+		if (con->coninhcount == 0)	/* shouldn't happen */
 			elog(ERROR, "relation %u has non-inherited constraint \"%s\"",
 				 childrelid, constrName);
 
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 76040a7601..ba83c9d9bb 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -1945,7 +1945,20 @@ DETAIL:  Key (a)=(2) is not present in table "pkey".
 delete from fkpart1.pkey where a = 1;
 ERROR:  update or delete on table "pkey" violates foreign key constraint "fk_part_a_fkey" on table "fk_part_1"
 DETAIL:  Key (a)=(1) is still referenced from table "fk_part_1".
+-- verify that attaching and detaching partitions manipulates the inheritance
+-- properties of their FK constraints correctly
+create schema fkpart2;
+create table fkpart2.pkey (a int primary key);
+create table fkpart2.fk_part (a int, constraint p_fkey foreign key (a) references fkpart2.pkey) partition by list (a);
+create table fkpart2.fk_part_1 partition of fkpart2.fk_part for values in (1) partition by list (a);
+create table fkpart2.fk_part_1_1 partition of fkpart2.fk_part_1 for values in (1);
+alter table fkpart2.fk_part_1 drop constraint p_fkey;	-- should fail
+ERROR:  cannot drop inherited constraint "p_fkey" of relation "fk_part_1"
+alter table fkpart2.fk_part_1_1 drop constraint p_fkey;	-- should fail
+ERROR:  cannot drop inherited constraint "p_fkey" of relation "fk_part_1_1"
+alter table fkpart2.fk_part detach partition fkpart2.fk_part_1;
+alter table fkpart2.fk_part_1 drop constraint p_fkey;
 \set VERBOSITY terse	\\ -- suppress cascade details
-drop schema fkpart0, fkpart1 cascade;
-NOTICE:  drop cascades to 5 other objects
+drop schema fkpart0, fkpart1, fkpart2 cascade;
+NOTICE:  drop cascades to 8 other objects
 \set VERBOSITY default
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index 9ed1166c66..966162d045 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -1392,6 +1392,18 @@ create table fkpart1.fk_part_1_2 partition of fkpart1.fk_part_1 for values in (2
 insert into fkpart1.fk_part_1 values (2);	-- should fail
 delete from fkpart1.pkey where a = 1;
 
+-- verify that attaching and detaching partitions manipulates the inheritance
+-- properties of their FK constraints correctly
+create schema fkpart2;
+create table fkpart2.pkey (a int primary key);
+create table fkpart2.fk_part (a int, constraint p_fkey foreign key (a) references fkpart2.pkey) partition by list (a);
+create table fkpart2.fk_part_1 partition of fkpart2.fk_part for values in (1) partition by list (a);
+create table fkpart2.fk_part_1_1 partition of fkpart2.fk_part_1 for values in (1);
+alter table fkpart2.fk_part_1 drop constraint p_fkey;	-- should fail
+alter table fkpart2.fk_part_1_1 drop constraint p_fkey;	-- should fail
+alter table fkpart2.fk_part detach partition fkpart2.fk_part_1;
+alter table fkpart2.fk_part_1 drop constraint p_fkey;
+
 \set VERBOSITY terse	\\ -- suppress cascade details
-drop schema fkpart0, fkpart1 cascade;
+drop schema fkpart0, fkpart1, fkpart2 cascade;
 \set VERBOSITY default
-- 
2.11.0

pg11-0001-Do-not-track-foreign-key-inheritance-by-dependencies.patchtext/plain; charset=UTF-8; name=pg11-0001-Do-not-track-foreign-key-inheritance-by-dependencies.patchDownload
From 5178942bc853b3c70ff03750477ae3519d82883f Mon Sep 17 00:00:00 2001
From: amit <amitlangote09@gmail.com>
Date: Tue, 22 Jan 2019 12:48:12 +0900
Subject: [PATCH] Do not track foreign key inheritance by dependencies

Inheritance information maintained in pg_constraint is enough to
prevent a child constraint to be dropped and for them to be dropped
when the parent constraint is dropped.  So, do not create
dependencies between the parent foreign key constraint and its
children.

Also, fix ATAddForeignKeyConstraint to set connoniherit on the parent
constraint correctly.
---
 src/backend/catalog/pg_constraint.c       | 27 +++++++++++++--------------
 src/backend/commands/indexcmds.c          | 18 ++++++++++++++++++
 src/backend/commands/tablecmds.c          | 24 ++++++++++++------------
 src/test/regress/expected/foreign_key.out | 17 +++++++++++++++--
 src/test/regress/sql/foreign_key.sql      | 14 +++++++++++++-
 5 files changed, 71 insertions(+), 29 deletions(-)

diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index 5720c652b2..3e939e014f 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -771,8 +771,8 @@ AlterConstraintNamespaces(Oid ownerId, Oid oldNspId,
  * ConstraintSetParentConstraint
  *		Set a partition's constraint as child of its parent table's
  *
- * This updates the constraint's pg_constraint row to show it as inherited, and
- * add a dependency to the parent so that it cannot be removed on its own.
+ * This updates the constraint's pg_constraint row to change its inheritance
+ * properties.
  */
 void
 ConstraintSetParentConstraint(Oid childConstrId, Oid parentConstrId)
@@ -781,8 +781,6 @@ ConstraintSetParentConstraint(Oid childConstrId, Oid parentConstrId)
 	Form_pg_constraint constrForm;
 	HeapTuple	tuple,
 				newtup;
-	ObjectAddress depender;
-	ObjectAddress referenced;
 
 	constrRel = heap_open(ConstraintRelationId, RowExclusiveLock);
 	tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(childConstrId));
@@ -794,25 +792,26 @@ ConstraintSetParentConstraint(Oid childConstrId, Oid parentConstrId)
 	{
 		constrForm->conislocal = false;
 		constrForm->coninhcount++;
+
+		/*
+		 * An inherited foreign key constraint can never have more than one
+		 * parent, because inheriting foreign keys is only allowed for
+		 * partitioning where multiple inheritance is disallowed.
+		 */
+		Assert(constrForm->coninhcount == 1);
+
 		constrForm->conparentid = parentConstrId;
 
 		CatalogTupleUpdate(constrRel, &tuple->t_self, newtup);
-
-		ObjectAddressSet(referenced, ConstraintRelationId, parentConstrId);
-		ObjectAddressSet(depender, ConstraintRelationId, childConstrId);
-
-		recordDependencyOn(&depender, &referenced, DEPENDENCY_INTERNAL_AUTO);
 	}
 	else
 	{
 		constrForm->coninhcount--;
-		if (constrForm->coninhcount <= 0)
-			constrForm->conislocal = true;
+		/* See the above comment. */
+		Assert(constrForm->coninhcount == 0);
+		constrForm->conislocal = true;
 		constrForm->conparentid = InvalidOid;
 
-		deleteDependencyRecordsForClass(ConstraintRelationId, childConstrId,
-										ConstraintRelationId,
-										DEPENDENCY_INTERNAL_AUTO);
 		CatalogTupleUpdate(constrRel, &tuple->t_self, newtup);
 	}
 
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index fec5bc5dd6..048fabcc61 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -974,9 +974,27 @@ DefineIndex(Oid relationId,
 						/* Attach index to parent and we're done. */
 						IndexSetParentIndex(cldidx, indexRelationId);
 						if (createdConstraintId != InvalidOid)
+						{
+							ObjectAddress depender;
+							ObjectAddress referenced;
+
 							ConstraintSetParentConstraint(cldConstrOid,
 														  createdConstraintId);
 
+							/*
+							 * Need to set an explicit dependency in this
+							 * case unlike other types of constraints where
+							 * the child constraint gets dropped due to
+							 * inheritance recursion.
+							 */
+							ObjectAddressSet(referenced, ConstraintRelationId,
+											 createdConstraintId);
+							ObjectAddressSet(depender, ConstraintRelationId,
+											 cldConstrOid);
+							recordDependencyOn(&depender, &referenced,
+											   DEPENDENCY_INTERNAL_AUTO);
+						}
+
 						if (!IndexIsValid(cldidx->rd_index))
 							invalidate_parent = true;
 
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 5a62adbafc..9ec27eb6bf 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -7344,6 +7344,9 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	bool		old_check_ok;
 	ObjectAddress address;
 	ListCell   *old_pfeqop_item = list_head(fkconstraint->old_conpfeqop);
+	/* Partitioned table's foreign key must always be inheritable. */
+	bool		connoinherit = (rel->rd_rel->relkind !=
+								RELKIND_PARTITIONED_TABLE);
 
 	/*
 	 * Grab ShareRowExclusiveLock on the pk table, so that someone doesn't
@@ -7710,7 +7713,7 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 									  NULL,
 									  true, /* islocal */
 									  0,	/* inhcount */
-									  true, /* isnoinherit */
+									  connoinherit,
 									  false);	/* is_internal */
 	ObjectAddressSet(address, ConstraintRelationId, constrOid);
 
@@ -7906,8 +7909,6 @@ CloneFkReferencing(Relation pg_constraint, Relation parentRel,
 		Constraint *fkconstraint;
 		bool		attach_it;
 		Oid			constrOid;
-		ObjectAddress parentAddr,
-					childAddr;
 		ListCell   *cell;
 		int			i;
 
@@ -7924,8 +7925,6 @@ CloneFkReferencing(Relation pg_constraint, Relation parentRel,
 			continue;
 		}
 
-		ObjectAddressSet(parentAddr, ConstraintRelationId, parentConstrOid);
-
 		DeconstructFkConstraintRow(tuple, &numfks, conkey, confkey,
 								   conpfeqop, conppeqop, conffeqop);
 		for (i = 0; i < numfks; i++)
@@ -8081,9 +8080,6 @@ CloneFkReferencing(Relation pg_constraint, Relation parentRel,
 								  1, false, true);
 		subclone = lappend_oid(subclone, constrOid);
 
-		ObjectAddressSet(childAddr, ConstraintRelationId, constrOid);
-		recordDependencyOn(&childAddr, &parentAddr, DEPENDENCY_INTERNAL_AUTO);
-
 		fkconstraint = makeNode(Constraint);
 		/* for now this is all we need */
 		fkconstraint->conname = pstrdup(NameStr(constrForm->conname));
@@ -9383,11 +9379,15 @@ ATExecDropConstraint(Relation rel, const char *constrName,
 
 		con = (Form_pg_constraint) GETSTRUCT(copy_tuple);
 
-		/* Right now only CHECK constraints can be inherited */
-		if (con->contype != CONSTRAINT_CHECK)
-			elog(ERROR, "inherited constraint is not a CHECK constraint");
+		/*
+		 * Only CHECK constraints and foreign key constraints can be
+		 * inherited.
+		*/
+		if (con->contype != CONSTRAINT_CHECK &&
+			con->contype != CONSTRAINT_FOREIGN)
+			elog(ERROR, "inherited constraint is not a CHECK constraint or a foreign key constraint");
 
-		if (con->coninhcount <= 0)	/* shouldn't happen */
+		if (con->coninhcount == 0)	/* shouldn't happen */
 			elog(ERROR, "relation %u has non-inherited constraint \"%s\"",
 				 childrelid, constrName);
 
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 4fd2341388..aa44daf0e8 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -1884,7 +1884,20 @@ DETAIL:  Key (a)=(2) is not present in table "pkey".
 delete from fkpart1.pkey where a = 1;
 ERROR:  update or delete on table "pkey" violates foreign key constraint "fk_part_a_fkey" on table "fk_part_1"
 DETAIL:  Key (a)=(1) is still referenced from table "fk_part_1".
+-- verify that attaching and detaching partitions manipulates the inheritance
+-- properties of their FK constraints correctly
+create schema fkpart2;
+create table fkpart2.pkey (a int primary key);
+create table fkpart2.fk_part (a int, constraint p_fkey foreign key (a) references fkpart2.pkey) partition by list (a);
+create table fkpart2.fk_part_1 partition of fkpart2.fk_part for values in (1) partition by list (a);
+create table fkpart2.fk_part_1_1 partition of fkpart2.fk_part_1 for values in (1);
+alter table fkpart2.fk_part_1 drop constraint p_fkey;	-- should fail
+ERROR:  cannot drop inherited constraint "p_fkey" of relation "fk_part_1"
+alter table fkpart2.fk_part_1_1 drop constraint p_fkey;	-- should fail
+ERROR:  cannot drop inherited constraint "p_fkey" of relation "fk_part_1_1"
+alter table fkpart2.fk_part detach partition fkpart2.fk_part_1;
+alter table fkpart2.fk_part_1 drop constraint p_fkey;
 \set VERBOSITY terse	\\ -- suppress cascade details
-drop schema fkpart0, fkpart1 cascade;
-NOTICE:  drop cascades to 5 other objects
+drop schema fkpart0, fkpart1, fkpart2 cascade;
+NOTICE:  drop cascades to 8 other objects
 \set VERBOSITY default
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index dd0be01c77..fe9b300d90 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -1341,6 +1341,18 @@ create table fkpart1.fk_part_1_2 partition of fkpart1.fk_part_1 for values in (2
 insert into fkpart1.fk_part_1 values (2);	-- should fail
 delete from fkpart1.pkey where a = 1;
 
+-- verify that attaching and detaching partitions manipulates the inheritance
+-- properties of their FK constraints correctly
+create schema fkpart2;
+create table fkpart2.pkey (a int primary key);
+create table fkpart2.fk_part (a int, constraint p_fkey foreign key (a) references fkpart2.pkey) partition by list (a);
+create table fkpart2.fk_part_1 partition of fkpart2.fk_part for values in (1) partition by list (a);
+create table fkpart2.fk_part_1_1 partition of fkpart2.fk_part_1 for values in (1);
+alter table fkpart2.fk_part_1 drop constraint p_fkey;	-- should fail
+alter table fkpart2.fk_part_1_1 drop constraint p_fkey;	-- should fail
+alter table fkpart2.fk_part detach partition fkpart2.fk_part_1;
+alter table fkpart2.fk_part_1 drop constraint p_fkey;
+
 \set VERBOSITY terse	\\ -- suppress cascade details
-drop schema fkpart0, fkpart1 cascade;
+drop schema fkpart0, fkpart1, fkpart2 cascade;
 \set VERBOSITY default
-- 
2.11.0

#10Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: Amit Langote (#9)
Re: problems with foreign keys on partitioned tables

On 2019-Jan-22, Amit Langote wrote:

On 2019/01/22 8:30, Alvaro Herrera wrote:

Hi Amit,

Will you please rebase 0002? Please add your proposed tests cases to
it, too.

Done. See the attached patches for HEAD and PG 11.

I'm not quite sure I understand why the one in DefineIndex needs the
deps but nothing else does. I fear that you added that one just to
appease the existing test that breaks otherwise, and I worry that with
that addition we're papering over some other, more fundamental bug.

--
�lvaro Herrera https://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#11Amit Langote
Langote_Amit_f8@lab.ntt.co.jp
In reply to: Alvaro Herrera (#10)
2 attachment(s)
Re: problems with foreign keys on partitioned tables

Hi,

On 2019/01/24 6:13, Alvaro Herrera wrote:

On 2019-Jan-22, Amit Langote wrote:

Done. See the attached patches for HEAD and PG 11.

I'm not quite sure I understand why the one in DefineIndex needs the
deps but nothing else does. I fear that you added that one just to
appease the existing test that breaks otherwise, and I worry that with
that addition we're papering over some other, more fundamental bug.

Thinking more on this, my proposal to rip dependencies between parent and
child constraints altogether to resolve the bug I initially reported is
starting to sound a bit overambitious especially considering that we'd
need to back-patch it (the patch didn't even consider index constraints
properly, creating a divergence between the behaviors of inherited foreign
key constraints and inherited index constraints). We can pursue it if
only to avoid bloating the catalog for what can be achieved with little
bit of additional code in tablecmds.c, but maybe we should refrain from
doing it in reaction to this particular bug.

I've updated the patch that implements a much simpler fix for this
particular bug. Just to reiterate, the following illustrates the bug:

create table pk (a int primary key);
create table p (a int references pk) partition by list (a);
create table p1 partition of p for values in (1) partition by list (a);
create table p11 partition of p1 for values in (1);
alter table p detach partition p1;
alter table p1 drop constraint p_a_fkey;
ERROR: constraint "p_a_fkey" of relation "p11" does not exist

The error occurs because ATExecDropConstraint when recursively called on
p11 cannot find the constraint as the dependency mechanism already dropped
it. The new fix is to return from ATExecDropConstraint without recursing
if the constraint being dropped is index or foreign constraint.

A few hunks of the originally proposed patch are attached here as 0001,
especially the part which fixes ATAddForeignKeyConstraint to pass the
correct value of connoninherit to CreateConstraintEntry (which should be
false for partitioned tables). With that change, many tests start failing
because of the above bug. That patch also adds a test case like the one
above, but it fails along with others due to the bug. Patch 0002 is the
aforementioned simpler fix to make the errors (existing and the newly
added) go away.

These patches apply unchanged to the PG 11 branch.

Thanks,
Amit

Attachments:

0001-Fix-ATAddForeignKeyConstraint-to-use-correct-value-o.patchtext/plain; charset=UTF-8; name=0001-Fix-ATAddForeignKeyConstraint-to-use-correct-value-o.patchDownload
From 8eb5ce74e8656b517ad5a4e960b70de0ff3bedd5 Mon Sep 17 00:00:00 2001
From: amit <amitlangote09@gmail.com>
Date: Thu, 24 Jan 2019 21:29:17 +0900
Subject: [PATCH 1/2] Fix ATAddForeignKeyConstraint to use correct value of
 connoinherit

While at it, add some Asserts to ConstraintSetParentConstraint to
assert the correct value of coninhcount.

Many tests fail, but the next patch should take care of them.
---
 src/backend/catalog/pg_constraint.c       | 11 +++++++++--
 src/backend/commands/tablecmds.c          |  5 ++++-
 src/test/regress/expected/foreign_key.out | 18 ++++++++++++++++--
 src/test/regress/sql/foreign_key.sql      | 15 ++++++++++++++-
 4 files changed, 43 insertions(+), 6 deletions(-)

diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index 446b54b9ff..d2b8489b6c 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -785,6 +785,12 @@ ConstraintSetParentConstraint(Oid childConstrId, Oid parentConstrId)
 	{
 		constrForm->conislocal = false;
 		constrForm->coninhcount++;
+		/*
+		 * An inherited foreign key constraint can never have more than one
+		 * parent, because inheriting foreign keys is only allowed for
+		 * partitioning where multiple inheritance is disallowed.
+		 */
+		Assert(constrForm->coninhcount == 1);
 		constrForm->conparentid = parentConstrId;
 
 		CatalogTupleUpdate(constrRel, &tuple->t_self, newtup);
@@ -797,8 +803,9 @@ ConstraintSetParentConstraint(Oid childConstrId, Oid parentConstrId)
 	else
 	{
 		constrForm->coninhcount--;
-		if (constrForm->coninhcount <= 0)
-			constrForm->conislocal = true;
+		/* See the above comment. */
+		Assert(constrForm->coninhcount == 0);
+		constrForm->conislocal = true;
 		constrForm->conparentid = InvalidOid;
 
 		deleteDependencyRecordsForClass(ConstraintRelationId, childConstrId,
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 738c178107..fea4d99735 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -7251,6 +7251,9 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	bool		old_check_ok;
 	ObjectAddress address;
 	ListCell   *old_pfeqop_item = list_head(fkconstraint->old_conpfeqop);
+	/* Partitioned table's foreign key must always be inheritable. */
+	bool		connoinherit = (rel->rd_rel->relkind !=
+								RELKIND_PARTITIONED_TABLE);
 
 	/*
 	 * Grab ShareRowExclusiveLock on the pk table, so that someone doesn't
@@ -7616,7 +7619,7 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 									  NULL,
 									  true, /* islocal */
 									  0,	/* inhcount */
-									  true, /* isnoinherit */
+									  connoinherit, /* isnoinherit */
 									  false);	/* is_internal */
 	ObjectAddressSet(address, ConstraintRelationId, constrOid);
 
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 76040a7601..b50bafef9e 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -1945,7 +1945,21 @@ DETAIL:  Key (a)=(2) is not present in table "pkey".
 delete from fkpart1.pkey where a = 1;
 ERROR:  update or delete on table "pkey" violates foreign key constraint "fk_part_a_fkey" on table "fk_part_1"
 DETAIL:  Key (a)=(1) is still referenced from table "fk_part_1".
+-- verify that attaching and detaching partitions manipulates the inheritance
+-- properties of their FK constraints correctly
+create schema fkpart2;
+create table fkpart2.pkey (a int primary key);
+create table fkpart2.fk_part (a int, constraint fkey foreign key (a) references fkpart2.pkey) partition by list (a);
+create table fkpart2.fk_part_1 partition of fkpart2.fk_part for values in (1) partition by list (a);
+create table fkpart2.fk_part_1_1 (a int, constraint my_fkey foreign key (a) references fkpart2.pkey);
+alter table fkpart2.fk_part_1 attach partition fkpart2.fk_part_1_1 for values in (1);
+alter table fkpart2.fk_part_1 drop constraint fkey;	-- should fail
+ERROR:  cannot drop inherited constraint "fkey" of relation "fk_part_1"
+alter table fkpart2.fk_part_1_1 drop constraint my_fkey;	-- should fail
+ERROR:  cannot drop inherited constraint "my_fkey" of relation "fk_part_1_1"
+alter table fkpart2.fk_part detach partition fkpart2.fk_part_1;
+alter table fkpart2.fk_part_1 drop constraint fkey;
 \set VERBOSITY terse	\\ -- suppress cascade details
-drop schema fkpart0, fkpart1 cascade;
-NOTICE:  drop cascades to 5 other objects
+drop schema fkpart0, fkpart1, fkpart2 cascade;
+NOTICE:  drop cascades to 8 other objects
 \set VERBOSITY default
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index 9ed1166c66..c02251eaa8 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -1392,6 +1392,19 @@ create table fkpart1.fk_part_1_2 partition of fkpart1.fk_part_1 for values in (2
 insert into fkpart1.fk_part_1 values (2);	-- should fail
 delete from fkpart1.pkey where a = 1;
 
+-- verify that attaching and detaching partitions manipulates the inheritance
+-- properties of their FK constraints correctly
+create schema fkpart2;
+create table fkpart2.pkey (a int primary key);
+create table fkpart2.fk_part (a int, constraint fkey foreign key (a) references fkpart2.pkey) partition by list (a);
+create table fkpart2.fk_part_1 partition of fkpart2.fk_part for values in (1) partition by list (a);
+create table fkpart2.fk_part_1_1 (a int, constraint my_fkey foreign key (a) references fkpart2.pkey);
+alter table fkpart2.fk_part_1 attach partition fkpart2.fk_part_1_1 for values in (1);
+alter table fkpart2.fk_part_1 drop constraint fkey;	-- should fail
+alter table fkpart2.fk_part_1_1 drop constraint my_fkey;	-- should fail
+alter table fkpart2.fk_part detach partition fkpart2.fk_part_1;
+alter table fkpart2.fk_part_1 drop constraint fkey;
+
 \set VERBOSITY terse	\\ -- suppress cascade details
-drop schema fkpart0, fkpart1 cascade;
+drop schema fkpart0, fkpart1, fkpart2 cascade;
 \set VERBOSITY default
-- 
2.11.0

0002-Don-t-recurse-in-ATExecDropConstraint-for-non-CHECK-.patchtext/plain; charset=UTF-8; name=0002-Don-t-recurse-in-ATExecDropConstraint-for-non-CHECK-.patchDownload
From c2c9bd4c4976902525ebfcb0e32eb8249df549c9 Mon Sep 17 00:00:00 2001
From: amit <amitlangote09@gmail.com>
Date: Thu, 24 Jan 2019 20:42:11 +0900
Subject: [PATCH 2/2] Don't recurse in ATExecDropConstraint for non-CHECK
 constraints

---
 src/backend/commands/tablecmds.c | 12 ++++++++++++
 1 file changed, 12 insertions(+)

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index fea4d99735..82e2cf1473 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -9134,6 +9134,7 @@ ATExecDropConstraint(Relation rel, const char *constrName,
 	HeapTuple	tuple;
 	bool		found = false;
 	bool		is_no_inherit_constraint = false;
+	char		contype;
 
 	/* At top level, permission check was done in ATPrepCmd, else do it */
 	if (recursing)
@@ -9203,6 +9204,7 @@ ATExecDropConstraint(Relation rel, const char *constrName,
 		performDeletion(&conobj, behavior, 0);
 
 		found = true;
+		contype = con->contype;
 	}
 
 	systable_endscan(scan);
@@ -9227,6 +9229,16 @@ ATExecDropConstraint(Relation rel, const char *constrName,
 	}
 
 	/*
+	 * Non-CHECK inherited constraints are dropped via dependency mechanism,
+	 * so we're done here for such constraints.
+	 */
+	if (contype != CONSTRAINT_CHECK)
+	{
+		table_close(conrel, RowExclusiveLock);
+		return;
+	}
+
+	/*
 	 * Propagate to children as appropriate.  Unlike most other ALTER
 	 * routines, we have to do this one level of recursion at a time; we can't
 	 * use find_all_inheritors to do it in one pass.
-- 
2.11.0

#12Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: Amit Langote (#11)
1 attachment(s)
Re: problems with foreign keys on partitioned tables

Hello

On 2019-Jan-24, Amit Langote wrote:

Thinking more on this, my proposal to rip dependencies between parent and
child constraints altogether to resolve the bug I initially reported is
starting to sound a bit overambitious especially considering that we'd
need to back-patch it (the patch didn't even consider index constraints
properly, creating a divergence between the behaviors of inherited foreign
key constraints and inherited index constraints). We can pursue it if
only to avoid bloating the catalog for what can be achieved with little
bit of additional code in tablecmds.c, but maybe we should refrain from
doing it in reaction to this particular bug.

While studying your fix it occurred to me that perhaps we could change
things so that we first collect a list of objects to drop, and only when
we're done recursing perform the deletion, as per the attached patch.
However, this fails for the test case in your 0001 patch (but not the
one you show in your email body), because you added a stealthy extra
ingredient to it: the constraint in the grandchild has a different name,
so when ATExecDropConstraint() tries to search for it by name, it's just
not there, not because it was dropped but because it has never existed
in the first place.

Unless I misunderstand, this means that your plan to remove those
catalog tuples won't work at all, because there is no way to find those
constraints other than via pg_depend if they have different names.

I'm leaning towards the idea that your patch is the definitive fix and
not just a backpatchable band-aid.

--
�lvaro Herrera https://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

Attachments:

not-fix.patchtext/x-diff; charset=us-asciiDownload
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 747acbd2b9..29860acaa2 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -417,7 +417,7 @@ static void CloneFkReferencing(Relation pg_constraint, Relation parentRel,
 static void ATExecDropConstraint(Relation rel, const char *constrName,
 					 DropBehavior behavior,
 					 bool recurse, bool recursing,
-					 bool missing_ok, LOCKMODE lockmode);
+					 bool missing_ok, LOCKMODE lockmode, ObjectAddresses *objsToDelete);
 static void ATPrepAlterColumnType(List **wqueue,
 					  AlteredTableInfo *tab, Relation rel,
 					  bool recurse, bool recursing,
@@ -4160,12 +4160,12 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab, Relation rel,
 		case AT_DropConstraint: /* DROP CONSTRAINT */
 			ATExecDropConstraint(rel, cmd->name, cmd->behavior,
 								 false, false,
-								 cmd->missing_ok, lockmode);
+								 cmd->missing_ok, lockmode, NULL);
 			break;
 		case AT_DropConstraintRecurse:	/* DROP CONSTRAINT with recursion */
 			ATExecDropConstraint(rel, cmd->name, cmd->behavior,
 								 true, false,
-								 cmd->missing_ok, lockmode);
+								 cmd->missing_ok, lockmode, NULL);
 			break;
 		case AT_AlterColumnType:	/* ALTER COLUMN TYPE */
 			address = ATExecAlterColumnType(tab, rel, cmd, lockmode);
@@ -9219,7 +9219,8 @@ static void
 ATExecDropConstraint(Relation rel, const char *constrName,
 					 DropBehavior behavior,
 					 bool recurse, bool recursing,
-					 bool missing_ok, LOCKMODE lockmode)
+					 bool missing_ok, LOCKMODE lockmode,
+					 ObjectAddresses *objsToDelete)
 {
 	List	   *children;
 	ListCell   *child;
@@ -9237,6 +9238,12 @@ ATExecDropConstraint(Relation rel, const char *constrName,
 
 	conrel = heap_open(ConstraintRelationId, RowExclusiveLock);
 
+	if (!recursing)
+	{
+		Assert(objsToDelete == NULL);
+		objsToDelete = new_object_addresses();
+	}
+
 	/*
 	 * Find and drop the target constraint
 	 */
@@ -9290,13 +9297,13 @@ ATExecDropConstraint(Relation rel, const char *constrName,
 		}
 
 		/*
-		 * Perform the actual constraint deletion
+		 * Register the constraint for deletion
 		 */
 		conobj.classId = ConstraintRelationId;
 		conobj.objectId = HeapTupleGetOid(tuple);
 		conobj.objectSubId = 0;
 
-		performDeletion(&conobj, behavior, 0);
+		add_exact_object_address(&conobj, objsToDelete);
 
 		found = true;
 	}
@@ -9402,7 +9409,7 @@ ATExecDropConstraint(Relation rel, const char *constrName,
 				/* Time to delete this child constraint, too */
 				ATExecDropConstraint(childrel, constrName, behavior,
 									 true, true,
-									 false, lockmode);
+									 false, lockmode, objsToDelete);
 			}
 			else
 			{
@@ -9435,6 +9442,9 @@ ATExecDropConstraint(Relation rel, const char *constrName,
 		heap_close(childrel, NoLock);
 	}
 
+	if (!recursing)
+		performMultipleDeletions(objsToDelete, behavior, 0);
+
 	heap_close(conrel, RowExclusiveLock);
 }
 
#13Amit Langote
amitlangote09@gmail.com
In reply to: Alvaro Herrera (#12)
Re: problems with foreign keys on partitioned tables

On Fri, Jan 25, 2019 at 12:08 AM Alvaro Herrera
<alvherre@2ndquadrant.com> wrote:

On 2019-Jan-24, Amit Langote wrote:

Thinking more on this, my proposal to rip dependencies between parent and
child constraints altogether to resolve the bug I initially reported is
starting to sound a bit overambitious especially considering that we'd
need to back-patch it (the patch didn't even consider index constraints
properly, creating a divergence between the behaviors of inherited foreign
key constraints and inherited index constraints). We can pursue it if
only to avoid bloating the catalog for what can be achieved with little
bit of additional code in tablecmds.c, but maybe we should refrain from
doing it in reaction to this particular bug.

While studying your fix it occurred to me that perhaps we could change
things so that we first collect a list of objects to drop, and only when
we're done recursing perform the deletion, as per the attached patch.
However, this fails for the test case in your 0001 patch (but not the
one you show in your email body), because you added a stealthy extra
ingredient to it: the constraint in the grandchild has a different name,
so when ATExecDropConstraint() tries to search for it by name, it's just
not there, not because it was dropped but because it has never existed
in the first place.

Doesn't the following performDeletion() at the start of
ATExecDropConstraint(), through findDependentObject()'s own recursion,
take care of deleting *all* constraints, including those of?

/*
* Perform the actual constraint deletion
*/
conobj.classId = ConstraintRelationId;
conobj.objectId = con->oid;
conobj.objectSubId = 0;

performDeletion(&conobj, behavior, 0);

Unless I misunderstand, this means that your plan to remove those
catalog tuples won't work at all, because there is no way to find those
constraints other than via pg_depend if they have different names.

Yeah, that's right. Actually, I gave up on developing the patch
further based on that approach (no-dependencies approach) when I
edited the test to give the grandchild constraint its own name.

I'm leaning towards the idea that your patch is the definitive fix and
not just a backpatchable band-aid.

Yeah, I think so too.

Thanks,
Amit

#14Amit Langote
amitlangote09@gmail.com
In reply to: Amit Langote (#13)
Re: problems with foreign keys on partitioned tables

On Fri, Jan 25, 2019 at 12:30 AM Amit Langote <amitlangote09@gmail.com> wrote:

On Fri, Jan 25, 2019 at 12:08 AM Alvaro Herrera
<alvherre@2ndquadrant.com> wrote:

On 2019-Jan-24, Amit Langote wrote:

Thinking more on this, my proposal to rip dependencies between parent and
child constraints altogether to resolve the bug I initially reported is
starting to sound a bit overambitious especially considering that we'd
need to back-patch it (the patch didn't even consider index constraints
properly, creating a divergence between the behaviors of inherited foreign
key constraints and inherited index constraints). We can pursue it if
only to avoid bloating the catalog for what can be achieved with little
bit of additional code in tablecmds.c, but maybe we should refrain from
doing it in reaction to this particular bug.

While studying your fix it occurred to me that perhaps we could change
things so that we first collect a list of objects to drop, and only when
we're done recursing perform the deletion, as per the attached patch.
However, this fails for the test case in your 0001 patch (but not the
one you show in your email body), because you added a stealthy extra
ingredient to it: the constraint in the grandchild has a different name,
so when ATExecDropConstraint() tries to search for it by name, it's just
not there, not because it was dropped but because it has never existed
in the first place.

Doesn't the following performDeletion() at the start of
ATExecDropConstraint(), through findDependentObject()'s own recursion,
take care of deleting *all* constraints, including those of?

Meant to say: "...including those of the grandchildren?"

/*
* Perform the actual constraint deletion
*/
conobj.classId = ConstraintRelationId;
conobj.objectId = con->oid;
conobj.objectSubId = 0;

performDeletion(&conobj, behavior, 0);

Thanks,
Amit

#15Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: Amit Langote (#14)
Re: problems with foreign keys on partitioned tables

On 2019-Jan-25, Amit Langote wrote:

On Fri, Jan 25, 2019 at 12:30 AM Amit Langote <amitlangote09@gmail.com> wrote:

Doesn't the following performDeletion() at the start of
ATExecDropConstraint(), through findDependentObject()'s own recursion,
take care of deleting *all* constraints, including those of?

Meant to say: "...including those of the grandchildren?"

/*
* Perform the actual constraint deletion
*/
conobj.classId = ConstraintRelationId;
conobj.objectId = con->oid;
conobj.objectSubId = 0;

performDeletion(&conobj, behavior, 0);

Of course it does when the dependencies are set up -- but in the
approach we just gave up on, those dependencies would not exist.
Anyway, my motivation was that performMultipleDeletions has the
advantage that it collects all objects to be dropped before deleting
anyway, and so the error that a constraint was dropped in a previous
recursion step would not occur.

--
�lvaro Herrera https://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#16Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: Amit Langote (#11)
Re: problems with foreign keys on partitioned tables

On 2019-Jan-24, Amit Langote wrote:

A few hunks of the originally proposed patch are attached here as 0001,
especially the part which fixes ATAddForeignKeyConstraint to pass the
correct value of connoninherit to CreateConstraintEntry (which should be
false for partitioned tables). With that change, many tests start failing
because of the above bug. That patch also adds a test case like the one
above, but it fails along with others due to the bug. Patch 0002 is the
aforementioned simpler fix to make the errors (existing and the newly
added) go away.

Cool, thanks. I made a bunch of fixes -- I added an elog(ERROR) check
to ensure a constraint whose parent is being set does not already have a
parent; that seemed in line with the new asserts that check the
coninhcount. I also moved those asserts, changing the spirit of what
they checked. Also: I wasn't sure about stopping recursion for legacy
inheritance in ATExecDropConstraint() for non-check constraints, so I
changed that to occur in partitioned only. Also, stylistic fixes.

I was mildly surprised to realize that the my_fkey constraint on
fk_part_1_1 is gone after dropping fkey on its parent, since it was
declared locally when that table was created. However, it makes perfect
sense in retrospect, since we made it dependent on its parent. I'm not
terribly happy about this, but I don't quite see a way to make it better
that doesn't require much more code than is warranted.

These patches apply unchanged to the PG 11 branch.

Yeah, only if you tried to compile, it would have complained about
table_close() ;-)

--
�lvaro Herrera https://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#17Amit Langote
Langote_Amit_f8@lab.ntt.co.jp
In reply to: Alvaro Herrera (#16)
Re: problems with foreign keys on partitioned tables

On 2019/01/25 2:18, Alvaro Herrera wrote:

On 2019-Jan-24, Amit Langote wrote:

A few hunks of the originally proposed patch are attached here as 0001,
especially the part which fixes ATAddForeignKeyConstraint to pass the
correct value of connoninherit to CreateConstraintEntry (which should be
false for partitioned tables). With that change, many tests start failing
because of the above bug. That patch also adds a test case like the one
above, but it fails along with others due to the bug. Patch 0002 is the
aforementioned simpler fix to make the errors (existing and the newly
added) go away.

Cool, thanks. I made a bunch of fixes -- I added an elog(ERROR) check
to ensure a constraint whose parent is being set does not already have a
parent; that seemed in line with the new asserts that check the
coninhcount. I also moved those asserts, changing the spirit of what
they checked. Also: I wasn't sure about stopping recursion for legacy
inheritance in ATExecDropConstraint() for non-check constraints, so I
changed that to occur in partitioned only. Also, stylistic fixes.

Thanks for the fixes and committing.

I was mildly surprised to realize that the my_fkey constraint on
fk_part_1_1 is gone after dropping fkey on its parent, since it was
declared locally when that table was created. However, it makes perfect
sense in retrospect, since we made it dependent on its parent. I'm not
terribly happy about this, but I don't quite see a way to make it better
that doesn't require much more code than is warranted.

Fwiw, CHECK constraints behave that way too. OTOH, detaching a partition
preserves all the constraints, even the ones that were never locally
defined on the partition.

These patches apply unchanged to the PG 11 branch.

Yeah, only if you tried to compile, it would have complained about
table_close() ;-)

Oops, sorry. I was really in a hurry that day as the dinnertime had passed.

Thanks,
Amit