Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.3:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.92) (envelope-from ) id 1ldhtB-0003r4-9l for pgsql-bugs@arkaria.postgresql.org; Mon, 03 May 2021 23:22:09 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.92) (envelope-from ) id 1ldho5-00065m-Ol for pgsql-bugs@arkaria.postgresql.org; Mon, 03 May 2021 23:16:53 +0000 Received: from makus.postgresql.org ([2001:4800:3e1:1::229]) by malur.postgresql.org with esmtps (TLS1.3:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.92) (envelope-from ) id 1ldho4-00065e-IY for pgsql-bugs@lists.postgresql.org; Mon, 03 May 2021 23:16:53 +0000 Received: from [66.111.4.26] (helo=out2-smtp.messagingengine.com) by makus.postgresql.org with esmtps (TLS1.3:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.92) (envelope-from ) id 1ldho1-0007gg-Jz for pgsql-bugs@lists.postgresql.org; Mon, 03 May 2021 23:16:51 +0000 Received: from compute1.internal (compute1.nyi.internal [10.202.2.41]) by mailout.nyi.internal (Postfix) with ESMTP id 7FCDC5C00DD; Mon, 3 May 2021 19:16:37 -0400 (EDT) Received: from mailfrontend1 ([10.202.2.162]) by compute1.internal (MEProxy); Mon, 03 May 2021 19:16:37 -0400 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d= messagingengine.com; h=cc:content-transfer-encoding:content-type :date:from:in-reply-to:message-id:mime-version:subject:to :x-me-proxy:x-me-proxy:x-me-sender:x-me-sender:x-sasl-enc; s= fm2; bh=7uif97TLWFwMBvLJTuiwUJWW1K65bPiL+R5b5DvsArw=; b=FaLzCv4/ jz2Qvf6qtM4NAEYj1zilngV72W84V9unVbYp5zDQVfWKUkNLXC3smM982paqQ/5s gCLZ/MOwsDnoPdNqwzk1bq9ocvjDtg2eL9QAUSvaORpIF/WOQtVuviwZ/1hnsfbT B6TzZaRd+s3y9MP5RPk/sPN7CSER1ybkq6B6B2UCFqFde5thCrpGFZcU6+U7GxDA jRx6s4uR9feRPCNimmzPEX40pGIjbF3tWcIDiMC1sBTyerqiPNwpJDq+wNNDBiov YAKuuUFilnOjGNv88M3LQ1ppSiJ9I4gnl1vMbzrhIIoXT2zKibRjZ4cQ2yQrf1ho zo4HktgKui+KFw== X-ME-Sender: X-ME-Proxy-Cause: gggruggvucftvghtrhhoucdtuddrgeduledrvdefhedgvddtucetufdoteggodetrfdotf fvucfrrhhofhhilhgvmecuhfgrshhtofgrihhlpdfqfgfvpdfurfetoffkrfgpnffqhgen uceurghilhhouhhtmecufedttdenucenucfjughrpeffhffvuffkgggtugfgjggfsehmke erredtredunecuhfhrohhmpeetlhhvrghrohcujfgvrhhrvghrrgcuoegrlhhvhhgvrhhr vgesrghlvhhhrdhnohdqihhprdhorhhgqeenucggtffrrghtthgvrhhnpefhfeegleekgf ejueehgfefveelfeetvdehffdtueetgffhkefhudehffekudetkeenucfkphepudeltddr leehrdduledrleehnecuvehluhhsthgvrhfuihiivgeptdenucfrrghrrghmpehmrghilh hfrhhomheprghlvhhhvghrrhgvsegrlhhvhhdrnhhoqdhiphdrohhrgh X-ME-Proxy: Received: from perhan.alvh.no-ip.org (unknown [190.95.19.95]) by mail.messagingengine.com (Postfix) with ESMTPA; Mon, 3 May 2021 19:16:36 -0400 (EDT) Received: by perhan.alvh.no-ip.org (Postfix, from userid 1000) id 2CE242A0ADC; Mon, 3 May 2021 19:16:33 -0400 (-04) Date: Mon, 3 May 2021 19:16:33 -0400 From: Alvaro Herrera To: Tom Lane Cc: pgsql-bugs@lists.postgresql.org Subject: Re: ALTER CONSTRAINT on a partitioned FK isn't working Message-ID: <20210503231633.GA6994@alvherre.pgsql> MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="+HP7ph2BbKc20aGI" Content-Disposition: inline Content-Transfer-Encoding: 8bit In-Reply-To: <20210429230228.GA25949@alvherre.pgsql> User-Agent: Mutt/1.10.1 (2018-07-13) X-Host-Lookup-Failed: Reverse DNS lookup failed for 66.111.4.26 (deferred) List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk --+HP7ph2BbKc20aGI Content-Type: text/plain; charset=iso-8859-1 Content-Disposition: inline Content-Transfer-Encoding: 8bit It turns out that more work is needed here to ensure we recurse to all children on both sides, and so that all involved triggers are altered. There is a case where it doesn't seem like we can easily give non-surprising behavior: when multi-level partitioning is used, a constraint is defined in the top level and then it is altered in the middle level, then we don't have access to the action triggers, because those are defined at the parent level. I opted for adding a WARNING message in that case. Other options were 1) to raise an error if altering a constraint that's not top level, but I fear that might break restoring dumps of existing database; 2) reach up and alter the parent constraint instead, but that seems too magical. This doesn't seem worth spending too much sweat on, since it's a pretty fringe case anyway. Patch for HEAD attached. I also noticed that we're calling InvokeObjectPostAlterHook wrong in one place: when altering the trigger properties, we call it with the constraint OID instead of the trigger OID. I'll fix that in a separate commit -- that was introduced by 578b229718e8 ("Remove WITH OIDS support, change oid catalog column visibility.") in pg12. It's sad that nobody ever noticed ... looks like the sepgsql stuff isn't getting much testing. -- Álvaro Herrera 39°49'30"S 73°17'W --+HP7ph2BbKc20aGI Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="constraints.patch" diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index d9ba87a2a3..496d7da7f3 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -355,8 +355,13 @@ static void AlterIndexNamespaces(Relation classRel, Relation rel, static void AlterSeqNamespaces(Relation classRel, Relation rel, Oid oldNspOid, Oid newNspOid, ObjectAddresses *objsMoved, LOCKMODE lockmode); -static ObjectAddress ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, - bool recurse, bool recursing, LOCKMODE lockmode); +static ObjectAddress ATExecAlterConstraint(List **wqueue, AlteredTableInfo *tab, + Relation rel, AlterTableCmd *cmd, + bool recurse, bool recursing, + LOCKMODE lockmode); +static bool ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel, + Relation rel, HeapTuple contuple, List **otherrelids, + LOCKMODE lockmode); static ObjectAddress ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName, bool recurse, bool recursing, LOCKMODE lockmode); @@ -4669,6 +4674,7 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd, break; case AT_AlterConstraint: /* ALTER CONSTRAINT */ ATSimplePermissions(rel, ATT_TABLE); + /* Recursion occurs during execution phase */ pass = AT_PASS_MISC; break; case AT_ValidateConstraint: /* VALIDATE CONSTRAINT */ @@ -4967,7 +4973,7 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab, lockmode); break; case AT_AlterConstraint: /* ALTER CONSTRAINT */ - address = ATExecAlterConstraint(rel, cmd, false, false, lockmode); + address = ATExecAlterConstraint(wqueue, tab, rel, cmd, false, false, lockmode); break; case AT_ValidateConstraint: /* VALIDATE CONSTRAINT */ address = ATExecValidateConstraint(wqueue, rel, cmd->name, false, @@ -10190,28 +10196,30 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk, * Update the attributes of a constraint. * * Currently only works for Foreign Key constraints. - * Foreign keys do not inherit, so we purposely ignore the - * recursion bit here, but we keep the API the same for when - * other constraint types are supported. * * If the constraint is modified, returns its address; otherwise, return * InvalidObjectAddress. */ static ObjectAddress -ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, - bool recurse, bool recursing, LOCKMODE lockmode) +ATExecAlterConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, + AlterTableCmd *cmd, bool recurse, bool recursing, + LOCKMODE lockmode) { Constraint *cmdcon; Relation conrel; + Relation tgrel; SysScanDesc scan; ScanKeyData skey[3]; HeapTuple contuple; Form_pg_constraint currcon; ObjectAddress address; + List *otherrelids = NIL; + ListCell *lc; cmdcon = castNode(Constraint, cmd->def); conrel = table_open(ConstraintRelationId, RowExclusiveLock); + tgrel = table_open(TriggerRelationId, RowExclusiveLock); /* * Find and check the target constraint @@ -10245,21 +10253,88 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, errmsg("constraint \"%s\" of relation \"%s\" is not a foreign key constraint", cmdcon->conname, RelationGetRelationName(rel)))); + /* + * If it's not the topmost constraint, raise a warning. + * + * The problem is that altering a non-topmost constraint leaves action + * triggers untouched, since they are not directly connected to this + * constraint but rather to its parent. We would like to forbid this + * action but cannot, because existing databases might be in this state + * already and pg_dumps would become unrestorable. + */ + if (OidIsValid(currcon->conparentid)) + ereport(WARNING, + (errmsg("constraint \"%s\" of relation \"%s\" is not top-level -- proceeding anyway", + cmdcon->conname, RelationGetRelationName(rel)), + errhint("You may want to alter the constraint in the ancestor table instead."))); + + /* + * Do the actual catalog work. We can skip changing if already in the + * desired state, but not if a partitioned table: partitions need to be + * processed regardless, in case they've had it locally changed. + */ + address = InvalidObjectAddress; + if (currcon->condeferrable != cmdcon->deferrable || + currcon->condeferred != cmdcon->initdeferred || + rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + { + if (ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, rel, contuple, + &otherrelids, lockmode)) + ObjectAddressSet(address, ConstraintRelationId, currcon->oid); + } + + /* + * ATExecConstrRecurse already invalidated relcache for the relations + * having the constraint itself; we also need to invalidate relations that + * have any triggers that implement the constraint. + */ + foreach(lc, otherrelids) + CacheInvalidateRelcacheByRelid(lfirst_oid(lc)); + + systable_endscan(scan); + + table_close(tgrel, RowExclusiveLock); + table_close(conrel, RowExclusiveLock); + + return address; +} + +/* + * Recursive subroutine of ATExecAlterConstraint + * + * *otherrelids is appended OIDs of relations containing affected triggers. + * + * Note that we must recurse even when the values are correct, in case + * indirect descendants have had their constraints altered locally. + * (This could be avoided if we forbade altering constraints in partitions + * but we don't do currently do that.) + */ +static bool +ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel, + Relation rel, HeapTuple contuple, List **otherrelids, + LOCKMODE lockmode) +{ + Form_pg_constraint currcon; + Oid conoid; + Oid refrelid; + bool changed = false; + + currcon = (Form_pg_constraint) GETSTRUCT(contuple); + conoid = currcon->oid; + refrelid = currcon->confrelid; + + /* + * Update pg_constraint with the flags from cmdcon. + * + * If called to modify a constraint that's already in the desired state, + * silently do nothing. + */ if (currcon->condeferrable != cmdcon->deferrable || currcon->condeferred != cmdcon->initdeferred) { HeapTuple copyTuple; - HeapTuple tgtuple; Form_pg_constraint copy_con; - List *otherrelids = NIL; - ScanKeyData tgkey; - SysScanDesc tgscan; - Relation tgrel; - ListCell *lc; - /* - * Now update the catalog, while we have the door open. - */ copyTuple = heap_copytuple(contuple); copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple); copy_con->condeferrable = cmdcon->deferrable; @@ -10267,20 +10342,29 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, CatalogTupleUpdate(conrel, ©Tuple->t_self, copyTuple); InvokeObjectPostAlterHook(ConstraintRelationId, - currcon->oid, 0); + conoid, 0); heap_freetuple(copyTuple); + changed = true; + } - /* - * Now we need to update the multiple entries in pg_trigger that - * implement the constraint. - */ - tgrel = table_open(TriggerRelationId, RowExclusiveLock); + /* Make new constraint flags visible to others */ + /* XXX only if the constraint changed? */ + CacheInvalidateRelcache(rel); + + /* + * Now we need to update the multiple entries in pg_trigger that implement + * the constraint. + */ + { + HeapTuple tgtuple; + ScanKeyData tgkey; + SysScanDesc tgscan; ScanKeyInit(&tgkey, Anum_pg_trigger_tgconstraint, BTEqualStrategyNumber, F_OIDEQ, - ObjectIdGetDatum(currcon->oid)); + ObjectIdGetDatum(conoid)); tgscan = systable_beginscan(tgrel, TriggerConstraintIndexId, true, NULL, 1, &tgkey); @@ -10289,6 +10373,7 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, { Form_pg_trigger tgform = (Form_pg_trigger) GETSTRUCT(tgtuple); Form_pg_trigger copy_tg; + HeapTuple copyTuple; /* * Remember OIDs of other relation(s) involved in FK constraint. @@ -10297,8 +10382,8 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, * change, but let's be conservative.) */ if (tgform->tgrelid != RelationGetRelid(rel)) - otherrelids = list_append_unique_oid(otherrelids, - tgform->tgrelid); + *otherrelids = list_append_unique_oid(*otherrelids, + tgform->tgrelid); /* * Update deferrability of RI_FKey_noaction_del, @@ -10319,37 +10404,52 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, copy_tg->tginitdeferred = cmdcon->initdeferred; CatalogTupleUpdate(tgrel, ©Tuple->t_self, copyTuple); - InvokeObjectPostAlterHook(TriggerRelationId, currcon->oid, 0); + InvokeObjectPostAlterHook(TriggerRelationId, tgform->oid, 0); heap_freetuple(copyTuple); } systable_endscan(tgscan); + } - table_close(tgrel, RowExclusiveLock); + /* + * If the table at either end of the constraint is partitioned, we need to + * recurse and handle every constraint that is a child of this one. + * + * (This assumes that the recurse flag is forcibly set for partitioned + * tables, and not set for legacy inheritance, though we don't check for + * that here.) + */ + if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE || + get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE) + { + ScanKeyData pkey; + SysScanDesc pscan; + HeapTuple childtup; - /* - * Invalidate relcache so that others see the new attributes. We must - * inval both the named rel and any others having relevant triggers. - * (At present there should always be exactly one other rel, but - * there's no need to hard-wire such an assumption here.) - */ - CacheInvalidateRelcache(rel); - foreach(lc, otherrelids) + ScanKeyInit(&pkey, + Anum_pg_constraint_conparentid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(conoid)); + + pscan = systable_beginscan(conrel, ConstraintParentIndexId, + true, NULL, 1, &pkey); + + while (HeapTupleIsValid(childtup = systable_getnext(pscan))) { - CacheInvalidateRelcacheByRelid(lfirst_oid(lc)); + Form_pg_constraint childcon = (Form_pg_constraint) GETSTRUCT(childtup); + Relation childrel; + + childrel = table_open(childcon->conrelid, lockmode); + ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, childrel, childtup, + otherrelids, lockmode); + table_close(childrel, NoLock); } - ObjectAddressSet(address, ConstraintRelationId, currcon->oid); + systable_endscan(pscan); } - else - address = InvalidObjectAddress; - systable_endscan(scan); - - table_close(conrel, RowExclusiveLock); - - return address; + return changed; } /* @@ -17764,6 +17864,8 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent, * 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. + * + * XXX is this correct if the partition is partitioned? */ fkconstraint = makeNode(Constraint); fkconstraint->conname = pstrdup(NameStr(conform->conname)); diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out index 7386f4d635..efd061b857 100644 --- a/src/test/regress/expected/foreign_key.out +++ b/src/test/regress/expected/foreign_key.out @@ -2313,6 +2313,95 @@ SET CONSTRAINTS fk_a_fkey DEFERRED; DELETE FROM pk WHERE a = 1; DELETE FROM fk WHERE a = 1; COMMIT; -- OK +-- Verify constraint deferrability when changed by ALTER +-- Partitioned table at referencing end +CREATE TABLE pt(f1 int, f2 int, f3 int, PRIMARY KEY(f1,f2)); +CREATE TABLE ref(f1 int, f2 int, f3 int) + PARTITION BY list(f1); +CREATE TABLE ref1 PARTITION OF ref FOR VALUES IN (1); +CREATE TABLE ref2 PARTITION OF ref FOR VALUES in (2); +ALTER TABLE ref ADD FOREIGN KEY(f1,f2) REFERENCES pt; +ALTER TABLE ref ALTER CONSTRAINT ref_f1_f2_fkey + DEFERRABLE INITIALLY DEFERRED; +INSERT INTO pt VALUES(1,2,3); +INSERT INTO ref VALUES(1,2,3); +BEGIN; +DELETE FROM pt; +DELETE FROM ref; +ABORT; +DROP TABLE pt, ref; +-- Multi-level partitioning at referencing end +CREATE TABLE pt(f1 int, f2 int, f3 int, PRIMARY KEY(f1,f2)); +CREATE TABLE ref(f1 int, f2 int, f3 int) + PARTITION BY list(f1); +CREATE TABLE ref1_2 PARTITION OF ref FOR VALUES IN (1, 2) PARTITION BY list (f2); +CREATE TABLE ref1 PARTITION OF ref1_2 FOR VALUES IN (1); +CREATE TABLE ref2 PARTITION OF ref1_2 FOR VALUES IN (2); +ALTER TABLE ref ADD FOREIGN KEY(f1,f2) REFERENCES pt; +INSERT INTO pt VALUES(1,2,3), (1,1,3); +INSERT INTO ref VALUES(1,2,3), (1,1,3); +SELECT tableoid::regclass, * from ref; + tableoid | f1 | f2 | f3 +----------+----+----+---- + ref1 | 1 | 1 | 3 + ref2 | 1 | 2 | 3 +(2 rows) + +ALTER TABLE ref1_2 ALTER CONSTRAINT ref_f1_f2_fkey + DEFERRABLE INITIALLY DEFERRED; +WARNING: constraint "ref_f1_f2_fkey" of relation "ref1_2" is not top-level -- proceeding anyway +HINT: You may want to alter the constraint in the ancestor table instead. +ALTER TABLE ref2 ALTER CONSTRAINT ref_f1_f2_fkey + DEFERRABLE INITIALLY IMMEDIATE; +WARNING: constraint "ref_f1_f2_fkey" of relation "ref2" is not top-level -- proceeding anyway +HINT: You may want to alter the constraint in the ancestor table instead. +BEGIN; +DELETE FROM pt; -- fails +ERROR: update or delete on table "pt" violates foreign key constraint "ref_f1_f2_fkey" on table "ref" +DETAIL: Key (f1, f2)=(1, 2) is still referenced from table "ref". +DELETE FROM ref; +ERROR: current transaction is aborted, commands ignored until end of transaction block +ABORT; +ALTER TABLE ref ALTER CONSTRAINT ref_f1_f2_fkey + DEFERRABLE INITIALLY DEFERRED; +BEGIN; +DELETE FROM pt; +DELETE FROM ref; +ABORT; +DROP TABLE pt, ref; +-- Partitioned table at referenced end +CREATE TABLE pt(f1 int, f2 int, f3 int, PRIMARY KEY(f1,f2)) + PARTITION BY LIST(f1); +CREATE TABLE pt1 PARTITION OF pt FOR VALUES IN (1); +CREATE TABLE pt2 PARTITION OF pt FOR VALUES IN (2); +CREATE TABLE ref(f1 int, f2 int, f3 int); +ALTER TABLE ref ADD FOREIGN KEY(f1,f2) REFERENCES pt; +ALTER TABLE ref ALTER CONSTRAINT ref_f1_f2_fkey + DEFERRABLE INITIALLY DEFERRED; +INSERT INTO pt VALUES(1,2,3); +INSERT INTO ref VALUES(1,2,3); +BEGIN; +DELETE FROM pt; +DELETE FROM ref; +ABORT; +DROP TABLE pt, ref; +-- Multi-level partitioning at at referenced end +CREATE TABLE pt(f1 int, f2 int, f3 int, PRIMARY KEY(f1,f2)) + PARTITION BY LIST(f1); +CREATE TABLE pt1_2 PARTITION OF pt FOR VALUES IN (1, 2) PARTITION BY LIST (f1); +CREATE TABLE pt1 PARTITION OF pt1_2 FOR VALUES IN (1); +CREATE TABLE pt2 PARTITION OF pt1_2 FOR VALUES IN (2); +CREATE TABLE ref(f1 int, f2 int, f3 int); +ALTER TABLE ref ADD FOREIGN KEY(f1,f2) REFERENCES pt; +ALTER TABLE ref ALTER CONSTRAINT ref_f1_f2_fkey + DEFERRABLE INITIALLY DEFERRED; +INSERT INTO pt VALUES(1,2,3); +INSERT INTO ref VALUES(1,2,3); +BEGIN; +DELETE FROM pt; +DELETE FROM ref; +ABORT; +DROP TABLE pt, ref; DROP SCHEMA fkpart9 CASCADE; NOTICE: drop cascades to 2 other objects DETAIL: drop cascades to table pk diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql index 67aa20435d..124993263d 100644 --- a/src/test/regress/sql/foreign_key.sql +++ b/src/test/regress/sql/foreign_key.sql @@ -1636,6 +1636,85 @@ SET CONSTRAINTS fk_a_fkey DEFERRED; DELETE FROM pk WHERE a = 1; DELETE FROM fk WHERE a = 1; COMMIT; -- OK + +-- Verify constraint deferrability when changed by ALTER +-- Partitioned table at referencing end +CREATE TABLE pt(f1 int, f2 int, f3 int, PRIMARY KEY(f1,f2)); +CREATE TABLE ref(f1 int, f2 int, f3 int) + PARTITION BY list(f1); +CREATE TABLE ref1 PARTITION OF ref FOR VALUES IN (1); +CREATE TABLE ref2 PARTITION OF ref FOR VALUES in (2); +ALTER TABLE ref ADD FOREIGN KEY(f1,f2) REFERENCES pt; +ALTER TABLE ref ALTER CONSTRAINT ref_f1_f2_fkey + DEFERRABLE INITIALLY DEFERRED; +INSERT INTO pt VALUES(1,2,3); +INSERT INTO ref VALUES(1,2,3); +BEGIN; +DELETE FROM pt; +DELETE FROM ref; +ABORT; +DROP TABLE pt, ref; +-- Multi-level partitioning at referencing end +CREATE TABLE pt(f1 int, f2 int, f3 int, PRIMARY KEY(f1,f2)); +CREATE TABLE ref(f1 int, f2 int, f3 int) + PARTITION BY list(f1); +CREATE TABLE ref1_2 PARTITION OF ref FOR VALUES IN (1, 2) PARTITION BY list (f2); +CREATE TABLE ref1 PARTITION OF ref1_2 FOR VALUES IN (1); +CREATE TABLE ref2 PARTITION OF ref1_2 FOR VALUES IN (2); +ALTER TABLE ref ADD FOREIGN KEY(f1,f2) REFERENCES pt; +INSERT INTO pt VALUES(1,2,3), (1,1,3); +INSERT INTO ref VALUES(1,2,3), (1,1,3); +SELECT tableoid::regclass, * from ref; +ALTER TABLE ref1_2 ALTER CONSTRAINT ref_f1_f2_fkey + DEFERRABLE INITIALLY DEFERRED; +ALTER TABLE ref2 ALTER CONSTRAINT ref_f1_f2_fkey + DEFERRABLE INITIALLY IMMEDIATE; +BEGIN; +DELETE FROM pt; -- fails +DELETE FROM ref; +ABORT; +ALTER TABLE ref ALTER CONSTRAINT ref_f1_f2_fkey + DEFERRABLE INITIALLY DEFERRED; +BEGIN; +DELETE FROM pt; +DELETE FROM ref; +ABORT; +DROP TABLE pt, ref; + +-- Partitioned table at referenced end +CREATE TABLE pt(f1 int, f2 int, f3 int, PRIMARY KEY(f1,f2)) + PARTITION BY LIST(f1); +CREATE TABLE pt1 PARTITION OF pt FOR VALUES IN (1); +CREATE TABLE pt2 PARTITION OF pt FOR VALUES IN (2); +CREATE TABLE ref(f1 int, f2 int, f3 int); +ALTER TABLE ref ADD FOREIGN KEY(f1,f2) REFERENCES pt; +ALTER TABLE ref ALTER CONSTRAINT ref_f1_f2_fkey + DEFERRABLE INITIALLY DEFERRED; +INSERT INTO pt VALUES(1,2,3); +INSERT INTO ref VALUES(1,2,3); +BEGIN; +DELETE FROM pt; +DELETE FROM ref; +ABORT; +DROP TABLE pt, ref; +-- Multi-level partitioning at at referenced end +CREATE TABLE pt(f1 int, f2 int, f3 int, PRIMARY KEY(f1,f2)) + PARTITION BY LIST(f1); +CREATE TABLE pt1_2 PARTITION OF pt FOR VALUES IN (1, 2) PARTITION BY LIST (f1); +CREATE TABLE pt1 PARTITION OF pt1_2 FOR VALUES IN (1); +CREATE TABLE pt2 PARTITION OF pt1_2 FOR VALUES IN (2); +CREATE TABLE ref(f1 int, f2 int, f3 int); +ALTER TABLE ref ADD FOREIGN KEY(f1,f2) REFERENCES pt; +ALTER TABLE ref ALTER CONSTRAINT ref_f1_f2_fkey + DEFERRABLE INITIALLY DEFERRED; +INSERT INTO pt VALUES(1,2,3); +INSERT INTO ref VALUES(1,2,3); +BEGIN; +DELETE FROM pt; +DELETE FROM ref; +ABORT; +DROP TABLE pt, ref; + DROP SCHEMA fkpart9 CASCADE; -- Verify ON UPDATE/DELETE behavior --+HP7ph2BbKc20aGI--