[BUG] Fix DETACH with FK pointing to a partitioned table fails
Hi,
(patch proposal below).
Consider a table with a FK pointing to a partitioned table.
CREATE TABLE p ( id bigint PRIMARY KEY )
PARTITION BY list (id);
CREATE TABLE p_1 PARTITION OF p FOR VALUES IN (1);
CREATE TABLE r_1 (
id bigint PRIMARY KEY,
p_id bigint NOT NULL,
FOREIGN KEY (p_id) REFERENCES p (id)
);
Now, attach this table "refg_1" as partition of another one having the same FK:
CREATE TABLE r (
id bigint PRIMARY KEY,
p_id bigint NOT NULL,
FOREIGN KEY (p_id) REFERENCES p (id)
) PARTITION BY list (id);
ALTER TABLE r ATTACH PARTITION r_1 FOR VALUES IN (1);
The old sub-FKs (below 18289) created in this table to enforce the action
triggers on referenced partitions are not deleted when the table becomes a
partition. Because of this, we have additional and useless triggers on the
referenced partitions and we can not DETACH this partition on the referencing
side anymore:
=> ALTER TABLE r DETACH PARTITION r_1;
ERROR: could not find ON INSERT check triggers of foreign key
constraint 18289
=> SELECT c.oid, conparentid,
conrelid::regclass,
confrelid::regclass,
t.tgfoid::regproc
FROM pg_constraint c
JOIN pg_trigger t ON t.tgconstraint = c.oid
WHERE confrelid::regclass = 'p_1'::regclass;
oid │ conparentid │ conrelid │ confrelid │ tgfoid
───────┼─────────────┼──────────┼───────────┼────────────────────────
18289 │ 18286 │ r_1 │ p_1 │ "RI_FKey_noaction_del"
18289 │ 18286 │ r_1 │ p_1 │ "RI_FKey_noaction_upd"
18302 │ 18299 │ r │ p_1 │ "RI_FKey_noaction_del"
18302 │ 18299 │ r │ p_1 │ "RI_FKey_noaction_upd"
(4 rows)
The legitimate constraint and triggers here are 18302. The old sub-FK
18289 having 18286 as parent should have gone during the ATTACH PARTITION.
Please, find in attachment a patch dropping old "sub-FK" during the ATTACH
PARTITION command and adding a regression test about it. At the very least, it
help understanding the problem and sketch a possible solution.
Regards,
Attachments:
v1-0001-Remove-useless-parted-FK-constraints-when-attachi.patchtext/x-patchDownload
From 5fc7997b9f9a17ee5a31f059c18e6c01fd716c04 Mon Sep 17 00:00:00 2001
From: Jehan-Guillaume de Rorthais <jgdr@dalibo.com>
Date: Wed, 5 Jul 2023 19:19:40 +0200
Subject: [PATCH v1] Remove useless parted-FK constraints when attaching a
partition
When a FK is referencing a partitioned table, this FK is parenting
as many other "parted-FK" constraints than the referencing side has
partitions. Each of these sub-constraints enforce only the
UPDATE/DELETE triggers on each referenced partition, but not the
check triggers on the referencing partition as this is already
handle by the parent constraint. These parted-FK are half-backed on
purpose.
However when attaching such standalone table to a partitioned table
having the same FK definition, all these sub-constraints were not
removed. This leave a useless action trigger on each referenced
partition, the legitimate one already checking against the root of
the referencing partition.
Moreover, when the partition is later detached,
DetachPartitionFinalize() look for all existing FK on the relation
and try to detach them from the parent triggers and constraints.
But because these parted-FK are half backed, calling
GetForeignKeyCheckTriggers() to later detach the check triggers
raise an ERROR:
ERROR: could not find ON INSERT check triggers of foreign key
constraint NNNNN.
---
src/backend/commands/tablecmds.c | 57 +++++++++++++++++++++--
src/test/regress/expected/foreign_key.out | 22 +++++++++
src/test/regress/sql/foreign_key.sql | 27 +++++++++++
3 files changed, 103 insertions(+), 3 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index fce5e6f220..7c1aa5d395 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -10566,9 +10566,11 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
Form_pg_constraint parentConstr;
HeapTuple partcontup;
Form_pg_constraint partConstr;
- ScanKeyData key;
+ ScanKeyData key[3];
SysScanDesc scan;
HeapTuple trigtup;
+ HeapTuple contup;
+ Relation pg_constraint;
Oid insertTriggerOid,
updateTriggerOid;
@@ -10631,12 +10633,12 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
* in the partition. We identify them because they have our constraint
* OID, as well as being on the referenced rel.
*/
- ScanKeyInit(&key,
+ ScanKeyInit(key,
Anum_pg_trigger_tgconstraint,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(fk->conoid));
scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
- NULL, 1, &key);
+ NULL, 1, key);
while ((trigtup = systable_getnext(scan)) != NULL)
{
Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
@@ -10684,6 +10686,55 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
partRelid);
+ /*
+ * If the referenced side of the FK is partionned, we need to remove the
+ * action triggers from these partitions as well as a legitimate one
+ * already exists and points to the root of the referencing partition.
+ * These action triggers to remove are binded to a FK constraints having
+ * this constraint as parent. Dropping these constraints cleans up
+ * everything.
+ */
+ pg_constraint = table_open(ConstraintRelationId, RowShareLock);
+ ScanKeyInit(&key[0],
+ Anum_pg_constraint_conrelid, BTEqualStrategyNumber,
+ F_OIDEQ, ObjectIdGetDatum(fk->conrelid));
+ ScanKeyInit(&key[1],
+ Anum_pg_constraint_conparentid, BTEqualStrategyNumber,
+ F_CHAREQ, ObjectIdGetDatum(fk->conoid));
+ ScanKeyInit(&key[2],
+ Anum_pg_constraint_contype, BTEqualStrategyNumber,
+ F_CHAREQ, CharGetDatum(CONSTRAINT_FOREIGN));
+ scan = systable_beginscan(pg_constraint, InvalidOid, true,
+ NULL, 2, key);
+
+ while ((contup = systable_getnext(scan)) != NULL)
+ {
+ ObjectAddress conobj;
+ Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(contup);
+
+ /*
+ * we need to remove the dependency between this old half-backend
+ * foreign key constraint and the main one so we can later drop
+ * it.
+ */
+ deleteDependencyRecordsFor(ConstraintRelationId,
+ con->oid,
+ false);
+
+ /* make dependency deletion visible to performDeletion */
+ CommandCounterIncrement();
+
+ conobj.classId = ConstraintRelationId;
+ conobj.objectId = con->oid;
+ conobj.objectSubId = 0;
+
+ /* drop the old constraint */
+ performDeletion(&conobj, DROP_CASCADE, 0);
+ }
+ systable_endscan(scan);
+ table_close(pg_constraint, RowShareLock);
+
+
CommandCounterIncrement();
return true;
}
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 55f7158c1a..1595739cf7 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -2917,3 +2917,25 @@ DETAIL: drop cascades to table fkpart11.pk
drop cascades to table fkpart11.fk_parted
drop cascades to table fkpart11.fk_another
drop cascades to function fkpart11.print_row()
+-- Test partitioned tables on both ends of the referential constraint
+-- test attaching and detaching a partition with an existing
+-- foreign key constraint
+BEGIN;
+CREATE TABLE parted_refd (
+ id bigint,
+ PRIMARY KEY (id)
+)
+PARTITION BY list (id);
+CREATE TABLE parted_refd_1 PARTITION OF parted_refd FOR VALUES IN (1);
+CREATE TABLE parted_refg (
+ id bigint,
+ p_id bigint NOT NULL,
+ PRIMARY KEY (id),
+ FOREIGN KEY (p_id) REFERENCES parted_refd (id)
+)
+PARTITION BY list (id);
+CREATE TABLE parted_refg_1 ( LIKE parted_refg INCLUDING ALL );
+ALTER TABLE parted_refg_1 ADD FOREIGN KEY (p_id) REFERENCES parted_refd (id);
+ALTER TABLE parted_refg ATTACH PARTITION parted_refg_1 FOR VALUES IN (1);
+ALTER TABLE parted_refg DETACH PARTITION parted_refg_1;
+ROLLBACK;
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index 22e177f89b..02828d3e94 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -2069,3 +2069,30 @@ UPDATE fkpart11.pk SET a = 3 WHERE a = 4;
UPDATE fkpart11.pk SET a = 1 WHERE a = 2;
DROP SCHEMA fkpart11 CASCADE;
+
+-- Test partitioned tables on both ends of the referential constraint
+
+-- test attaching and detaching a partition with an existing
+-- foreign key constraint
+BEGIN;
+CREATE TABLE parted_refd (
+ id bigint,
+ PRIMARY KEY (id)
+)
+PARTITION BY list (id);
+CREATE TABLE parted_refd_1 PARTITION OF parted_refd FOR VALUES IN (1);
+
+CREATE TABLE parted_refg (
+ id bigint,
+ p_id bigint NOT NULL,
+ PRIMARY KEY (id),
+ FOREIGN KEY (p_id) REFERENCES parted_refd (id)
+)
+PARTITION BY list (id);
+
+CREATE TABLE parted_refg_1 ( LIKE parted_refg INCLUDING ALL );
+ALTER TABLE parted_refg_1 ADD FOREIGN KEY (p_id) REFERENCES parted_refd (id);
+
+ALTER TABLE parted_refg ATTACH PARTITION parted_refg_1 FOR VALUES IN (1);
+ALTER TABLE parted_refg DETACH PARTITION parted_refg_1;
+ROLLBACK;
--
2.40.1
On 2023-Jul-05, Jehan-Guillaume de Rorthais wrote:
ALTER TABLE r ATTACH PARTITION r_1 FOR VALUES IN (1);
The old sub-FKs (below 18289) created in this table to enforce the action
triggers on referenced partitions are not deleted when the table becomes a
partition. Because of this, we have additional and useless triggers on the
referenced partitions and we can not DETACH this partition on the referencing
side anymore:
Oh, hm, interesting. Thanks for the report and patch. I found a couple
of minor issues with it (most serious one: nkeys should be 3, not 2;
also sysscan should use conrelid index), but I'll try and complete it so
that it's ready for 2023-08-10's releases.
Regards
--
Álvaro Herrera 48°01'N 7°57'E — https://www.EnterpriseDB.com/
I think old "sub-FK" should not be dropped, that will be violates foreign
key constraint. For example :
postgres=# insert into r values(1,1);
INSERT 0 1
postgres=# ALTER TABLE r DETACH PARTITION r_1;
ALTER TABLE
postgres=# delete from p_1 where id = 1;
DELETE 1
postgres=# select * from r_1;
id | p_id
----+------
1 | 1
(1 row)
If I run above SQLs on pg12.12, it will report error below:
postgres=# delete from p_1 where id = 1;
ERROR: update or delete on table "p_1" violates foreign key constraint
"r_1_p_id_fkey1" on table "r_1"
DETAIL: Key (id)=(1) is still referenced from table "r_1".
Alvaro Herrera <alvherre@alvh.no-ip.org> 于2023年7月31日周一 20:58写道:
Show quoted text
On 2023-Jul-05, Jehan-Guillaume de Rorthais wrote:
ALTER TABLE r ATTACH PARTITION r_1 FOR VALUES IN (1);
The old sub-FKs (below 18289) created in this table to enforce the action
triggers on referenced partitions are not deleted when the table becomesa
partition. Because of this, we have additional and useless triggers on
the
referenced partitions and we can not DETACH this partition on the
referencing
side anymore:
Oh, hm, interesting. Thanks for the report and patch. I found a couple
of minor issues with it (most serious one: nkeys should be 3, not 2;
also sysscan should use conrelid index), but I'll try and complete it so
that it's ready for 2023-08-10's releases.Regards
--
Álvaro Herrera 48°01'N 7°57'E —
https://www.EnterpriseDB.com/
On 2023-Aug-03, tender wang wrote:
I think old "sub-FK" should not be dropped, that will be violates foreign
key constraint.
Yeah, I've been playing more with the patch and it is definitely not
doing the right things. Just eyeballing the contents of pg_trigger and
pg_constraint for partitions added by ALTER...ATTACH shows that the
catalog contents are inconsistent with those added by CREATE TABLE
PARTITION OF.
--
Álvaro Herrera PostgreSQL Developer — https://www.EnterpriseDB.com/
I think the code to determine that fk of a partition is inherited or not is
not enough.
For example, in this case, foreign key r_1_p_id_fkey1 is not inherited
from parent.
If conform->conparentid(in DetachPartitionFinalize func) is valid, we
should recheck confrelid(pg_constraint) field.
I try to fix this problem in the attached patch.
Any thoughts.
Alvaro Herrera <alvherre@alvh.no-ip.org> 于2023年8月3日周四 17:02写道:
Show quoted text
On 2023-Aug-03, tender wang wrote:
I think old "sub-FK" should not be dropped, that will be violates
foreign
key constraint.
Yeah, I've been playing more with the patch and it is definitely not
doing the right things. Just eyeballing the contents of pg_trigger and
pg_constraint for partitions added by ALTER...ATTACH shows that the
catalog contents are inconsistent with those added by CREATE TABLE
PARTITION OF.--
Álvaro Herrera PostgreSQL Developer —
https://www.EnterpriseDB.com/
Attachments:
0001-Recheck-foreign-key-of-a-partition-is-inherited-from.patchtext/plain; charset=US-ASCII; name=0001-Recheck-foreign-key-of-a-partition-is-inherited-from.patchDownload
From d84395c7321c201a78661de0b41b76e71ab10678 Mon Sep 17 00:00:00 2001
From: "tender.wang" <tender.wang@openpie.com>
Date: Thu, 3 Aug 2023 17:23:06 +0800
Subject: [PATCH] Recheck foreign key of a partition is inherited from parent.
Previously, fk is inherited if conparentid(pg_constraint) is valid.
It is not enough and we should compare confrelid field to determine
fk is inherited.
---
src/backend/commands/tablecmds.c | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 727f151750..1447433109 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -18556,6 +18556,8 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
Form_pg_constraint conform;
+ HeapTuple parentTup;
+ Form_pg_constraint parentForm;
Constraint *fkconstraint;
Oid insertTriggerOid,
updateTriggerOid;
@@ -18573,6 +18575,23 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
continue;
}
+ /* recheck confrelid field */
+ if (OidIsValid(conform->conparentid))
+ {
+ parentTup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(conform->conparentid));
+ if (!HeapTupleIsValid(parentTup))
+ elog(ERROR, "cache lookup failed for constraint %u", conform->conparentid);
+ parentForm = (Form_pg_constraint) GETSTRUCT(parentTup);
+ /* It is not inherited foreign keys */
+ if (parentForm->confrelid != conform->confrelid)
+ {
+ ReleaseSysCache(contup);
+ ReleaseSysCache(parentTup);
+ continue;
+ }
+ ReleaseSysCache(parentTup);
+ }
+
/* unset conparentid and adjust conislocal, coninhcount, etc. */
ConstraintSetParentConstraint(fk->conoid, InvalidOid, InvalidOid);
--
2.25.1
Oversight the DetachPartitionFinalize(), I found another bug below:
postgres=# CREATE TABLE p ( id bigint PRIMARY KEY ) PARTITION BY list (id);
CREATE TABLE
postgres=# CREATE TABLE p_1 PARTITION OF p FOR VALUES IN (1);
CREATE TABLE
postgres=# CREATE TABLE r_1 (
postgres(# id bigint PRIMARY KEY,
postgres(# p_id bigint NOT NULL
postgres(# );
CREATE TABLE
postgres=# CREATE TABLE r (
postgres(# id bigint PRIMARY KEY,
postgres(# p_id bigint NOT NULL,
postgres(# FOREIGN KEY (p_id) REFERENCES p (id)
postgres(# ) PARTITION BY list (id);
CREATE TABLE
postgres=# ALTER TABLE r ATTACH PARTITION r_1 FOR VALUES IN (1);
ALTER TABLE
postgres=# ALTER TABLE r DETACH PARTITION r_1;
ALTER TABLE
postgres=# insert into r_1 values(1,1);
ERROR: insert or update on table "r_1" violates foreign key constraint
"r_p_id_fkey"
DETAIL: Key (p_id)=(1) is not present in table "p".
After detach operation, r_1 is normal relation and the inherited foreign
key 'r_p_id_fkey' should be removed.
tender wang <tndrwang@gmail.com> 于2023年8月3日周四 17:34写道:
Show quoted text
I think the code to determine that fk of a partition is inherited or not
is not enough.
For example, in this case, foreign key r_1_p_id_fkey1 is not inherited
from parent.If conform->conparentid(in DetachPartitionFinalize func) is valid, we
should recheck confrelid(pg_constraint) field.I try to fix this problem in the attached patch.
Any thoughts.Alvaro Herrera <alvherre@alvh.no-ip.org> 于2023年8月3日周四 17:02写道:
On 2023-Aug-03, tender wang wrote:
I think old "sub-FK" should not be dropped, that will be violates
foreign
key constraint.
Yeah, I've been playing more with the patch and it is definitely not
doing the right things. Just eyeballing the contents of pg_trigger and
pg_constraint for partitions added by ALTER...ATTACH shows that the
catalog contents are inconsistent with those added by CREATE TABLE
PARTITION OF.--
Álvaro Herrera PostgreSQL Developer —
https://www.EnterpriseDB.com/
Oversight the DetachPartitionFinalize() again, I found the root cause why
'r_p_id_fkey' wat not removed.
DetachPartitionFinalize() call the GetParentedForeignKeyRefs() func to get
tuple from pg_constraint that will be delete but failed.
according to the comments, the GetParentedForeignKeyRefs() func get the
tuple reference me not I reference others.
I try to fix this bug :
i. ConstraintSetParentConstraint() should not be called in
DetachPartitionFinalize(), because after conparentid was set to 0,
we can not find inherited foreign keys.
ii. create another function like GetParentedForeignKeyRefs(), but the
ScanKey should be conrelid field not confrelid.
I quickly test on my above solution in my env, can be solve above issue.
tender wang <tndrwang@gmail.com> 于2023年8月4日周五 17:04写道:
Show quoted text
Oversight the DetachPartitionFinalize(), I found another bug below:
postgres=# CREATE TABLE p ( id bigint PRIMARY KEY ) PARTITION BY list (id);
CREATE TABLE
postgres=# CREATE TABLE p_1 PARTITION OF p FOR VALUES IN (1);
CREATE TABLE
postgres=# CREATE TABLE r_1 (
postgres(# id bigint PRIMARY KEY,
postgres(# p_id bigint NOT NULL
postgres(# );
CREATE TABLE
postgres=# CREATE TABLE r (
postgres(# id bigint PRIMARY KEY,
postgres(# p_id bigint NOT NULL,
postgres(# FOREIGN KEY (p_id) REFERENCES p (id)
postgres(# ) PARTITION BY list (id);
CREATE TABLE
postgres=# ALTER TABLE r ATTACH PARTITION r_1 FOR VALUES IN (1);
ALTER TABLE
postgres=# ALTER TABLE r DETACH PARTITION r_1;
ALTER TABLE
postgres=# insert into r_1 values(1,1);
ERROR: insert or update on table "r_1" violates foreign key constraint
"r_p_id_fkey"
DETAIL: Key (p_id)=(1) is not present in table "p".After detach operation, r_1 is normal relation and the inherited foreign
key 'r_p_id_fkey' should be removed.tender wang <tndrwang@gmail.com> 于2023年8月3日周四 17:34写道:
I think the code to determine that fk of a partition is inherited or not
is not enough.
For example, in this case, foreign key r_1_p_id_fkey1 is not inherited
from parent.If conform->conparentid(in DetachPartitionFinalize func) is valid, we
should recheck confrelid(pg_constraint) field.I try to fix this problem in the attached patch.
Any thoughts.Alvaro Herrera <alvherre@alvh.no-ip.org> 于2023年8月3日周四 17:02写道:
On 2023-Aug-03, tender wang wrote:
I think old "sub-FK" should not be dropped, that will be violates
foreign
key constraint.
Yeah, I've been playing more with the patch and it is definitely not
doing the right things. Just eyeballing the contents of pg_trigger and
pg_constraint for partitions added by ALTER...ATTACH shows that the
catalog contents are inconsistent with those added by CREATE TABLE
PARTITION OF.--
Álvaro Herrera PostgreSQL Developer —
https://www.EnterpriseDB.com/
The foreign key still works even though partition was detached. Is this
behavior expected?
I can't find the answer in the document. If it is expected behavior ,
please ignore the bug I reported a few days ago.
tender wang <tndrwang@gmail.com> 于2023年8月4日周五 17:04写道:
Show quoted text
Oversight the DetachPartitionFinalize(), I found another bug below:
postgres=# CREATE TABLE p ( id bigint PRIMARY KEY ) PARTITION BY list (id);
CREATE TABLE
postgres=# CREATE TABLE p_1 PARTITION OF p FOR VALUES IN (1);
CREATE TABLE
postgres=# CREATE TABLE r_1 (
postgres(# id bigint PRIMARY KEY,
postgres(# p_id bigint NOT NULL
postgres(# );
CREATE TABLE
postgres=# CREATE TABLE r (
postgres(# id bigint PRIMARY KEY,
postgres(# p_id bigint NOT NULL,
postgres(# FOREIGN KEY (p_id) REFERENCES p (id)
postgres(# ) PARTITION BY list (id);
CREATE TABLE
postgres=# ALTER TABLE r ATTACH PARTITION r_1 FOR VALUES IN (1);
ALTER TABLE
postgres=# ALTER TABLE r DETACH PARTITION r_1;
ALTER TABLE
postgres=# insert into r_1 values(1,1);
ERROR: insert or update on table "r_1" violates foreign key constraint
"r_p_id_fkey"
DETAIL: Key (p_id)=(1) is not present in table "p".After detach operation, r_1 is normal relation and the inherited foreign
key 'r_p_id_fkey' should be removed.tender wang <tndrwang@gmail.com> 于2023年8月3日周四 17:34写道:
I think the code to determine that fk of a partition is inherited or not
is not enough.
For example, in this case, foreign key r_1_p_id_fkey1 is not inherited
from parent.If conform->conparentid(in DetachPartitionFinalize func) is valid, we
should recheck confrelid(pg_constraint) field.I try to fix this problem in the attached patch.
Any thoughts.Alvaro Herrera <alvherre@alvh.no-ip.org> 于2023年8月3日周四 17:02写道:
On 2023-Aug-03, tender wang wrote:
I think old "sub-FK" should not be dropped, that will be violates
foreign
key constraint.
Yeah, I've been playing more with the patch and it is definitely not
doing the right things. Just eyeballing the contents of pg_trigger and
pg_constraint for partitions added by ALTER...ATTACH shows that the
catalog contents are inconsistent with those added by CREATE TABLE
PARTITION OF.--
Álvaro Herrera PostgreSQL Developer —
https://www.EnterpriseDB.com/
On 2023-Aug-07, tender wang wrote:
The foreign key still works even though partition was detached. Is this
behavior expected?
Well, there's no reason for it not to, right? For example, if you
detach a partition and then attach it again, you don't have to scan the
partition on attach, because you know the constraint has remained valid
all along.
--
Álvaro Herrera 48°01'N 7°57'E — https://www.EnterpriseDB.com/
"In fact, the basic problem with Perl 5's subroutines is that they're not
crufty enough, so the cruft leaks out into user-defined code instead, by
the Conservation of Cruft Principle." (Larry Wall, Apocalypse 6)
On Thu, 3 Aug 2023 11:02:43 +0200
Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
On 2023-Aug-03, tender wang wrote:
I think old "sub-FK" should not be dropped, that will be violates foreign
key constraint.Yeah, I've been playing more with the patch and it is definitely not
doing the right things. Just eyeballing the contents of pg_trigger and
pg_constraint for partitions added by ALTER...ATTACH shows that the
catalog contents are inconsistent with those added by CREATE TABLE
PARTITION OF.
Well, as stated in my orignal message, at the patch helps understanding the
problem and sketch a possible solution. It definitely is not complete.
After DETACHing the table, we surely needs to check everything again and
recreating what is needed to keep the FK consistent.
But should we keep the FK after DETACH? Did you check the two other discussions
related to FK, self-FK & partition? Unfortunately, as Tender experienced, the
more we dig the more we find bugs. Moreover, the second one might seems
unsolvable and deserve a closer look. See:
* FK broken after DETACHing referencing part
/messages/by-id/20230420144344.40744130@karst
* Issue attaching a table to a partitioned table with an auto-referenced
foreign key
/messages/by-id/20230707175859.17c91538@karst
Hi
Is there any conclusion to this issue?
Jehan-Guillaume de Rorthais <jgdr@dalibo.com> 于2023年8月10日周四 23:03写道:
Show quoted text
On Thu, 3 Aug 2023 11:02:43 +0200
Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:On 2023-Aug-03, tender wang wrote:
I think old "sub-FK" should not be dropped, that will be violates
foreign
key constraint.
Yeah, I've been playing more with the patch and it is definitely not
doing the right things. Just eyeballing the contents of pg_trigger and
pg_constraint for partitions added by ALTER...ATTACH shows that the
catalog contents are inconsistent with those added by CREATE TABLE
PARTITION OF.Well, as stated in my orignal message, at the patch helps understanding the
problem and sketch a possible solution. It definitely is not complete.After DETACHing the table, we surely needs to check everything again and
recreating what is needed to keep the FK consistent.But should we keep the FK after DETACH? Did you check the two other
discussions
related to FK, self-FK & partition? Unfortunately, as Tender experienced,
the
more we dig the more we find bugs. Moreover, the second one might seems
unsolvable and deserve a closer look. See:* FK broken after DETACHing referencing part
/messages/by-id/20230420144344.40744130@karst
* Issue attaching a table to a partitioned table with an auto-referenced
foreign key
/messages/by-id/20230707175859.17c91538@karst
On 2023-Oct-25, tender wang wrote:
Hi
Is there any conclusion to this issue?
None yet. I intend to work on this at some point, hopefully soon.
--
Álvaro Herrera PostgreSQL Developer — https://www.EnterpriseDB.com/
Hi Alvaro,
I re-analyzed this issue, and here is my analysis process.
step 1: CREATE TABLE p ( id bigint PRIMARY KEY )
PARTITION BY list (id);
step2: CREATE TABLE p_1 PARTITION OF p FOR VALUES IN (1);
step3: CREATE TABLE r_1 (
id bigint PRIMARY KEY,
p_id bigint NOT NULL,
FOREIGN KEY (p_id) REFERENCES p (id)
);
After above step 3 operations, we have below catalog tuples:
postgres=# select oid, relname from pg_class where relname = 'p';
oid | relname
-------+---------
16384 | p
(1 row)
postgres=# select oid, relname from pg_class where relname = 'p_1';
oid | relname
-------+---------
16389 | p_1
(1 row)
postgres=# select oid, relname from pg_class where relname = 'r_1';
oid | relname
-------+---------
16394 | r_1
(1 row)
postgres=# select oid, conname,conrelid,conparentid,confrelid from
pg_constraint where conrelid = 16394;
oid | conname | conrelid | conparentid | confrelid
-------+-------------------+----------+-------------+-----------
16397 | r_1_p_id_not_null | 16394 | 0 | 0
16399 | r_1_pkey | 16394 | 0 | 0
16400 | r_1_p_id_fkey | 16394 | 0 | 16384
16403 | r_1_p_id_fkey1 | 16394 | 16400 | 16389
(4 rows)
postgres=# select oid, tgrelid, tgparentid,
tgconstrrelid,tgconstrindid,tgconstraint from pg_trigger where tgconstraint
= 16403;
oid | tgrelid | tgparentid | tgconstrrelid | tgconstrindid | tgconstraint
-------+---------+------------+---------------+---------------+--------------
16404 | 16389 | 16401 | 16394 | 16392 | 16403
16405 | 16389 | 16402 | 16394 | 16392 | 16403
(2 rows)
postgres=# select oid, tgrelid, tgparentid,
tgconstrrelid,tgconstrindid,tgconstraint from pg_trigger where tgconstraint
= 16400;
oid | tgrelid | tgparentid | tgconstrrelid | tgconstrindid | tgconstraint
-------+---------+------------+---------------+---------------+--------------
16401 | 16384 | 0 | 16394 | 16387 | 16400
16402 | 16384 | 0 | 16394 | 16387 | 16400
16406 | 16394 | 0 | 16384 | 16387 | 16400
16407 | 16394 | 0 | 16384 | 16387 | 16400
(4 rows)
Because table p is partitioned table and it has one child table p_1. So
when r_1 add foreign key constraint, according to addFkRecurseReferenced(),
each partition should have one pg_constraint row(e.g. r_1_p_id_fkey1).
After called addFkRecurseReferenced() in ATAddForeignKeyConstraint(),
addFkRecurseReferencing() will be called, in which
it will add INSERT check trigger and UPDATE check trigger for r_1_p_id_fkey
but not for r_1_p_id_fkey1.
So when detach r_1 from r, according to DetachPartitionFinalize(), the
inherited fks should unlink relationship from parent.
The created INSERT and UPDATE check triggers should unlink relationship
link fks. But just like I said above, the r_1_p_id_fkey1
actually doesn't have INSERT check trigger.
I slightly modified the previous patch,but I didn't add test case, because
I found another issue.
After done ALTER TABLE r ATTACH PARTITION r_1 FOR VALUES IN (1);
I run the oidjoins.sql and has warnings as belwo:
psql:/tender/postgres/src/test/regress/sql/oidjoins.sql:49: WARNING: FK
VIOLATION IN pg_trigger({tgparentid}): ("(0,3)",16401)
psql:/tender/postgres/src/test/regress/sql/oidjoins.sql:49: WARNING: FK
VIOLATION IN pg_trigger({tgparentid}): ("(0,4)",16402)
postgres=# select oid, tgrelid, tgparentid,
tgconstrrelid,tgconstrindid,tgconstraint from pg_trigger where oid >= 16384;
oid | tgrelid | tgparentid | tgconstrrelid | tgconstrindid |
tgconstraint
-------+---------+------------+---------------+---------------+--------------
16404 | 16389 | 16401 | 16394 | 16392 | 16403
16405 | 16389 | 16402 | 16394 | 16392 | 16403
16415 | 16384 | 0 | 16408 | 16387 | 16414
16416 | 16384 | 0 | 16408 | 16387 | 16414
16418 | 16389 | 16415 | 16408 | 16392 | 16417
16419 | 16389 | 16416 | 16408 | 16392 | 16417
16420 | 16408 | 0 | 16384 | 16387 | 16414
16421 | 16408 | 0 | 16384 | 16387 | 16414
16406 | 16394 | 16420 | 16384 | 16387 | 16400
16407 | 16394 | 16421 | 16384 | 16387 | 16400
(10 rows)
oid = 16401 and oid = 16402 has been deleted.
The two trigger tuples are deleted in tryAttachPartitionForeignKey called
by CloneFkReferencing.
/*
* Looks good! Attach this constraint. The action triggers in the new
* partition become redundant -- the parent table already has equivalent
* ones, and those will be able to reach the partition. Remove the ones
* in the partition. We identify them because they have our constraint
* OID, as well as being on the referenced rel.
*/
The attached patch can't fix above issue. I'm not sure about the impact of
this issue. Maybe redundant triggers no need removed.
But it surely make oidjoings.sql fail if I add test case into v2 patch, so
I don't add test case in v2 patch.
No test case is not good patch. I just share my idea about this issue. Hope
to get your reply.
Alvaro Herrera <alvherre@alvh.no-ip.org> 于2023年10月25日周三 20:13写道:
Show quoted text
On 2023-Oct-25, tender wang wrote:
Hi
Is there any conclusion to this issue?None yet. I intend to work on this at some point, hopefully soon.
--
Álvaro Herrera PostgreSQL Developer —
https://www.EnterpriseDB.com/
Attachments:
v2-0001-Fix-partition-detach-issue.patchapplication/x-patch; name=v2-0001-Fix-partition-detach-issue.patchDownload
From 24491419ad65871e54207d3ef481d8abe529e1e1 Mon Sep 17 00:00:00 2001
From: "tender.wang" <tender.wang@openpie.com>
Date: Fri, 27 Oct 2023 13:48:48 +0800
Subject: [PATCH v2] Fix partition detach issue.
---
src/backend/commands/tablecmds.c | 21 +++++++++++++++++++++
1 file changed, 21 insertions(+)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 416a98e7ce..3b897b620a 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -19356,7 +19356,9 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
{
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
+ HeapTuple parentConTup;
Form_pg_constraint conform;
+ Form_pg_constraint parentConForm;
Constraint *fkconstraint;
Oid insertTriggerOid,
updateTriggerOid;
@@ -19374,6 +19376,24 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
continue;
}
+ /* For referenced-side, if it is partitioned table, each partition
+ * has one row in pg_constraint. But it doesn't have INSERT CHECK trigger
+ */
+ Assert(OidIsValid(conform->conparentid));
+ parentConTup = SearchSysCache1(CONSTROID,
+ ObjectIdGetDatum(conform->conparentid));
+ if (!HeapTupleIsValid(parentConTup))
+ elog(ERROR, "cache lookup failed for constraint %u",
+ conform->conparentid);
+ parentConForm = (Form_pg_constraint)GETSTRUCT(parentConTup);
+ if (parentConForm->confrelid != conform->confrelid &&
+ parentConForm->conrelid == conform->conrelid)
+ {
+ ReleaseSysCache(contup);
+ ReleaseSysCache(parentConTup);
+ continue;
+ }
+
/* unset conparentid and adjust conislocal, coninhcount, etc. */
ConstraintSetParentConstraint(fk->conoid, InvalidOid, InvalidOid);
@@ -19421,6 +19441,7 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
NULL, NULL);
ReleaseSysCache(contup);
+ ReleaseSysCache(parentConTup);
}
list_free_deep(fks);
if (trigrel)
--
2.25.1
Hi Alvaro,
Recently, Alexander reported the same issue on [1]/messages/by-id/18541-628a61bc267cd2d3@postgresql.org. And before that,
another same issue was reported on [2]/messages/by-id/GVAP278MB02787E7134FD691861635A8BC9032@GVAP278MB0278.CHEP278.PROD.OUTLOOK.COM.
So I try to re-work those issues. In my last email on this thread, I said
that
"
I slightly modified the previous patch,but I didn't add test case, because
I found another issue.
After done ALTER TABLE r ATTACH PARTITION r_1 FOR VALUES IN (1);
I run the oidjoins.sql and has warnings as belwo:
psql:/tender/postgres/src/test/regress/sql/oidjoins.sql:49: WARNING: FK
VIOLATION IN pg_trigger({tgparentid}): ("(0,3)",16401)
psql:/tender/postgres/src/test/regress/sql/oidjoins.sql:49: WARNING: FK
VIOLATION IN pg_trigger({tgparentid}): ("(0,4)",16402)
"
And I gave the explanation:
"
The two trigger tuples are deleted in tryAttachPartitionForeignKey called
by CloneFkReferencing.
/*
* Looks good! Attach this constraint. The action triggers in the new
* partition become redundant -- the parent table already has equivalent
* ones, and those will be able to reach the partition. Remove the ones
* in the partition. We identify them because they have our constraint
* OID, as well as being on the referenced rel.
*/
"
I try to fix above fk violation. I have two ideas.
i. Do not remove redundant, but when detaching parittion, the action
trigger on referenced side will be create again.
I have consider about this situation.
ii. We still remove redundant, and the remove the child action trigger,
too. If we do this way.
Should we create action trigger recursively on referced side when detaching
partition.
I can't decide which one is better. And I'm not sure that keep this FK
VIOLATION will cause some problem.
I rebase and send v3 patch, which only fix NOT FOUND INSERT CHECK TRIGGER.
[1]: /messages/by-id/18541-628a61bc267cd2d3@postgresql.org
/messages/by-id/18541-628a61bc267cd2d3@postgresql.org
[2]: /messages/by-id/GVAP278MB02787E7134FD691861635A8BC9032@GVAP278MB0278.CHEP278.PROD.OUTLOOK.COM
/messages/by-id/GVAP278MB02787E7134FD691861635A8BC9032@GVAP278MB0278.CHEP278.PROD.OUTLOOK.COM
--
Tender Wang
Attachments:
v3-0001-Fix-partition-detach-issue.patchapplication/octet-stream; name=v3-0001-Fix-partition-detach-issue.patchDownload
From a7b1569820252f8d5a51910550a1eedaa8632ab3 Mon Sep 17 00:00:00 2001
From: "tender.wang" <tender.wang@openpie.com>
Date: Fri, 27 Oct 2023 13:48:48 +0800
Subject: [PATCH v3 1/2] Fix partition detach issue.
---
src/backend/commands/tablecmds.c | 21 +++++++++++++++++++++
1 file changed, 21 insertions(+)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 721d24783b..d5c6da04fb 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -19185,7 +19185,9 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
{
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
+ HeapTuple parentConTup;
Form_pg_constraint conform;
+ Form_pg_constraint parentConForm;
Constraint *fkconstraint;
Oid insertTriggerOid,
updateTriggerOid;
@@ -19203,6 +19205,24 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
continue;
}
+ /* For referenced-side, if it is partitioned table, each partition
+ * has one row in pg_constraint. But it doesn't have INSERT CHECK trigger
+ */
+ Assert(OidIsValid(conform->conparentid));
+ parentConTup = SearchSysCache1(CONSTROID,
+ ObjectIdGetDatum(conform->conparentid));
+ if (!HeapTupleIsValid(parentConTup))
+ elog(ERROR, "cache lookup failed for constraint %u",
+ conform->conparentid);
+ parentConForm = (Form_pg_constraint)GETSTRUCT(parentConTup);
+ if (parentConForm->confrelid != conform->confrelid &&
+ parentConForm->conrelid == conform->conrelid)
+ {
+ ReleaseSysCache(contup);
+ ReleaseSysCache(parentConTup);
+ continue;
+ }
+
/* unset conparentid and adjust conislocal, coninhcount, etc. */
ConstraintSetParentConstraint(fk->conoid, InvalidOid, InvalidOid);
@@ -19250,6 +19270,7 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
NULL, NULL);
ReleaseSysCache(contup);
+ ReleaseSysCache(parentConTup);
}
list_free_deep(fks);
if (trigrel)
--
2.34.1
v3-0002-Add-test-case.patchapplication/octet-stream; name=v3-0002-Add-test-case.patchDownload
From 9344a0f5215c2745d875422b66120599878cfda1 Mon Sep 17 00:00:00 2001
From: Tender Wang <tndrwang@gmail.com>
Date: Thu, 18 Jul 2024 10:12:18 +0800
Subject: [PATCH v3 2/2] Add test case.
---
src/backend/commands/tablecmds.c | 2 +-
src/test/regress/expected/foreign_key.out | 19 +++++++++++++++++++
src/test/regress/sql/foreign_key.sql | 20 ++++++++++++++++++++
3 files changed, 40 insertions(+), 1 deletion(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d5c6da04fb..2bdd63f0b6 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -19214,7 +19214,7 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
if (!HeapTupleIsValid(parentConTup))
elog(ERROR, "cache lookup failed for constraint %u",
conform->conparentid);
- parentConForm = (Form_pg_constraint)GETSTRUCT(parentConTup);
+ parentConForm = (Form_pg_constraint) GETSTRUCT(parentConTup);
if (parentConForm->confrelid != conform->confrelid &&
parentConForm->conrelid == conform->conrelid)
{
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 46764bd9e3..e36ff9deb3 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -2917,3 +2917,22 @@ DETAIL: drop cascades to table fkpart11.pk
drop cascades to table fkpart11.fk_parted
drop cascades to table fkpart11.fk_another
drop cascades to function fkpart11.print_row()
+CREATE TABLE fk_p ( id bigint PRIMARY KEY ) PARTITION BY list (id);
+CREATE TABLE fk_p_1 PARTITION OF fk_p FOR VALUES IN (1);
+CREATE TABLE fk_r_1 (
+ id bigint PRIMARY KEY,
+ p_id bigint NOT NULL,
+ FOREIGN KEY (p_id) REFERENCES fk_p (id)
+);
+CREATE TABLE fk_r (
+ id bigint PRIMARY KEY,
+ p_id bigint NOT NULL,
+ FOREIGN KEY (p_id) REFERENCES fk_p (id)
+) PARTITION BY list (id);
+ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1);
+ALTER TABLE fk_r DETACH PARTITION fk_r_1;
+DROP TABLE fk_p CASCADE;
+NOTICE: drop cascades to 2 other objects
+DETAIL: drop cascades to constraint fk_r_1_p_id_fkey on table fk_r_1
+drop cascades to constraint fk_r_p_id_fkey on table fk_r
+DROP TABLE fk_r CASCADE;
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index f5e0938999..d38b10613a 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -2069,3 +2069,23 @@ UPDATE fkpart11.pk SET a = 3 WHERE a = 4;
UPDATE fkpart11.pk SET a = 1 WHERE a = 2;
DROP SCHEMA fkpart11 CASCADE;
+
+CREATE TABLE fk_p ( id bigint PRIMARY KEY ) PARTITION BY list (id);
+CREATE TABLE fk_p_1 PARTITION OF fk_p FOR VALUES IN (1);
+
+CREATE TABLE fk_r_1 (
+ id bigint PRIMARY KEY,
+ p_id bigint NOT NULL,
+ FOREIGN KEY (p_id) REFERENCES fk_p (id)
+);
+
+CREATE TABLE fk_r (
+ id bigint PRIMARY KEY,
+ p_id bigint NOT NULL,
+ FOREIGN KEY (p_id) REFERENCES fk_p (id)
+) PARTITION BY list (id);
+
+ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1);
+ALTER TABLE fk_r DETACH PARTITION fk_r_1;
+DROP TABLE fk_p CASCADE;
+DROP TABLE fk_r CASCADE;
--
2.34.1
Hello,
I think the fix for the check triggers should be as the attached. Very
close to what you did, but you were skipping some operations that needed
to be kept. AFAICS this patch works correctly for the posted cases.
I haven't looked at the action triggers yet; I think we need to create
one trigger for each partition of the referenced side, so we need to
loop instead of doing a single one.
I find this pair of queries useful; they show which constraints exist
and which triggers belong to each. We need to make the constraints and
triggers match after a detach right as things would be if the
just-detached partition were an individual table having the same foreign
key.
--
Álvaro Herrera PostgreSQL Developer — https://www.EnterpriseDB.com/
"People get annoyed when you try to debug them." (Larry Wall)
Attachments:
0001-Fix-partition-detach-on-tables-with-FKs-to-partition.patchtext/x-diff; charset=utf-8Download
From 925739ef0c6b64c73b021ed930f65f140004ceb5 Mon Sep 17 00:00:00 2001
From: Alvaro Herrera <alvherre@alvh.no-ip.org>
Date: Fri, 19 Jul 2024 15:12:38 +0200
Subject: [PATCH] Fix partition detach on tables with FKs to partitioned tables
---
src/backend/commands/tablecmds.c | 49 +++++++++++++++++++++++++-------
1 file changed, 38 insertions(+), 11 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 721d24783b..ca777d3c88 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -19185,8 +19185,11 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
{
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
+ HeapTuple parentConTup;
Form_pg_constraint conform;
+ Form_pg_constraint parentConForm;
Constraint *fkconstraint;
+ Oid parentConstrOid;
Oid insertTriggerOid,
updateTriggerOid;
@@ -19203,22 +19206,46 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
continue;
}
+ parentConstrOid = conform->conparentid;
+
/* unset conparentid and adjust conislocal, coninhcount, etc. */
ConstraintSetParentConstraint(fk->conoid, InvalidOid, InvalidOid);
/*
- * Also, look up the partition's "check" triggers corresponding to the
- * constraint being detached and detach them from the parent triggers.
+ * Search for the partition's check triggers that implement the
+ * constraint being detached, and make them no longer children of the
+ * triggers on the parent table. However, if the referenced side is a
+ * partitioned table, there are no such check triggers (we know that
+ * the referenced side is partitioned because our constraint row points
+ * to a partition, whereas our parent constraint points to its parent
+ * partitioned table.)
*/
- GetForeignKeyCheckTriggers(trigrel,
- fk->conoid, fk->confrelid, fk->conrelid,
- &insertTriggerOid, &updateTriggerOid);
- Assert(OidIsValid(insertTriggerOid));
- TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
- RelationGetRelid(partRel));
- Assert(OidIsValid(updateTriggerOid));
- TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
- RelationGetRelid(partRel));
+ Assert(OidIsValid(conform->conparentid));
+ parentConTup = SearchSysCache1(CONSTROID,
+ ObjectIdGetDatum(parentConstrOid));
+ if (!HeapTupleIsValid(parentConTup))
+ elog(ERROR, "cache lookup failed for constraint %u",
+ conform->conparentid);
+ parentConForm = (Form_pg_constraint) GETSTRUCT(parentConTup);
+
+ if (parentConForm->conrelid != conform->conrelid)
+ {
+ /*
+ * Also, look up the partition's "check" triggers corresponding to the
+ * constraint being detached and detach them from the parent triggers.
+ */
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ Assert(OidIsValid(updateTriggerOid));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ }
+
+ ReleaseSysCache(parentConTup);
/*
* Make the action triggers on the referenced relation. When this was
--
2.39.2
Alvaro Herrera <alvherre@alvh.no-ip.org> 于2024年7月19日周五 21:18写道:
Hello,
I think the fix for the check triggers should be as the attached. Very
close to what you did, but you were skipping some operations that needed
to be kept. AFAICS this patch works correctly for the posted cases.
After applying the attached, the r_1_p_id_fkey1 will have redundant action
triggers, as below:
postgres=# select oid, conname, contype, conrelid, conindid,conparentid,
confrelid,conislocal,coninhcount, connoinherit from pg_constraint where oid
= 16402;
oid | conname | contype | conrelid | conindid | conparentid |
confrelid | conislocal | coninhcount | connoinherit
-------+----------------+---------+----------+----------+-------------+-----------+------------+-------------+--------------
16402 | r_1_p_id_fkey1 | f | 16394 | 16392 | 0 |
16389 | t | 0 | f
(1 row)
postgres=# select oid, tgrelid, tgparentid, tgconstrrelid, tgconstrindid,
tgconstraint from pg_trigger where tgconstraint = 16402;
oid | tgrelid | tgparentid | tgconstrrelid | tgconstrindid | tgconstraint
-------+---------+------------+---------------+---------------+--------------
16403 | 16389 | 16400 | 16394 | 16392 | 16402
16404 | 16389 | 16401 | 16394 | 16392 | 16402
16422 | 16389 | 0 | 16394 | 16392 | 16402
16423 | 16389 | 0 | 16394 | 16392 | 16402
(4 rows)
--
Tender Wang
Alvaro Herrera <alvherre@alvh.no-ip.org> 于2024年7月19日周五 21:18写道:
I find this pair of queries useful; they show which constraints exist
and which triggers belong to each. We need to make the constraints and
triggers match after a detach right as things would be if the
just-detached partition were an individual table having the same foreign
key.
I don't find the useful queries in your last email. Can you provide them.
Thanks.
--
Tender Wang
On Mon, Jul 22, 2024 at 1:52 PM Tender Wang <tndrwang@gmail.com> wrote:
Alvaro Herrera <alvherre@alvh.no-ip.org> 于2024年7月19日周五 21:18写道:
Hello,
I think the fix for the check triggers should be as the attached. Very
close to what you did, but you were skipping some operations that needed
to be kept. AFAICS this patch works correctly for the posted cases.After applying the attached, the r_1_p_id_fkey1 will have redundant action
triggers, as below:
postgres=# select oid, conname, contype, conrelid, conindid,conparentid, confrelid,conislocal,coninhcount, connoinherit from pg_constraint where oid = 16402;
oid | conname | contype | conrelid | conindid | conparentid | confrelid | conislocal | coninhcount | connoinherit
-------+----------------+---------+----------+----------+-------------+-----------+------------+-------------+--------------
16402 | r_1_p_id_fkey1 | f | 16394 | 16392 | 0 | 16389 | t | 0 | f
(1 row)postgres=# select oid, tgrelid, tgparentid, tgconstrrelid, tgconstrindid, tgconstraint from pg_trigger where tgconstraint = 16402;
oid | tgrelid | tgparentid | tgconstrrelid | tgconstrindid | tgconstraint
-------+---------+------------+---------------+---------------+--------------
16403 | 16389 | 16400 | 16394 | 16392 | 16402
16404 | 16389 | 16401 | 16394 | 16392 | 16402
16422 | 16389 | 0 | 16394 | 16392 | 16402
16423 | 16389 | 0 | 16394 | 16392 | 16402
(4 rows)
Yes, seems Alvaro has mentioned that he hasn't looked at the
action triggers, in the attached patch, I add some logic that
first check if there exists action triggers, if yes, just update
their Parent Trigger to InvalidOid.
--
Tender Wang
--
Regards
Junwang Zhao
Attachments:
v2-0001-Fix-partition-detach-on-tables-with-FKs-to-partit.patchapplication/octet-stream; name=v2-0001-Fix-partition-detach-on-tables-with-FKs-to-partit.patchDownload
From a8796b6aa2f515a7feb151f2fcc825298587192f Mon Sep 17 00:00:00 2001
From: Alvaro Herrera <alvherre@alvh.no-ip.org>
Date: Fri, 19 Jul 2024 15:12:38 +0200
Subject: [PATCH v2] Fix partition detach on tables with FKs to partitioned
tables
---
src/backend/commands/tablecmds.c | 136 +++++++++++++++++++++----------
1 file changed, 94 insertions(+), 42 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 721d24783b4..f68ec20cf97 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -547,7 +547,8 @@ static bool tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
static void GetForeignKeyActionTriggers(Relation trigrel,
Oid conoid, Oid confrelid, Oid conrelid,
Oid *deleteTriggerOid,
- Oid *updateTriggerOid);
+ Oid *updateTriggerOid,
+ bool missing_ok);
static void GetForeignKeyCheckTriggers(Relation trigrel,
Oid conoid, Oid confrelid, Oid conrelid,
Oid *insertTriggerOid,
@@ -10698,7 +10699,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
*/
GetForeignKeyActionTriggers(trigrel, constrOid,
constrForm->confrelid, constrForm->conrelid,
- &deleteTriggerOid, &updateTriggerOid);
+ &deleteTriggerOid, &updateTriggerOid,
+ false);
addFkRecurseReferenced(NULL,
fkconstraint,
@@ -11153,7 +11155,8 @@ static void
GetForeignKeyActionTriggers(Relation trigrel,
Oid conoid, Oid confrelid, Oid conrelid,
Oid *deleteTriggerOid,
- Oid *updateTriggerOid)
+ Oid *updateTriggerOid,
+ bool missing_ok)
{
ScanKeyData key;
SysScanDesc scan;
@@ -11195,10 +11198,10 @@ GetForeignKeyActionTriggers(Relation trigrel,
#endif
}
- if (!OidIsValid(*deleteTriggerOid))
+ if (!OidIsValid(*deleteTriggerOid) && !missing_ok)
elog(ERROR, "could not find ON DELETE action trigger of foreign key constraint %u",
conoid);
- if (!OidIsValid(*updateTriggerOid))
+ if (!OidIsValid(*updateTriggerOid) && !missing_ok)
elog(ERROR, "could not find ON UPDATE action trigger of foreign key constraint %u",
conoid);
@@ -19185,10 +19188,15 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
{
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
+ HeapTuple parentConTup;
Form_pg_constraint conform;
+ Form_pg_constraint parentConForm;
Constraint *fkconstraint;
+ Oid parentConstrOid;
Oid insertTriggerOid,
updateTriggerOid;
+ Oid deleteActionTriggerOid,
+ updateActionTriggerOid;
contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
if (!HeapTupleIsValid(contup))
@@ -19203,51 +19211,95 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
continue;
}
+ parentConstrOid = conform->conparentid;
+
/* unset conparentid and adjust conislocal, coninhcount, etc. */
ConstraintSetParentConstraint(fk->conoid, InvalidOid, InvalidOid);
/*
- * Also, look up the partition's "check" triggers corresponding to the
- * constraint being detached and detach them from the parent triggers.
+ * Search for the partition's check triggers that implement the
+ * constraint being detached, and make them no longer children of the
+ * triggers on the parent table. However, if the referenced side is a
+ * partitioned table, there are no such check triggers (we know that
+ * the referenced side is partitioned because our constraint row
+ * points to a partition, whereas our parent constraint points to its
+ * parent partitioned table.)
*/
- GetForeignKeyCheckTriggers(trigrel,
- fk->conoid, fk->confrelid, fk->conrelid,
- &insertTriggerOid, &updateTriggerOid);
- Assert(OidIsValid(insertTriggerOid));
- TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
- RelationGetRelid(partRel));
- Assert(OidIsValid(updateTriggerOid));
- TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
- RelationGetRelid(partRel));
+ Assert(OidIsValid(conform->conparentid));
+ parentConTup = SearchSysCache1(CONSTROID,
+ ObjectIdGetDatum(parentConstrOid));
+ if (!HeapTupleIsValid(parentConTup))
+ elog(ERROR, "cache lookup failed for constraint %u",
+ conform->conparentid);
+ parentConForm = (Form_pg_constraint) GETSTRUCT(parentConTup);
+
+ if (parentConForm->conrelid != conform->conrelid)
+ {
+ /*
+ * Also, look up the partition's "check" triggers corresponding to
+ * the constraint being detached and detach them from the parent
+ * triggers.
+ */
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ Assert(OidIsValid(updateTriggerOid));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ }
+
+ ReleaseSysCache(parentConTup);
/*
- * 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.
+ * There are chances that the action triggers already exists on the
+ * referenced relation. If they do, .
*/
- fkconstraint = makeNode(Constraint);
- fkconstraint->contype = CONSTRAINT_FOREIGN;
- fkconstraint->conname = pstrdup(NameStr(conform->conname));
- fkconstraint->deferrable = conform->condeferrable;
- fkconstraint->initdeferred = conform->condeferred;
- fkconstraint->location = -1;
- fkconstraint->pktable = NULL;
- fkconstraint->fk_attrs = NIL;
- fkconstraint->pk_attrs = NIL;
- fkconstraint->fk_matchtype = conform->confmatchtype;
- fkconstraint->fk_upd_action = conform->confupdtype;
- fkconstraint->fk_del_action = conform->confdeltype;
- fkconstraint->fk_del_set_cols = NIL;
- fkconstraint->old_conpfeqop = NIL;
- fkconstraint->old_pktable_oid = InvalidOid;
- fkconstraint->skip_validation = false;
- fkconstraint->initially_valid = true;
-
- createForeignKeyActionTriggers(partRel, conform->confrelid,
- fkconstraint, fk->conoid,
- conform->conindid,
- InvalidOid, InvalidOid,
- NULL, NULL);
+ GetForeignKeyActionTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &deleteActionTriggerOid, &updateActionTriggerOid,
+ true);
+ if (OidIsValid(deleteActionTriggerOid))
+ {
+ Assert(OidIsValid(updateActionTriggerOid));
+ TriggerSetParentTrigger(trigrel, deleteActionTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ TriggerSetParentTrigger(trigrel, updateActionTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ }
+ else
+ {
+ /*
+ * Make the action triggers on the referenced relation. When this
+ * was a partition the action triggers pointed to the parent rel
+ * (they still do), but now we need separate ones of our own.
+ */
+ fkconstraint = makeNode(Constraint);
+ fkconstraint->contype = CONSTRAINT_FOREIGN;
+ fkconstraint->conname = pstrdup(NameStr(conform->conname));
+ fkconstraint->deferrable = conform->condeferrable;
+ fkconstraint->initdeferred = conform->condeferred;
+ fkconstraint->location = -1;
+ fkconstraint->pktable = NULL;
+ fkconstraint->fk_attrs = NIL;
+ fkconstraint->pk_attrs = NIL;
+ fkconstraint->fk_matchtype = conform->confmatchtype;
+ fkconstraint->fk_upd_action = conform->confupdtype;
+ fkconstraint->fk_del_action = conform->confdeltype;
+ fkconstraint->fk_del_set_cols = NIL;
+ fkconstraint->old_conpfeqop = NIL;
+ fkconstraint->old_pktable_oid = InvalidOid;
+ fkconstraint->skip_validation = false;
+ fkconstraint->initially_valid = true;
+
+ createForeignKeyActionTriggers(partRel, conform->confrelid,
+ fkconstraint, fk->conoid,
+ conform->conindid,
+ InvalidOid, InvalidOid,
+ NULL, NULL);
+ }
ReleaseSysCache(contup);
}
--
2.39.2
On Fri, Jul 26, 2024 at 2:36 PM Junwang Zhao <zhjwpku@gmail.com> wrote:
On Mon, Jul 22, 2024 at 1:52 PM Tender Wang <tndrwang@gmail.com> wrote:
Alvaro Herrera <alvherre@alvh.no-ip.org> 于2024年7月19日周五 21:18写道:
Hello,
I think the fix for the check triggers should be as the attached. Very
close to what you did, but you were skipping some operations that needed
to be kept. AFAICS this patch works correctly for the posted cases.After applying the attached, the r_1_p_id_fkey1 will have redundant action
triggers, as below:
postgres=# select oid, conname, contype, conrelid, conindid,conparentid, confrelid,conislocal,coninhcount, connoinherit from pg_constraint where oid = 16402;
oid | conname | contype | conrelid | conindid | conparentid | confrelid | conislocal | coninhcount | connoinherit
-------+----------------+---------+----------+----------+-------------+-----------+------------+-------------+--------------
16402 | r_1_p_id_fkey1 | f | 16394 | 16392 | 0 | 16389 | t | 0 | f
(1 row)postgres=# select oid, tgrelid, tgparentid, tgconstrrelid, tgconstrindid, tgconstraint from pg_trigger where tgconstraint = 16402;
oid | tgrelid | tgparentid | tgconstrrelid | tgconstrindid | tgconstraint
-------+---------+------------+---------------+---------------+--------------
16403 | 16389 | 16400 | 16394 | 16392 | 16402
16404 | 16389 | 16401 | 16394 | 16392 | 16402
16422 | 16389 | 0 | 16394 | 16392 | 16402
16423 | 16389 | 0 | 16394 | 16392 | 16402
(4 rows)Yes, seems Alvaro has mentioned that he hasn't looked at the
action triggers, in the attached patch, I add some logic that
first check if there exists action triggers, if yes, just update
their Parent Trigger to InvalidOid.--
Tender Wang--
Regards
Junwang Zhao
There is a bug report[0]/messages/by-id/18541-628a61bc267cd2d3@postgresql.org Tender comments might be the same
issue as this one, but I tried Alvaro's and mine patch, neither
could solve that problem, I did not tried Tender's earlier patch
thought. I post the test script below in case you are interested.
CREATE TABLE t1 (a int, PRIMARY KEY (a));
CREATE TABLE t (a int, PRIMARY KEY (a), FOREIGN KEY (a) REFERENCES t1)
PARTITION BY LIST (a);
ALTER TABLE t ATTACH PARTITION t1 FOR VALUES IN (1);
ALTER TABLE t DETACH PARTITION t1;
ALTER TABLE t ATTACH PARTITION t1 FOR VALUES IN (1);
[0]: /messages/by-id/18541-628a61bc267cd2d3@postgresql.org
--
Regards
Junwang Zhao
On 2024-Jul-26, Junwang Zhao wrote:
There is a bug report[0] Tender comments might be the same
issue as this one, but I tried Alvaro's and mine patch, neither
could solve that problem, I did not tried Tender's earlier patch
thought. I post the test script below in case you are interested.
Yeah, I've been looking at this whole debacle this week and after
looking at it more closely, I realized that the overall problem requires
a much more invasive solution -- namely, that on DETACH, if the
referenced table is partitioned, we need to create additional
pg_constraint entries from the now-standalone table (was partition) to
each of the partitions of the referenced table; and also add action
triggers to each of those. Without that, the constraint is incomplete
and doesn't work (as reported multiple times already).
One thing I have not yet tried is what if the partition being detach is
also partitioned. I mean, do we need to handle each sub-partition
explicitly in some way? I think the answer is no, but it needs tests.
I have written the patch to do this on detach, and AFAICS it works well,
though it changes the behavior of some existing tests (IIRC related to
self-referencing FKs). Also, the next problem is making sure that
ATTACH deals with it correctly. I'm on this bit today.
Self-referencing FKs seem to have additional problems :-(
The queries I was talking about are these
\set tables ''''prim.*''',''forign.*''',''''lone''''
select oid, conparentid, contype, conname, conrelid::regclass, confrelid::regclass, conkey, confkey, conindid::regclass from pg_constraint where contype = 'f' and (conrelid::regclass::text ~ any (array[:tables]) or confrelid::regclass::text ~ any (array[:tables])) order by contype, conrelid, confrelid; select tgconstraint, oid, tgrelid::regclass, tgconstrrelid::regclass, tgname, tgparentid, tgconstrindid::regclass, tgfoid::regproc from pg_trigger where tgconstraint in (select oid from pg_constraint where conrelid::regclass::text ~ any (array[:tables]) or confrelid::regclass::text ~ any (array[:tables])) order by tgconstraint, tgrelid::regclass::text, tgfoid;
Written as a single line in psql they let you quickly see all the
constraints and their associated triggers, so for instance you can see
whether this sequence
create table prim (a int primary key) partition by list (a);
create table prim1 partition of prim for values in (1);
create table prim2 partition of prim for values in (2);
create table forign (a int references prim) partition by list (a);
create table forign1 partition of forign for values in (1);
create table forign2 partition of forign for values in (2);
alter table forign detach partition forign1;
produces the same set of constraints and triggers as this other sequence
create table prim (a int primary key) partition by list (a);
create table prim1 partition of prim for values in (1);
create table prim2 partition of prim for values in (2);
create table forign (a int references prim) partition by list (a);
create table forign2 partition of forign for values in (2);
create table forign1 (a int references prim);
The patch is more or less like the attached, far from ready.
--
Álvaro Herrera Breisgau, Deutschland — https://www.EnterpriseDB.com/
Syntax error: function hell() needs an argument.
Please choose what hell you want to involve.
Attachments:
patch.1text/plain; charset=utf-8Download
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 721d24783b..1ea4003aec 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -10634,7 +10634,7 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
* Because we're only expanding the key space at the referenced side,
* we don't need to prevent any operation in the referencing table, so
* AccessShareLock suffices (assumes that dropping the constraint
- * acquires AEL).
+ * acquires AccessExclusiveLock).
*/
fkRel = table_open(constrForm->conrelid, AccessShareLock);
@@ -19175,8 +19175,10 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
DropClonedTriggersFromPartition(RelationGetRelid(partRel));
/*
- * Detach any foreign keys that are inherited. This includes creating
- * additional action triggers.
+ * Detach any foreign keys that are inherited. This requires marking some
+ * constraints and triggers as no longer having parents, as well as
+ * creating additional constraint entries and triggers, depending on the
+ * shape of each FK.
*/
fks = copyObject(RelationGetFKeyList(partRel));
if (fks != NIL)
@@ -19185,10 +19187,15 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
{
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
+ HeapTuple parentConTup;
Form_pg_constraint conform;
+ Form_pg_constraint parentConForm;
Constraint *fkconstraint;
- Oid insertTriggerOid,
- updateTriggerOid;
+ Oid parentConstrOid;
+ Oid insertCheckTrigOid,
+ updateCheckTrigOid,
+ updateActionTrigOid,
+ deleteActionTrigOid;
contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
if (!HeapTupleIsValid(contup))
@@ -19203,28 +19210,34 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
continue;
}
- /* unset conparentid and adjust conislocal, coninhcount, etc. */
- ConstraintSetParentConstraint(fk->conoid, InvalidOid, InvalidOid);
-
/*
- * Also, look up the partition's "check" triggers corresponding to the
- * constraint being detached and detach them from the parent triggers.
+ * To create a FK constraint in the to-be-detached partition
+ * equivalent to what exists in the parent table, what to do depends
+ * on whether the referenced table is partitioned or not. If it
+ * isn't, then just reparenting the pg_constraint row and pg_trigger
+ * rows for the existing constraint, and creating new action triggers,
+ * is sufficient. However, if the referenced table is partitioned,
+ * then we must create additional pg_constraint rows -- one for each
+ * partition -- and the necessary action triggers for each of them.
*/
- GetForeignKeyCheckTriggers(trigrel,
- fk->conoid, fk->confrelid, fk->conrelid,
- &insertTriggerOid, &updateTriggerOid);
- Assert(OidIsValid(insertTriggerOid));
- TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
- RelationGetRelid(partRel));
- Assert(OidIsValid(updateTriggerOid));
- TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
- RelationGetRelid(partRel));
+ parentConstrOid = conform->conparentid;
- /*
- * 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.
- */
+ Assert(OidIsValid(conform->conparentid));
+ parentConTup = SearchSysCache1(CONSTROID,
+ ObjectIdGetDatum(parentConstrOid));
+ if (!HeapTupleIsValid(parentConTup))
+ elog(ERROR, "cache lookup failed for constraint %u",
+ conform->conparentid);
+ parentConForm = (Form_pg_constraint) GETSTRUCT(parentConTup);
+
+ if (parentConForm->confrelid == RelationGetRelid(rel))
+ {
+ elog(WARNING, "detaching self-referencing FK %u (parent %u) from %s to %s",
+ conform->oid, conform->conparentid,
+ RelationGetRelationName(partRel), RelationGetRelationName(rel));
+ }
+
+ /* Create a node we'll use throughout this */
fkconstraint = makeNode(Constraint);
fkconstraint->contype = CONSTRAINT_FOREIGN;
fkconstraint->conname = pstrdup(NameStr(conform->conname));
@@ -19240,15 +19253,178 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
fkconstraint->fk_del_set_cols = NIL;
fkconstraint->old_conpfeqop = NIL;
fkconstraint->old_pktable_oid = InvalidOid;
- fkconstraint->skip_validation = false;
+ fkconstraint->skip_validation = true; /* XXX hmmm */
fkconstraint->initially_valid = true;
+ /*
+ * The constraint on this table must be marked no longer a child of
+ * the parent's constraint; and its check triggers too.
+ */
+ ConstraintSetParentConstraint(fk->conoid, InvalidOid, InvalidOid);
+
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertCheckTrigOid, &updateCheckTrigOid);
+ Assert(OidIsValid(insertCheckTrigOid));
+ TriggerSetParentTrigger(trigrel, insertCheckTrigOid, InvalidOid,
+ RelationGetRelid(partRel));
+ Assert(OidIsValid(updateCheckTrigOid));
+ TriggerSetParentTrigger(trigrel, updateCheckTrigOid, InvalidOid,
+ RelationGetRelid(partRel));
+
+ /*
+ * Now create the action triggers in this table that point to the
+ * referenced table.
+ */
createForeignKeyActionTriggers(partRel, conform->confrelid,
fkconstraint, fk->conoid,
conform->conindid,
InvalidOid, InvalidOid,
- NULL, NULL);
+ &deleteActionTrigOid, &updateActionTrigOid);
+ /*
+ * If the referenced side is partitioned (which we know because our
+ * parent's constraint points to a different relation than ours) then
+ * we must, in addition to the above, create pg_constraint rows that
+ * point to each partition, each with its own action triggers.
+ */
+ if (parentConForm->conrelid != conform->conrelid)
+ {
+ List *children;
+ ListCell *child;
+ Relation refdRel;
+ Oid indexOid;
+ int numfks;
+ AttrNumber conkey[INDEX_MAX_KEYS];
+ AttrNumber confkey[INDEX_MAX_KEYS];
+ Oid conpfeqop[INDEX_MAX_KEYS];
+ Oid conppeqop[INDEX_MAX_KEYS];
+ Oid conffeqop[INDEX_MAX_KEYS];
+ int numfkdelsetcols;
+ AttrNumber confdelsetcols[INDEX_MAX_KEYS];
+ List *colnames = NIL;
+ char *fkaddition;
+
+ DeconstructFkConstraintRow(contup,
+ &numfks,
+ conkey,
+ confkey,
+ conpfeqop,
+ conppeqop,
+ conffeqop,
+ &numfkdelsetcols,
+ confdelsetcols);
+
+ for (int i = 0; i < numfks; i++)
+ {
+ Form_pg_attribute att;
+
+ att = TupleDescAttr(RelationGetDescr(partRel),
+ conkey[i] - 1);
+ colnames = lappend(colnames, makeString(NameStr(att->attname)));
+ }
+ fkaddition = ChooseForeignKeyConstraintNameAddition(colnames);
+
+ /*
+ * We're not expanding nor shrinking key space, so AccessShareLock
+ * is sufficient here given that dropping a constraint requires
+ * AccessExclusiveLock.
+ */
+ refdRel = table_open(fk->confrelid, AccessShareLock);
+
+ /*
+ * When the referenced table is partitioned, we need new
+ * pg_constraint entries that point from our partition to each
+ * partition of the other table, and also the action triggers on
+ * each.
+ */
+ children = find_all_inheritors(fk->confrelid, NoLock, NULL);
+ foreach(child, children)
+ {
+ Oid childoid = lfirst_oid(child);
+ Relation fchild;
+ AttrNumber mapped_confkey[INDEX_MAX_KEYS];
+ AttrMap *attmap;
+ char *conname;
+ Oid constrOid;
+ ObjectAddress address,
+ referenced;
+
+ /* A constraint for the topmost parent already exists */
+ if (childoid == fk->confrelid)
+ continue;
+
+ fchild = table_open(childoid, AccessShareLock);
+
+ attmap = build_attrmap_by_name(RelationGetDescr(refdRel),
+ RelationGetDescr(fchild),
+ false);
+ for (int i = 0; i < numfks; i++)
+ mapped_confkey[i] = attmap->attnums[confkey[i] - 1];
+
+ conname = ChooseConstraintName(RelationGetRelationName(partRel),
+ fkaddition, "fkey",
+ RelationGetNamespace(partRel), NIL);
+
+ indexOid = index_get_partition(fchild, conform->conindid);
+
+ constrOid =
+ CreateConstraintEntry(conname,
+ RelationGetNamespace(partRel),
+ CONSTRAINT_FOREIGN,
+ fkconstraint->deferrable,
+ fkconstraint->initdeferred,
+ fkconstraint->initially_valid,
+ fk->conoid,
+ RelationGetRelid(partRel),
+ mapped_confkey,
+ numfks,
+ numfks,
+ InvalidOid,
+ indexOid,
+ childoid,
+ conkey,
+ conpfeqop,
+ conppeqop,
+ conffeqop,
+ numfks,
+ fkconstraint->fk_upd_action,
+ fkconstraint->fk_del_action,
+ confdelsetcols,
+ numfkdelsetcols,
+ fkconstraint->fk_matchtype,
+ NULL,
+ NULL,
+ NULL,
+ false,
+ 1,
+ false,
+ false);
+
+ /*
+ * Give this constraint partition-type dependencies on the
+ * parent constraint as well as the table.
+ */
+ ObjectAddressSet(address, ConstraintRelationId, constrOid);
+ ObjectAddressSet(referenced, ConstraintRelationId, fk->conoid);
+ recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_PRI);
+ ObjectAddressSet(referenced, RelationRelationId, fk->conrelid);
+ recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_SEC);
+
+ /* And now create the action triggers that go with it */
+ createForeignKeyActionTriggers(partRel, childoid,
+ fkconstraint,
+ constrOid, indexOid,
+ deleteActionTrigOid, updateActionTrigOid,
+ NULL, NULL);
+
+ table_close(fchild, NoLock);
+ }
+
+ table_close(refdRel, NoLock);
+ }
+
+ ReleaseSysCache(parentConTup);
ReleaseSysCache(contup);
}
list_free_deep(fks);
Junwang Zhao <zhjwpku@gmail.com> 于2024年7月26日周五 14:57写道:
There is a bug report[0] Tender comments might be the same
issue as this one, but I tried Alvaro's and mine patch, neither
could solve that problem, I did not tried Tender's earlier patch
thought. I post the test script below in case you are interested.
My earlier patch should handle Alexander reported case. But I did not do
more
test. I'm not sure that wether or not has dismatching between pg_constraint
and pg_trigger.
I aggred with Alvaro said that "requires a much more invasive solution".
--
Tender Wang
On 2024-Jul-26, Tender Wang wrote:
Junwang Zhao <zhjwpku@gmail.com> 于2024年7月26日周五 14:57写道:
There is a bug report[0] Tender comments might be the same issue as
this one, but I tried Alvaro's and mine patch, neither could solve
that problem, I did not tried Tender's earlier patch thought. I post
the test script below in case you are interested.My earlier patch should handle Alexander reported case. But I did not
do more test. I'm not sure that wether or not has dismatching between
pg_constraint and pg_trigger.I aggred with Alvaro said that "requires a much more invasive
solution".
Here's the patch which, as far as I can tell, fixes all the reported
problems (other than the one in bug 18541, for which I proposed an
unrelated fix in that thread[1]/messages/by-id/202408072222.icgykv5yrql5@alvherre.pgsql). If you can double-check, I would very
much appreciate that. Also, I think the test cases the patch adds
reflect the provided examples sufficiently, but if we're still failing
to cover some, please let me know.
As I understand, this fix needs to be applied all the way back to 12,
because the overall functionality is that old. However, in branches 14
and back, the patch doesn't apply cleanly, because of the changes we
made in commit f4566345cf40 :-( I'm tempted to fix it in branches 15,
16, 17, master now and potentially backpatch later, to avoid dragging
things along further. It's taken long enough already.
[1]: /messages/by-id/202408072222.icgykv5yrql5@alvherre.pgsql
--
Álvaro Herrera Breisgau, Deutschland — https://www.EnterpriseDB.com/
"World domination is proceeding according to plan" (Andrew Morton)
Attachments:
0001-Rework-foreign-key-mangling-during-ATTACH-DETACH.patchtext/x-diff; charset=utf-8Download
From 4937a327afd51cddfdad7e01f3ccd9493b893bab Mon Sep 17 00:00:00 2001
From: Alvaro Herrera <alvherre@alvh.no-ip.org>
Date: Tue, 6 Aug 2024 16:59:04 -0400
Subject: [PATCH] Rework foreign key mangling during ATTACH/DETACH
... when the reference table is partitioned.
It turns out that the catalog representation we chose for foreign keys
connecting partitioned tables is suboptimal, particularly in the sense
that a standalone table has a different way to represent it when
referencing a partitioned table, than when the same table becomes a
partition (and vice versa). This difference means we need to spawn
additional catalog rows on detach, and remove them on attach.
As a very obvious symptom, we were missing action triggers, which means
that you could update/delete rows from the referenced partitioned table
that still had referencing rows at the other side, and fail to throw the
required errors. This means existing FKs might have rows that break
relational integrity. Another possible problem is that trying to
reattach a table that had been detached would fail indicating that
internal triggers cannot be found, which from the user's point of view
is nonsensical.
We might want to rethink the representation in the future to avoid this
messiness, but the code now seems to do what's required to make the
constraints operate correctly.
co-author : Tender Wang <tndrwang@gmail.com>
Reported-by: Guillaume Lelarge <guillaume@lelarge.info>
Reported-by: Jehan-Guillaume de Rorthais <jgdr@dalibo.com>
Reported-by: Thomas Baehler (SBB CFF FFS) <thomas.baehler2@sbb.ch>
Discussion: https://postgr.es/m/20230420144344.40744130@karst
Discussion: https://postgr.es/m/20230705233028.2f554f73@karst
Discussion: https://postgr.es/m/GVAP278MB02787E7134FD691861635A8BC9032@GVAP278MB0278.CHEP278.PROD.OUTLOOK.COM
Discussion: https://postgr.es/m/18541-628a61bc267cd2d3@postgresql.org
verify: 20230707175859.17c91538@karst
---
src/backend/commands/tablecmds.c | 315 +++++++++++++++++++---
src/test/regress/expected/foreign_key.out | 59 ++++
src/test/regress/sql/foreign_key.sql | 30 +++
3 files changed, 373 insertions(+), 31 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 1f94f4fdbb..bb40541ba1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -10641,7 +10641,7 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
* Because we're only expanding the key space at the referenced side,
* we don't need to prevent any operation in the referencing table, so
* AccessShareLock suffices (assumes that dropping the constraint
- * acquires AEL).
+ * acquires AccessExclusiveLock).
*/
fkRel = table_open(constrForm->conrelid, AccessShareLock);
@@ -11147,6 +11147,83 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
partRelid);
+ /*
+ * If the referenced table is partitioned, then the partition we're
+ * attaching now has extra pg_constraint rows and action triggers that are
+ * no longer needed. Remove those.
+ */
+ if (get_rel_relkind(fk->confrelid) == RELKIND_PARTITIONED_TABLE)
+ {
+ Relation pg_constraint = table_open(ConstraintRelationId, RowShareLock);
+ ObjectAddresses *objs;
+ HeapTuple consttup;
+
+ ScanKeyInit(&key,
+ Anum_pg_constraint_conrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(fk->conrelid));
+
+ scan = systable_beginscan(pg_constraint,
+ ConstraintRelidTypidNameIndexId,
+ true, NULL, 1, &key);
+ objs = new_object_addresses();
+ while ((consttup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_constraint conform = (Form_pg_constraint) GETSTRUCT(consttup);
+
+ if (conform->conparentid != fk->conoid)
+ continue;
+ else
+ {
+ ObjectAddress addr;
+ int n;
+ SysScanDesc scan2;
+ ScanKeyData key2;
+
+ ObjectAddressSet(addr, ConstraintRelationId, conform->oid);
+ add_exact_object_address(&addr, objs);
+
+ /*
+ * First we must delete the dependency records that bind
+ * the constraint records together.
+ */
+ n = deleteDependencyRecordsForSpecific(ConstraintRelationId,
+ conform->oid,
+ DEPENDENCY_INTERNAL,
+ ConstraintRelationId,
+ fk->conoid);
+ if (n != 1)
+ elog(WARNING, "oops: found %d instead of 1 deps from %u to %u",
+ n, conform->oid, fk->conoid);
+
+ /*
+ * Now search for the triggers for this constraint and set
+ * them up for deletion too
+ */
+ ScanKeyInit(&key2,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conform->oid));
+ scan2 = systable_beginscan(trigrel, TriggerConstraintIndexId,
+ true, NULL, 1, &key2);
+ while ((trigtup = systable_getnext(scan2)) != NULL)
+ {
+ ObjectAddressSet(addr, TriggerRelationId,
+ ((Form_pg_trigger) GETSTRUCT(trigtup))->oid);
+ add_exact_object_address(&addr, objs);
+ }
+ systable_endscan(scan2);
+ }
+ }
+ /* make the dependency deletions visible */
+ CommandCounterIncrement();
+ performMultipleDeletions(objs, DROP_RESTRICT,
+ PERFORM_DELETION_INTERNAL);
+ systable_endscan(scan);
+
+ table_close(pg_constraint, RowShareLock);
+ }
+
CommandCounterIncrement();
return true;
}
@@ -19182,8 +19259,10 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
DropClonedTriggersFromPartition(RelationGetRelid(partRel));
/*
- * Detach any foreign keys that are inherited. This includes creating
- * additional action triggers.
+ * Detach any foreign keys that are inherited. This requires marking some
+ * constraints and triggers as no longer having parents, as well as
+ * creating additional constraint entries and triggers, depending on the
+ * shape of each FK.
*/
fks = copyObject(RelationGetFKeyList(partRel));
if (fks != NIL)
@@ -19192,10 +19271,15 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
{
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
+ HeapTuple parentConTup;
Form_pg_constraint conform;
+ Form_pg_constraint parentConForm;
Constraint *fkconstraint;
- Oid insertTriggerOid,
- updateTriggerOid;
+ Oid parentConstrOid;
+ Oid insertCheckTrigOid,
+ updateCheckTrigOid,
+ updateActionTrigOid,
+ deleteActionTrigOid;
contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
if (!HeapTupleIsValid(contup))
@@ -19210,52 +19294,221 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
continue;
}
- /* unset conparentid and adjust conislocal, coninhcount, etc. */
- ConstraintSetParentConstraint(fk->conoid, InvalidOid, InvalidOid);
-
/*
- * Also, look up the partition's "check" triggers corresponding to the
- * constraint being detached and detach them from the parent triggers.
+ * To create a FK constraint in the to-be-detached partition
+ * equivalent to what exists in the parent table, what to do depends
+ * on whether the referenced table is partitioned or not. If the
+ * referenced table isn't partitioned, then just reparenting the
+ * pg_constraint row and pg_trigger rows for the existing constraint,
+ * and creating new action triggers, is sufficient.
+ *
+ * If the referenced table is partitioned, then we must create
+ * additional pg_constraint rows -- one for each partition -- and the
+ * necessary action triggers for each constraint.
*/
- GetForeignKeyCheckTriggers(trigrel,
- fk->conoid, fk->confrelid, fk->conrelid,
- &insertTriggerOid, &updateTriggerOid);
- Assert(OidIsValid(insertTriggerOid));
- TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
- RelationGetRelid(partRel));
- Assert(OidIsValid(updateTriggerOid));
- TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
- RelationGetRelid(partRel));
+ parentConstrOid = conform->conparentid;
- /*
- * 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.
- */
+ Assert(OidIsValid(conform->conparentid));
+ parentConTup = SearchSysCache1(CONSTROID,
+ ObjectIdGetDatum(parentConstrOid));
+ if (!HeapTupleIsValid(parentConTup))
+ elog(ERROR, "cache lookup failed for constraint %u",
+ conform->conparentid);
+ parentConForm = (Form_pg_constraint) GETSTRUCT(parentConTup);
+
+ /* Create a synthetic node we'll use throughout */
fkconstraint = makeNode(Constraint);
fkconstraint->contype = CONSTRAINT_FOREIGN;
fkconstraint->conname = pstrdup(NameStr(conform->conname));
fkconstraint->deferrable = conform->condeferrable;
fkconstraint->initdeferred = conform->condeferred;
- fkconstraint->location = -1;
- fkconstraint->pktable = NULL;
- fkconstraint->fk_attrs = NIL;
- fkconstraint->pk_attrs = NIL;
+ fkconstraint->skip_validation = true;
+ fkconstraint->initially_valid = true;
+ /* a few irrelevant fields omitted here */
+ fkconstraint->pktable = NULL; /* not needed */
+ fkconstraint->fk_attrs = NIL; /* not needed */
+ fkconstraint->pk_attrs = NIL; /* not needed */
fkconstraint->fk_matchtype = conform->confmatchtype;
fkconstraint->fk_upd_action = conform->confupdtype;
fkconstraint->fk_del_action = conform->confdeltype;
fkconstraint->fk_del_set_cols = NIL;
fkconstraint->old_conpfeqop = NIL;
fkconstraint->old_pktable_oid = InvalidOid;
- fkconstraint->skip_validation = false;
- fkconstraint->initially_valid = true;
+ fkconstraint->location = -1;
+ /*
+ * The constraint on this table must be marked no longer a child of
+ * the parent's constraint, as do its check triggers.
+ */
+ ConstraintSetParentConstraint(fk->conoid, InvalidOid, InvalidOid);
+
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertCheckTrigOid, &updateCheckTrigOid);
+ Assert(OidIsValid(insertCheckTrigOid));
+ TriggerSetParentTrigger(trigrel, insertCheckTrigOid, InvalidOid,
+ RelationGetRelid(partRel));
+ Assert(OidIsValid(updateCheckTrigOid));
+ TriggerSetParentTrigger(trigrel, updateCheckTrigOid, InvalidOid,
+ RelationGetRelid(partRel));
+
+ /*
+ * Now create the action triggers in this table that point to the
+ * referenced table.
+ */
createForeignKeyActionTriggers(partRel, conform->confrelid,
fkconstraint, fk->conoid,
conform->conindid,
InvalidOid, InvalidOid,
- NULL, NULL);
+ &deleteActionTrigOid, &updateActionTrigOid);
+ /*
+ * If the referenced side is partitioned (which we know because our
+ * parent's constraint points to a different relation than ours) then
+ * we must, in addition to the above, create pg_constraint rows that
+ * point to each partition, each with its own action triggers.
+ */
+ if (parentConForm->conrelid != conform->conrelid)
+ {
+ List *children;
+ ListCell *child;
+ Relation refdRel;
+ Oid indexOid;
+ int numfks;
+ AttrNumber conkey[INDEX_MAX_KEYS];
+ AttrNumber confkey[INDEX_MAX_KEYS];
+ Oid conpfeqop[INDEX_MAX_KEYS];
+ Oid conppeqop[INDEX_MAX_KEYS];
+ Oid conffeqop[INDEX_MAX_KEYS];
+ int numfkdelsetcols;
+ AttrNumber confdelsetcols[INDEX_MAX_KEYS];
+ List *colnames = NIL;
+ char *fkaddition;
+
+ DeconstructFkConstraintRow(contup,
+ &numfks,
+ conkey,
+ confkey,
+ conpfeqop,
+ conppeqop,
+ conffeqop,
+ &numfkdelsetcols,
+ confdelsetcols);
+
+ /*
+ * Choose the "name2" part to give to ChooseConstraintName. This
+ * is common to all constraints we create below.
+ */
+ for (int i = 0; i < numfks; i++)
+ {
+ Form_pg_attribute att;
+
+ att = TupleDescAttr(RelationGetDescr(partRel),
+ conkey[i] - 1);
+ colnames = lappend(colnames, makeString(NameStr(att->attname)));
+ }
+ fkaddition = ChooseForeignKeyConstraintNameAddition(colnames);
+
+ /*
+ * We're not expanding nor shrinking key space, so AccessShareLock
+ * is sufficient here given that dropping a constraint requires
+ * AccessExclusiveLock.
+ */
+ refdRel = table_open(fk->confrelid, AccessShareLock);
+
+ /*
+ * When the referenced table is partitioned, we need new
+ * pg_constraint entries that point from our partition to each
+ * partition of the other table, and also the action triggers on
+ * each.
+ */
+ children = find_all_inheritors(fk->confrelid, NoLock, NULL);
+ foreach(child, children)
+ {
+ Oid childoid = lfirst_oid(child);
+ Relation fchild;
+ AttrNumber mapped_confkey[INDEX_MAX_KEYS];
+ AttrMap *attmap;
+ char *conname;
+ Oid constrOid;
+ ObjectAddress address,
+ referenced;
+
+ /* A constraint for the top rel already exists, so skip it */
+ if (childoid == fk->confrelid)
+ continue;
+
+ fchild = table_open(childoid, AccessShareLock);
+
+ /*
+ * Map the referenced attnums in case this partition has a
+ * different column layout.
+ */
+ attmap = build_attrmap_by_name(RelationGetDescr(refdRel),
+ RelationGetDescr(fchild),
+ false);
+ for (int i = 0; i < numfks; i++)
+ mapped_confkey[i] = attmap->attnums[confkey[i] - 1];
+
+ conname = ChooseConstraintName(RelationGetRelationName(partRel),
+ fkaddition, "fkey",
+ RelationGetNamespace(partRel), NIL);
+
+ /* Create the pg_constraint row to this specific partition */
+ indexOid = index_get_partition(fchild, conform->conindid);
+ constrOid =
+ CreateConstraintEntry(conname,
+ RelationGetNamespace(partRel),
+ CONSTRAINT_FOREIGN,
+ fkconstraint->deferrable,
+ fkconstraint->initdeferred,
+ fkconstraint->initially_valid,
+ fk->conoid,
+ RelationGetRelid(partRel),
+ conkey,
+ numfks,
+ numfks,
+ InvalidOid,
+ indexOid,
+ childoid,
+ mapped_confkey,
+ conpfeqop,
+ conppeqop,
+ conffeqop,
+ numfks,
+ fkconstraint->fk_upd_action,
+ fkconstraint->fk_del_action,
+ confdelsetcols,
+ numfkdelsetcols,
+ fkconstraint->fk_matchtype,
+ NULL,
+ NULL,
+ NULL,
+ false,
+ 1,
+ false,
+ false);
+
+ /* Give it a dependency on the "real" (parent) constraint */
+ ObjectAddressSet(address, ConstraintRelationId, constrOid);
+ ObjectAddressSet(referenced, ConstraintRelationId, fk->conoid);
+ recordDependencyOn(&address, &referenced, DEPENDENCY_INTERNAL);
+
+ /* And create the action triggers that go with it */
+ createForeignKeyActionTriggers(partRel, childoid,
+ fkconstraint,
+ constrOid, indexOid,
+ deleteActionTrigOid,
+ updateActionTrigOid,
+ NULL, NULL);
+
+ table_close(fchild, NoLock);
+ }
+
+ table_close(refdRel, NoLock);
+ }
+
+ ReleaseSysCache(parentConTup);
ReleaseSysCache(contup);
}
list_free_deep(fks);
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 46764bd9e3..2fc280d3bd 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -2917,3 +2917,62 @@ DETAIL: drop cascades to table fkpart11.pk
drop cascades to table fkpart11.fk_parted
drop cascades to table fkpart11.fk_another
drop cascades to function fkpart11.print_row()
+-- When a table is attached as partition to a partitioned table that has
+-- a foreign key to another partitioned table, it acquires a clone of the
+-- FK. Upon detach, this clone is not removed, but instead becomes an
+-- independent FK. If it then attaches to the partitioned table again,
+-- the FK from the parent "takes over" ownership of the independent FK rather
+-- than creating a separate one.
+CREATE SCHEMA fkpart12
+ CREATE TABLE fk_p ( id bigint PRIMARY KEY ) PARTITION BY list (id)
+ CREATE TABLE fk_p_1 PARTITION OF fk_p FOR VALUES IN (1)
+ CREATE TABLE fk_p_2 PARTITION OF fk_p FOR VALUES IN (2)
+ CREATE TABLE fk_r_1 ( id bigint PRIMARY KEY, p_id bigint NOT NULL)
+ CREATE TABLE fk_r_2 ( id bigint PRIMARY KEY, p_id bigint NOT NULL)
+ CREATE TABLE fk_r ( id bigint PRIMARY KEY, p_id bigint NOT NULL,
+ FOREIGN KEY (p_id) REFERENCES fk_p (id)
+ ) PARTITION BY list (id);
+SET search_path TO fkpart12;
+ALTER TABLE fk_r ATTACH PARTITION fk_r_2 FOR VALUES IN (2);
+ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1);
+\d fk_r_1
+ Table "fkpart12.fk_r_1"
+ Column | Type | Collation | Nullable | Default
+--------+--------+-----------+----------+---------
+ id | bigint | | not null |
+ p_id | bigint | | not null |
+Partition of: fk_r FOR VALUES IN ('1')
+Indexes:
+ "fk_r_1_pkey" PRIMARY KEY, btree (id)
+Foreign-key constraints:
+ TABLE "fk_r" CONSTRAINT "fk_r_p_id_fkey" FOREIGN KEY (p_id) REFERENCES fk_p(id)
+
+ALTER TABLE fk_r DETACH PARTITION fk_r_1;
+\d fk_r_1
+ Table "fkpart12.fk_r_1"
+ Column | Type | Collation | Nullable | Default
+--------+--------+-----------+----------+---------
+ id | bigint | | not null |
+ p_id | bigint | | not null |
+Indexes:
+ "fk_r_1_pkey" PRIMARY KEY, btree (id)
+Foreign-key constraints:
+ "fk_r_p_id_fkey" FOREIGN KEY (p_id) REFERENCES fk_p(id)
+
+ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1);
+\d fk_r_1
+ Table "fkpart12.fk_r_1"
+ Column | Type | Collation | Nullable | Default
+--------+--------+-----------+----------+---------
+ id | bigint | | not null |
+ p_id | bigint | | not null |
+Partition of: fk_r FOR VALUES IN ('1')
+Indexes:
+ "fk_r_1_pkey" PRIMARY KEY, btree (id)
+Foreign-key constraints:
+ TABLE "fk_r" CONSTRAINT "fk_r_p_id_fkey" FOREIGN KEY (p_id) REFERENCES fk_p(id)
+
+SET client_min_messages TO warning;
+DROP SCHEMA fkpart12 CASCADE;
+RESET client_min_messages;
+RESET search_path;
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index f5e0938999..359eb6eefe 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -2069,3 +2069,33 @@ UPDATE fkpart11.pk SET a = 3 WHERE a = 4;
UPDATE fkpart11.pk SET a = 1 WHERE a = 2;
DROP SCHEMA fkpart11 CASCADE;
+
+-- When a table is attached as partition to a partitioned table that has
+-- a foreign key to another partitioned table, it acquires a clone of the
+-- FK. Upon detach, this clone is not removed, but instead becomes an
+-- independent FK. If it then attaches to the partitioned table again,
+-- the FK from the parent "takes over" ownership of the independent FK rather
+-- than creating a separate one.
+CREATE SCHEMA fkpart12
+ CREATE TABLE fk_p ( id bigint PRIMARY KEY ) PARTITION BY list (id)
+ CREATE TABLE fk_p_1 PARTITION OF fk_p FOR VALUES IN (1)
+ CREATE TABLE fk_p_2 PARTITION OF fk_p FOR VALUES IN (2)
+ CREATE TABLE fk_r_1 ( id bigint PRIMARY KEY, p_id bigint NOT NULL)
+ CREATE TABLE fk_r_2 ( id bigint PRIMARY KEY, p_id bigint NOT NULL)
+ CREATE TABLE fk_r ( id bigint PRIMARY KEY, p_id bigint NOT NULL,
+ FOREIGN KEY (p_id) REFERENCES fk_p (id)
+ ) PARTITION BY list (id);
+SET search_path TO fkpart12;
+ALTER TABLE fk_r ATTACH PARTITION fk_r_2 FOR VALUES IN (2);
+
+ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1);
+\d fk_r_1
+ALTER TABLE fk_r DETACH PARTITION fk_r_1;
+\d fk_r_1
+ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1);
+\d fk_r_1
+
+SET client_min_messages TO warning;
+DROP SCHEMA fkpart12 CASCADE;
+RESET client_min_messages;
+RESET search_path;
--
2.39.2
Alvaro Herrera <alvherre@alvh.no-ip.org> 于2024年8月8日周四 06:50写道:
On 2024-Jul-26, Tender Wang wrote:
Junwang Zhao <zhjwpku@gmail.com> 于2024年7月26日周五 14:57写道:
There is a bug report[0] Tender comments might be the same issue as
this one, but I tried Alvaro's and mine patch, neither could solve
that problem, I did not tried Tender's earlier patch thought. I post
the test script below in case you are interested.My earlier patch should handle Alexander reported case. But I did not
do more test. I'm not sure that wether or not has dismatching between
pg_constraint and pg_trigger.I aggred with Alvaro said that "requires a much more invasive
solution".Here's the patch which, as far as I can tell, fixes all the reported
problems (other than the one in bug 18541, for which I proposed an
unrelated fix in that thread[1]). If you can double-check, I would very
much appreciate that. Also, I think the test cases the patch adds
reflect the provided examples sufficiently, but if we're still failing
to cover some, please let me know.
Thanks for your work. I will take some time to look it in detail.
--
Tender Wang
Alvaro Herrera <alvherre@alvh.no-ip.org> 于2024年8月8日周四 06:50写道:
On 2024-Jul-26, Tender Wang wrote:
Junwang Zhao <zhjwpku@gmail.com> 于2024年7月26日周五 14:57写道:
There is a bug report[0] Tender comments might be the same issue as
this one, but I tried Alvaro's and mine patch, neither could solve
that problem, I did not tried Tender's earlier patch thought. I post
the test script below in case you are interested.My earlier patch should handle Alexander reported case. But I did not
do more test. I'm not sure that wether or not has dismatching between
pg_constraint and pg_trigger.I aggred with Alvaro said that "requires a much more invasive
solution".Here's the patch which, as far as I can tell, fixes all the reported
problems (other than the one in bug 18541, for which I proposed an
unrelated fix in that thread[1]). If you can double-check, I would very
much appreciate that. Also, I think the test cases the patch adds
reflect the provided examples sufficiently, but if we're still failing
to cover some, please let me know.
I did a lot of tests, and did not report error and did not find any
warnings using oidjoins.sql.
+1
--
Tender Wang
Alvaro Herrera <alvherre@alvh.no-ip.org> 于2024年8月8日周四 06:50写道:
On 2024-Jul-26, Tender Wang wrote:
Junwang Zhao <zhjwpku@gmail.com> 于2024年7月26日周五 14:57写道:
There is a bug report[0] Tender comments might be the same issue as
this one, but I tried Alvaro's and mine patch, neither could solve
that problem, I did not tried Tender's earlier patch thought. I post
the test script below in case you are interested.My earlier patch should handle Alexander reported case. But I did not
do more test. I'm not sure that wether or not has dismatching between
pg_constraint and pg_trigger.I aggred with Alvaro said that "requires a much more invasive
solution".Here's the patch which, as far as I can tell, fixes all the reported
problems (other than the one in bug 18541, for which I proposed an
unrelated fix in that thread[1]). If you can double-check, I would very
much appreciate that. Also, I think the test cases the patch adds
reflect the provided examples sufficiently, but if we're still failing
to cover some, please let me know.As I understand, this fix needs to be applied all the way back to 12,
because the overall functionality is that old. However, in branches 14
and back, the patch doesn't apply cleanly, because of the changes we
made in commit f4566345cf40 :-( I'm tempted to fix it in branches 15,
16, 17, master now and potentially backpatch later, to avoid dragging
things along further. It's taken long enough already.
I haven't seen this patch on master. Is there something that we fotget to
handle?
--
Tender Wang
On 2024-Aug-20, Tender Wang wrote:
As I understand, this fix needs to be applied all the way back to 12,
because the overall functionality is that old. However, in branches 14
and back, the patch doesn't apply cleanly, because of the changes we
made in commit f4566345cf40 :-( I'm tempted to fix it in branches 15,
16, 17, master now and potentially backpatch later, to avoid dragging
things along further. It's taken long enough already.I haven't seen this patch on master. Is there something that we fotget to
handle?
I haven't pushed it yet, mostly because of being unsure about not doing
anything for the oldest branches (14 and back).
--
Álvaro Herrera 48°01'N 7°57'E — https://www.EnterpriseDB.com/
"Java is clearly an example of money oriented programming" (A. Stepanov)
Alvaro Herrera <alvherre@alvh.no-ip.org> 于2024年8月20日周二 10:25写道:
On 2024-Aug-20, Tender Wang wrote:
As I understand, this fix needs to be applied all the way back to 12,
because the overall functionality is that old. However, in branches 14
and back, the patch doesn't apply cleanly, because of the changes we
made in commit f4566345cf40 :-( I'm tempted to fix it in branches 15,
16, 17, master now and potentially backpatch later, to avoid dragging
things along further. It's taken long enough already.I haven't seen this patch on master. Is there something that we fotget
to
handle?
I haven't pushed it yet, mostly because of being unsure about not doing
anything for the oldest branches (14 and back).
I only did codes and tests on master. I'm not sure how much complicated it
would be
to fix this issue on branches 14 and back.
--
Tender Wang
On Wed, 7 Aug 2024 18:50:10 -0400
Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
On 2024-Jul-26, Tender Wang wrote:
Junwang Zhao <zhjwpku@gmail.com> 于2024年7月26日周五 14:57写道:
There is a bug report[0] Tender comments might be the same issue as
this one, but I tried Alvaro's and mine patch, neither could solve
that problem, I did not tried Tender's earlier patch thought. I post
the test script below in case you are interested.My earlier patch should handle Alexander reported case. But I did not
do more test. I'm not sure that wether or not has dismatching between
pg_constraint and pg_trigger.I aggred with Alvaro said that "requires a much more invasive
solution".Here's the patch which, as far as I can tell, fixes all the reported
problems (other than the one in bug 18541, for which I proposed an
unrelated fix in that thread[1]). If you can double-check, I would very
much appreciate that. Also, I think the test cases the patch adds
reflect the provided examples sufficiently, but if we're still failing
to cover some, please let me know.
I'm back on this issue as well. I start poking at this patch to review it,
test it, challenge it and then report here.
I'll try to check if some other issues might have lost/forgot on they way as
well.
In the meantime, thank you Alvaro, Tender and Junwang for your work, time,
research and patches!
On 2024-Aug-20, Jehan-Guillaume de Rorthais wrote:
I'm back on this issue as well. I start poking at this patch to review it,
test it, challenge it and then report here.I'll try to check if some other issues might have lost/forgot on they way as
well.
Thanks, much appreciated, looking forward to your feedback.
--
Álvaro Herrera Breisgau, Deutschland — https://www.EnterpriseDB.com/
On 2024-Aug-19, Alvaro Herrera wrote:
I haven't pushed it yet, mostly because of being unsure about not doing
anything for the oldest branches (14 and back).
Last night, after much mulling on this, it occurred to me that one easy
way out of this problem for the old branches, without having to write
more code, is to simply remove the constraint from the partition when
it's detached (but only if they reference a partitioned relation). It's
not a great solution, but at least we're no longer leaving bogus catalog
entries around. That would be like the attached patch, which was cut
from 14 and applies cleanly to 12 and 13. I'd throw in a couple of
tests and call it a day.
(TBH the idea of leaving the partition without a foreign key feels to me
like travelling in a car without a seat belt -- it feels instinctively
dangerous. This is why I went such lengths to keep FKs on detach
initially. But I'm not inclined to spend more time on this issue.
However ... what about fixing catalog content that's already broken
after past detach ops?)
--
Álvaro Herrera PostgreSQL Developer — https://www.EnterpriseDB.com/
Attachments:
v14-0001-drop-constraint-when-detaching.patchtext/x-diff; charset=utf-8Download
From e95fde451ee73b89f1a5ba50b7f2e34f5fb98bde Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=81lvaro=20Herrera?= <alvherre@alvh.no-ip.org>
Date: Wed, 21 Aug 2024 17:47:03 -0400
Subject: [PATCH v14] drop constraint when detaching
---
src/backend/commands/tablecmds.c | 79 +++++++++++++++++++++-----------
1 file changed, 53 insertions(+), 26 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c39e6799ca..7ef904f63e 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -18143,8 +18143,8 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
DropClonedTriggersFromPartition(RelationGetRelid(partRel));
/*
- * Detach any foreign keys that are inherited. This includes creating
- * additional action triggers.
+ * Detach any foreign keys that are inherited -- or, if they reference
+ * partitioned tables, drop them.
*/
fks = copyObject(RelationGetFKeyList(partRel));
foreach(cell, fks)
@@ -18152,7 +18152,6 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
Form_pg_constraint conform;
- Constraint *fkconstraint;
contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
if (!HeapTupleIsValid(contup))
@@ -18167,34 +18166,62 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
continue;
}
- /* unset conparentid and adjust conislocal, coninhcount, etc. */
+ /* Mark the constraint as independent */
ConstraintSetParentConstraint(fk->conoid, InvalidOid, 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.
+ * If the constraint references a partitioned table, just drop the
+ * constraint, because it's more work to preserve the constraint
+ * correctly.
+ *
+ * If it references a plain table, then we can create the action
+ * triggers and it'll be okay.
*/
- fkconstraint = makeNode(Constraint);
- fkconstraint->contype = CONSTRAINT_FOREIGN;
- fkconstraint->conname = pstrdup(NameStr(conform->conname));
- fkconstraint->deferrable = conform->condeferrable;
- fkconstraint->initdeferred = conform->condeferred;
- fkconstraint->location = -1;
- fkconstraint->pktable = NULL;
- fkconstraint->fk_attrs = NIL;
- fkconstraint->pk_attrs = NIL;
- fkconstraint->fk_matchtype = conform->confmatchtype;
- fkconstraint->fk_upd_action = conform->confupdtype;
- fkconstraint->fk_del_action = conform->confdeltype;
- fkconstraint->old_conpfeqop = NIL;
- fkconstraint->old_pktable_oid = InvalidOid;
- fkconstraint->skip_validation = false;
- fkconstraint->initially_valid = true;
+ if (get_rel_relkind(fk->confrelid) == RELKIND_PARTITIONED_TABLE)
+ {
+ ObjectAddress constraddr;
- createForeignKeyActionTriggers(partRel, conform->confrelid,
- fkconstraint, fk->conoid,
- conform->conindid);
+ /* make the dependency deletions above visible */
+ CommandCounterIncrement();
+
+ /*
+ * And drop the constraint and its triggers; if this partition is
+ * partitioned, then the corresponding objects in the partitions
+ * too.
+ */
+ ObjectAddressSet(constraddr, ConstraintRelationId, fk->conoid);
+ performDeletion(&constraddr, DROP_CASCADE, 0);
+ }
+ else
+ {
+ Constraint *fkconstraint;
+
+ /*
+ * Make the action triggers on the referenced relation. When this
+ * was a partition the action triggers pointed to the parent rel
+ * (they still do), but now we need separate ones of our own.
+ */
+ fkconstraint = makeNode(Constraint);
+ fkconstraint->contype = CONSTRAINT_FOREIGN;
+ fkconstraint->conname = pstrdup(NameStr(conform->conname));
+ fkconstraint->deferrable = conform->condeferrable;
+ fkconstraint->initdeferred = conform->condeferred;
+ fkconstraint->location = -1;
+ fkconstraint->pktable = NULL;
+ fkconstraint->fk_attrs = NIL;
+ fkconstraint->pk_attrs = NIL;
+ fkconstraint->fk_matchtype = conform->confmatchtype;
+ fkconstraint->fk_upd_action = conform->confupdtype;
+ fkconstraint->fk_del_action = conform->confdeltype;
+ fkconstraint->old_conpfeqop = NIL;
+ fkconstraint->old_pktable_oid = InvalidOid;
+ fkconstraint->skip_validation = false;
+ fkconstraint->initially_valid = true;
+
+ createForeignKeyActionTriggers(partRel, conform->confrelid,
+ fkconstraint, fk->conoid,
+ conform->conindid);
+ }
ReleaseSysCache(contup);
}
--
2.39.2
Alvaro Herrera <alvherre@alvh.no-ip.org> 于2024年8月22日周四 06:00写道:
On 2024-Aug-19, Alvaro Herrera wrote:
I haven't pushed it yet, mostly because of being unsure about not doing
anything for the oldest branches (14 and back).Last night, after much mulling on this, it occurred to me that one easy
way out of this problem for the old branches, without having to write
more code, is to simply remove the constraint from the partition when
it's detached (but only if they reference a partitioned relation). It's
not a great solution, but at least we're no longer leaving bogus catalog
entries around. That would be like the attached patch, which was cut
from 14 and applies cleanly to 12 and 13. I'd throw in a couple of
tests and call it a day.
I apply the v14 patch on branch REL_14_STABLE. I run this thread issue and I
find below error.
postgres=# CREATE TABLE p ( id bigint PRIMARY KEY ) PARTITION BY list (id);
CREATE TABLE p_1 PARTITION OF p FOR VALUES IN (1);
CREATE TABLE r_1 (
id bigint PRIMARY KEY,
p_id bigint NOT NULL,
FOREIGN KEY (p_id) REFERENCES p (id)
);
CREATE TABLE r (
id bigint PRIMARY KEY,
p_id bigint NOT NULL,
FOREIGN KEY (p_id) REFERENCES p (id)
) PARTITION BY list (id);
ALTER TABLE r ATTACH PARTITION r_1 FOR VALUES IN (1);
ALTER TABLE r DETACH PARTITION r_1;
CREATE TABLE
CREATE TABLE
CREATE TABLE
CREATE TABLE
ALTER TABLE
ERROR: cache lookup failed for constraint 16400
I haven't look into details to find out where cause above error.
--
Tender Wang
Tender Wang <tndrwang@gmail.com> 于2024年8月22日周四 11:19写道:
Alvaro Herrera <alvherre@alvh.no-ip.org> 于2024年8月22日周四 06:00写道:
On 2024-Aug-19, Alvaro Herrera wrote:
I haven't pushed it yet, mostly because of being unsure about not doing
anything for the oldest branches (14 and back).Last night, after much mulling on this, it occurred to me that one easy
way out of this problem for the old branches, without having to write
more code, is to simply remove the constraint from the partition when
it's detached (but only if they reference a partitioned relation). It's
not a great solution, but at least we're no longer leaving bogus catalog
entries around. That would be like the attached patch, which was cut
from 14 and applies cleanly to 12 and 13. I'd throw in a couple of
tests and call it a day.I apply the v14 patch on branch REL_14_STABLE. I run this thread issue and
I
find below error.
postgres=# CREATE TABLE p ( id bigint PRIMARY KEY ) PARTITION BY list (id);
CREATE TABLE p_1 PARTITION OF p FOR VALUES IN (1);
CREATE TABLE r_1 (
id bigint PRIMARY KEY,
p_id bigint NOT NULL,
FOREIGN KEY (p_id) REFERENCES p (id)
);
CREATE TABLE r (
id bigint PRIMARY KEY,
p_id bigint NOT NULL,
FOREIGN KEY (p_id) REFERENCES p (id)
) PARTITION BY list (id);
ALTER TABLE r ATTACH PARTITION r_1 FOR VALUES IN (1);
ALTER TABLE r DETACH PARTITION r_1;
CREATE TABLE
CREATE TABLE
CREATE TABLE
CREATE TABLE
ALTER TABLE
ERROR: cache lookup failed for constraint 16400
I guess it is because cascade dropping, the oid=16400 has been deleted.
Adding a list that remember dropped constraint, if we find the parent of
constraint
is in the list, we skip.
By the way, I run above SQL sequences on REL_14_STABLE without your partch.
I didn't find reporting error, and running oidjoins.sql didn't report
warnings.
Do I miss something?
--
Tender Wang
On 2024-Aug-22, Tender Wang wrote:
I apply the v14 patch on branch REL_14_STABLE. I run this thread issue and I
find below error.
[...]
ERROR: cache lookup failed for constraint 16400I haven't look into details to find out where cause above error.
Right, we try to drop the constraint twice. We can dodge this by
collecting all constraints to drop in the loop and process them in a
single performMultipleDeletions, as in the attached v14-2.
TBH I think it's a bit infuriating that we lose the constraint (which
was explicitly declared) because of ATTACH/DETACH. So the behavior of
v15 and above is better.
By the way, I run above SQL sequences on REL_14_STABLE without your
partch. I didn't find reporting error, and running oidjoins.sql
didn't report warnings. Do I miss something?
I think the action triggers are missing, so if you keep rows in the r_1
table after you've detached them, you can still delete them from the
referenced table p, instead of getting the error that you should get.
--
Álvaro Herrera PostgreSQL Developer — https://www.EnterpriseDB.com/
"In fact, the basic problem with Perl 5's subroutines is that they're not
crufty enough, so the cruft leaks out into user-defined code instead, by
the Conservation of Cruft Principle." (Larry Wall, Apocalypse 6)
Attachments:
v14-2-0001-drop-constraint-when-detaching.patchtext/x-diff; charset=utf-8Download
From 43fc7215c1519e698fe1d58edd46f87da8529186 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=81lvaro=20Herrera?= <alvherre@alvh.no-ip.org>
Date: Wed, 21 Aug 2024 17:47:03 -0400
Subject: [PATCH v14-2] drop constraint when detaching
---
src/backend/commands/tablecmds.c | 84 ++++++++++++++++++++++----------
1 file changed, 58 insertions(+), 26 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c39e6799ca..3a8ffe30eb 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -18129,6 +18129,7 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
new_repl[Natts_pg_class];
HeapTuple tuple,
newtuple;
+ ObjectAddresses *dropobjs = NULL;
if (concurrent)
{
@@ -18143,8 +18144,8 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
DropClonedTriggersFromPartition(RelationGetRelid(partRel));
/*
- * Detach any foreign keys that are inherited. This includes creating
- * additional action triggers.
+ * Detach any foreign keys that are inherited -- or, if they reference
+ * partitioned tables, drop them.
*/
fks = copyObject(RelationGetFKeyList(partRel));
foreach(cell, fks)
@@ -18152,7 +18153,6 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
Form_pg_constraint conform;
- Constraint *fkconstraint;
contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
if (!HeapTupleIsValid(contup))
@@ -18167,39 +18167,71 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
continue;
}
- /* unset conparentid and adjust conislocal, coninhcount, etc. */
+ /* Mark the constraint as independent */
ConstraintSetParentConstraint(fk->conoid, InvalidOid, 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.
+ * If the constraint references a partitioned table, just drop the
+ * constraint, because it's more work to preserve the constraint
+ * correctly.
+ *
+ * If it references a plain table, then we can create the action
+ * triggers and it'll be okay.
*/
- fkconstraint = makeNode(Constraint);
- fkconstraint->contype = CONSTRAINT_FOREIGN;
- fkconstraint->conname = pstrdup(NameStr(conform->conname));
- fkconstraint->deferrable = conform->condeferrable;
- fkconstraint->initdeferred = conform->condeferred;
- fkconstraint->location = -1;
- fkconstraint->pktable = NULL;
- fkconstraint->fk_attrs = NIL;
- fkconstraint->pk_attrs = NIL;
- fkconstraint->fk_matchtype = conform->confmatchtype;
- fkconstraint->fk_upd_action = conform->confupdtype;
- fkconstraint->fk_del_action = conform->confdeltype;
- fkconstraint->old_conpfeqop = NIL;
- fkconstraint->old_pktable_oid = InvalidOid;
- fkconstraint->skip_validation = false;
- fkconstraint->initially_valid = true;
+ if (get_rel_relkind(fk->confrelid) == RELKIND_PARTITIONED_TABLE)
+ {
+ ObjectAddress constraddr;
- createForeignKeyActionTriggers(partRel, conform->confrelid,
- fkconstraint, fk->conoid,
- conform->conindid);
+ /* make the dependency deletions above visible */
+ CommandCounterIncrement();
+
+ /*
+ * Remember the constraint and its triggers for later deletion.
+ */
+ if (dropobjs == NULL)
+ dropobjs = new_object_addresses();
+ ObjectAddressSet(constraddr, ConstraintRelationId, fk->conoid);
+ add_exact_object_address(&constraddr, dropobjs);
+ }
+ else
+ {
+ Constraint *fkconstraint;
+
+ /*
+ * Make the action triggers on the referenced relation. When this
+ * was a partition the action triggers pointed to the parent rel
+ * (they still do), but now we need separate ones of our own.
+ */
+ fkconstraint = makeNode(Constraint);
+ fkconstraint->contype = CONSTRAINT_FOREIGN;
+ fkconstraint->conname = pstrdup(NameStr(conform->conname));
+ fkconstraint->deferrable = conform->condeferrable;
+ fkconstraint->initdeferred = conform->condeferred;
+ fkconstraint->location = -1;
+ fkconstraint->pktable = NULL;
+ fkconstraint->fk_attrs = NIL;
+ fkconstraint->pk_attrs = NIL;
+ fkconstraint->fk_matchtype = conform->confmatchtype;
+ fkconstraint->fk_upd_action = conform->confupdtype;
+ fkconstraint->fk_del_action = conform->confdeltype;
+ fkconstraint->old_conpfeqop = NIL;
+ fkconstraint->old_pktable_oid = InvalidOid;
+ fkconstraint->skip_validation = false;
+ fkconstraint->initially_valid = true;
+
+ createForeignKeyActionTriggers(partRel, conform->confrelid,
+ fkconstraint, fk->conoid,
+ conform->conindid);
+ }
ReleaseSysCache(contup);
}
list_free_deep(fks);
+ /* If we collected any constraints for deletion, do so now. */
+ if (dropobjs != NULL)
+ performMultipleDeletions(dropobjs, DROP_CASCADE, 0);
+
/*
* Any sub-constraints that are in the referenced-side of a larger
* constraint have to be removed. This partition is no longer part of the
--
2.39.2
Alvaro Herrera <alvherre@alvh.no-ip.org> 于2024年8月23日周五 02:41写道:
On 2024-Aug-22, Tender Wang wrote:
I apply the v14 patch on branch REL_14_STABLE. I run this thread issue
and I
find below error.
[...]
ERROR: cache lookup failed for constraint 16400I haven't look into details to find out where cause above error.
Right, we try to drop the constraint twice. We can dodge this by
collecting all constraints to drop in the loop and process them in a
single performMultipleDeletions, as in the attached v14-2.
Can we move the CommandCounterIncrement() in
if (get_rel_relkind(fk->confrelid) == RELKIND_PARTITIONED_TABLE) block
to be close to performMultipleDeletions().
Others look good to me.
TBH I think it's a bit infuriating that we lose the constraint (which
was explicitly declared) because of ATTACH/DETACH.
Agree.
Do you think it is friendly to users if we add hints that inform them the
constraint was dropped?
--
Tender Wang
Hi,
On Tue, 20 Aug 2024 23:09:27 -0400
Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
On 2024-Aug-20, Jehan-Guillaume de Rorthais wrote:
I'm back on this issue as well. I start poking at this patch to review it,
test it, challenge it and then report here.I'll try to check if some other issues might have lost/forgot on they way as
well.Thanks, much appreciated, looking forward to your feedback.
Sorry, it took me a while to come back to you on this topic. It has been hard to
untangle subjects, reproductions and patch…
There's three distinct issues/thread:
* Constraint & trigger catalog cleanup [1]/messages/by-id/20230705233028.2f554f73@karst (this thread)
* FK broken after DETACH [2]/messages/by-id/20230420144344.40744130@karst
* Maintenance consideration about self referencing FK between partitions [3]/messages/by-id/20230707175859.17c91538@karst
0. Splitting in two commits
Your patch addresses two bugs:
* one for the constraint & trigger catalog cleanup;
* one for the FK broken after DETACH.
These issues are unrelated, therefore I am wondering if it would be better
to split their resolution in two different patches.
Last year, I reported them in two different threads [1]/messages/by-id/20230705233028.2f554f73@karst[2]/messages/by-id/20230420144344.40744130@karst. The first with
implementation consideration, the second with a demo/proposal/draft fix.
Unfortunately, this discussion about the first bug slipped to the second one
when Tender stumbled on this bug as well and reported it. But, both bugs can
be triggered independently, and have distinct fixes.
Finally, splitting the patch might help setting finer patch co-authoring. I
know my patch for [2]/messages/by-id/20230420144344.40744130@karst was a draft and somewhat trivial, but I spend a fair
amount of time to report, then produce a draft patch, so I was wondering if
it would be candidate to a co-author flag on this (small, humble and
refactored by you) patch?
I'm definitely not involved (yet) in the second part though.
1. Constraint & trigger catalog cleanup [1]/messages/by-id/20230705233028.2f554f73@karst
I have been focusing on the current master branch and haven't taken into
consideration backpatching related issues yet.
When I first studied this bug and reported it, I held on writing a patch
because it seemed it would duplicate some existing code. I wrote:
I poked around DetachPartitionFinalize() to try to find a way to fix this,
but it looks like it would duplicate a bunch of code from other code path
(eg. from CloneFkReferenced).
My proposal was to clean everything related to the old FK and use some
existing code path to create a fresh and cleaner one. This requires some
refactoring in existing code, but we would win a common path of code between
create/attach/detach, a cleaner catalog and easier code maintenance.
I've finally been able to write a PoC that implement this by calling
addFkRecurseReferenced() from DetachPartitionFinalize(). I can't join
it here because it is currently an ugly draft and I still have some work
to do. But I would really like to have a little more time (one or two days) to
explore this avenue further before you commit yours, if you don't mind? Or
maybe you already have considered this avenue and rejected it?
2. FK broken after DETACH [2]/messages/by-id/20230420144344.40744130@karst
Comparing your patch to my draft from [2]/messages/by-id/20230420144344.40744130@karst, I just have a question about the
refactoring.
Fencing the constraint/trigger removal inside a conditional
RELKIND_PARTITIONED_TABLE block of code was obvious. It avoids some useless
catalog scan compared to my draft patch.
Also, the "contype == CONSTRAINT_FOREIGN" I had sounds safe to remove.
However, is it clean/light enough to add the "conparentid == fk->conoid" in
the scan key as I did? I'm not sure it saves anything else but the small
conditional block you inserted inside the loop, but I wonder if there's a
serious concern about this anyway?
Last, considering the tests, I think we should add some rows in the tables,
to make sure the FK is correctly enforced after DETACH. Something like:
CREATE SCHEMA fkpart12
CREATE TABLE fk_p ( id bigint PRIMARY KEY ) PARTITION BY list (id)
CREATE TABLE fk_p_1 PARTITION OF fk_p FOR VALUES IN (1)
CREATE TABLE fk_p_2 PARTITION OF fk_p FOR VALUES IN (2)
CREATE TABLE fk_r_1 ( id bigint PRIMARY KEY, p_id bigint NOT NULL)
CREATE TABLE fk_r_2 ( id bigint PRIMARY KEY, p_id bigint NOT NULL)
CREATE TABLE fk_r ( id bigint PRIMARY KEY, p_id bigint NOT NULL,
FOREIGN KEY (p_id) REFERENCES fk_p (id)
) PARTITION BY list (id);
SET search_path TO fkpart12;
INSERT INTO fk_p VALUES (1);
ALTER TABLE fk_r ATTACH PARTITION fk_r_2 FOR VALUES IN (2);
ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1);
\d fk_r_1
INSERT INTO fk_r VALUES (1,1);
ALTER TABLE fk_r DETACH PARTITION fk_r_1;
\d fk_r_1
INSERT INTO c_1 VALUES (2,2); -- fails as EXPECTED
DELETE FROM p; -- should fails but was buggy
ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1);
\d fk_r_1
3. Self referencing FK between partitions [3]/messages/by-id/20230707175859.17c91538@karst
You added to your commit message:
verify: 20230707175859.17c91538@karst
I'm not sure what the "verify" flag means. Unfortunately, your patch doesn't
help on this topic.
This bug really needs more discussion and design consideration. I have
thought about this problem and haven't found any solution that don't involve
breaking the current core behavior. It really looks like an impossible bug to
fix without dropping the constraint itself. On both side. Maybe the only sane
behavior would be to forbid detaching the partition if it would break the
constraint.
But let's discuss this on the related thread, should we?
Thank you for reading me all the way down to the bottom!
Regards,
[1]: /messages/by-id/20230705233028.2f554f73@karst
[2]: /messages/by-id/20230420144344.40744130@karst
[3]: /messages/by-id/20230707175859.17c91538@karst
Jehan-Guillaume de Rorthais <jgdr@dalibo.com> 于2024年9月3日周二 05:02写道:
Hi,
On Tue, 20 Aug 2024 23:09:27 -0400
Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:On 2024-Aug-20, Jehan-Guillaume de Rorthais wrote:
I'm back on this issue as well. I start poking at this patch to review
it,
test it, challenge it and then report here.
I'll try to check if some other issues might have lost/forgot on they
way as
well.
Thanks, much appreciated, looking forward to your feedback.
Sorry, it took me a while to come back to you on this topic. It has been
hard to
untangle subjects, reproductions and patch…There's three distinct issues/thread:
* Constraint & trigger catalog cleanup [1] (this thread)
* FK broken after DETACH [2]
* Maintenance consideration about self referencing FK between partitions
[3]
The third issue has been fixed, and codes have been pushed. Because of my
misunderstanding,
It should not be here.
0. Splitting in two commits
Your patch addresses two bugs:
* one for the constraint & trigger catalog cleanup;
* one for the FK broken after DETACH.These issues are unrelated, therefore I am wondering if it would be
better
to split their resolution in two different patches.Last year, I reported them in two different threads [1][2]. The first
with
implementation consideration, the second with a demo/proposal/draft fix.Unfortunately, this discussion about the first bug slipped to the second
one
when Tender stumbled on this bug as well and reported it. But, both bugs
can
be triggered independently, and have distinct fixes.
It's ok that these two issues are fixed together. It is because current
codes don't handle better
when the referenced side is the partition table.
Finally, splitting the patch might help setting finer patch
co-authoring. I
know my patch for [2] was a draft and somewhat trivial, but I spend a
fair
amount of time to report, then produce a draft patch, so I was wondering
if
it would be candidate to a co-author flag on this (small, humble and
refactored by you) patch?I'm definitely not involved (yet) in the second part though.
1. Constraint & trigger catalog cleanup [1]
I have been focusing on the current master branch and haven't taken into
consideration backpatching related issues yet.When I first studied this bug and reported it, I held on writing a patch
because it seemed it would duplicate some existing code. I wrote:I poked around DetachPartitionFinalize() to try to find a way to fix
this,
but it looks like it would duplicate a bunch of code from other code
path
(eg. from CloneFkReferenced).
My proposal was to clean everything related to the old FK and use some
existing code path to create a fresh and cleaner one. This requires some
refactoring in existing code, but we would win a common path of code
between
create/attach/detach, a cleaner catalog and easier code maintenance.I've finally been able to write a PoC that implement this by calling
addFkRecurseReferenced() from DetachPartitionFinalize(). I can't join
it here because it is currently an ugly draft and I still have some work
to do. But I would really like to have a little more time (one or two
days) to
explore this avenue further before you commit yours, if you don't mind?
Or
maybe you already have considered this avenue and rejected it?2. FK broken after DETACH [2]
Comparing your patch to my draft from [2], I just have a question about
the
refactoring.Fencing the constraint/trigger removal inside a conditional
RELKIND_PARTITIONED_TABLE block of code was obvious. It avoids some
useless
catalog scan compared to my draft patch.Also, the "contype == CONSTRAINT_FOREIGN" I had sounds safe to remove.
However, is it clean/light enough to add the "conparentid == fk->conoid"
in
the scan key as I did? I'm not sure it saves anything else but the small
conditional block you inserted inside the loop, but I wonder if there's a
serious concern about this anyway?Last, considering the tests, I think we should add some rows in the
tables,
to make sure the FK is correctly enforced after DETACH. Something like:CREATE SCHEMA fkpart12
CREATE TABLE fk_p ( id bigint PRIMARY KEY ) PARTITION BY list (id)
CREATE TABLE fk_p_1 PARTITION OF fk_p FOR VALUES IN (1)
CREATE TABLE fk_p_2 PARTITION OF fk_p FOR VALUES IN (2)
CREATE TABLE fk_r_1 ( id bigint PRIMARY KEY, p_id bigint NOT NULL)
CREATE TABLE fk_r_2 ( id bigint PRIMARY KEY, p_id bigint NOT NULL)
CREATE TABLE fk_r ( id bigint PRIMARY KEY, p_id bigint NOT NULL,
FOREIGN KEY (p_id) REFERENCES fk_p (id)
) PARTITION BY list (id);
SET search_path TO fkpart12;INSERT INTO fk_p VALUES (1);
ALTER TABLE fk_r ATTACH PARTITION fk_r_2 FOR VALUES IN (2);
ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1);
\d fk_r_1INSERT INTO fk_r VALUES (1,1);
ALTER TABLE fk_r DETACH PARTITION fk_r_1;
\d fk_r_1INSERT INTO c_1 VALUES (2,2); -- fails as EXPECTED
DELETE FROM p; -- should fails but was buggyALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1);
\d fk_r_13. Self referencing FK between partitions [3]
You added to your commit message:
verify: 20230707175859.17c91538@karst
I'm not sure what the "verify" flag means. Unfortunately, your patch
doesn't
help on this topic.This bug really needs more discussion and design consideration. I have
thought about this problem and haven't found any solution that don't
involve
breaking the current core behavior. It really looks like an impossible
bug to
fix without dropping the constraint itself. On both side. Maybe the only
sane
behavior would be to forbid detaching the partition if it would break the
constraint.But let's discuss this on the related thread, should we?
Thank you for reading me all the way down to the bottom!
Regards,
[1] /messages/by-id/20230705233028.2f554f73@karst
[2] /messages/by-id/20230420144344.40744130@karst
[3] /messages/by-id/20230707175859.17c91538@karst
--
Tender Wang
Hi Tender,
On Tue, 3 Sep 2024 10:16:44 +0800
Tender Wang <tndrwang@gmail.com> wrote:
Jehan-Guillaume de Rorthais <jgdr@dalibo.com> 于2024年9月3日周二 05:02写道:
[…]
* Constraint & trigger catalog cleanup [1] (this thread)
* FK broken after DETACH [2]
* Maintenance consideration about self referencing FK between partitions
[3]The third issue has been fixed, and codes have been pushed. Because of my
misunderstanding,
It should not be here.
I just retried the SQL scenario Guillaume gave on both master and master with
Alvaro's patch. See:
/messages/by-id/CAECtzeWHCA+6tTcm2Oh2+g7fURUJpLZb-=pRXgeWJ-Pi+VU=_w@mail.gmail.com
It doesn't seem fixed at all. Maybe you are mixing up with another thread/issue?
0. Splitting in two commits
[…]
Unfortunately, this discussion about the first bug slipped to the second
one when Tender stumbled on this bug as well and reported it. But, both
bugs can be triggered independently, and have distinct fixes.It's ok that these two issues are fixed together. It is because current
codes don't handle better when the referenced side is the partition table.
I don't feel the same. Mixing two discussions and fixes together in the same
thread and commit makes life harder.
Last year, when you found the other bug, I tried to point you to the
right thread to avoid mixing subjects:
/messages/by-id/20230810170345.26e41b05@karst
If I wrote about the third (non fixed) issue yesterday, it's just because
Alvaro included a reference to it in his commit message. But I think we should
really keep up with this issue on its own, dedicated discussion:
/messages/by-id/CAECtzeWHCA+6tTcm2Oh2+g7fURUJpLZb-=pRXgeWJ-Pi+VU=_w@mail.gmail.com
Regards
Jehan-Guillaume de Rorthais <jgdr@dalibo.com> 于2024年9月3日周二 17:26写道:
Hi Tender,
On Tue, 3 Sep 2024 10:16:44 +0800
Tender Wang <tndrwang@gmail.com> wrote:Jehan-Guillaume de Rorthais <jgdr@dalibo.com> 于2024年9月3日周二 05:02写道:
[…]
* Constraint & trigger catalog cleanup [1] (this thread)
* FK broken after DETACH [2]
* Maintenance consideration about self referencing FK betweenpartitions
[3]
The third issue has been fixed, and codes have been pushed. Because of
my
misunderstanding,
It should not be here.I just retried the SQL scenario Guillaume gave on both master and master
with
Alvaro's patch. See:/messages/by-id/CAECtzeWHCA+6tTcm2Oh2+g7fURUJpLZb-=pRXgeWJ-Pi+VU=_w@mail.gmail.com
It doesn't seem fixed at all. Maybe you are mixing up with another
thread/issue?
Sorry, I mixed up the third issue with the Alexander reported issue.
Please ignore the above noise.
0. Splitting in two commits
[…]
Unfortunately, this discussion about the first bug slipped to the
second
one when Tender stumbled on this bug as well and reported it. But,
both
bugs can be triggered independently, and have distinct fixes.
It's ok that these two issues are fixed together. It is because current
codes don't handle better when the referenced side is the partitiontable.
I don't feel the same. Mixing two discussions and fixes together in the
same
thread and commit makes life harder.
Hmm, these two issues have a close relationship. Anyway, I think it's ok to
fix the two issues together.
Last year, when you found the other bug, I tried to point you to the
right thread to avoid mixing subjects:/messages/by-id/20230810170345.26e41b05@karst
If I wrote about the third (non fixed) issue yesterday, it's just because
Alvaro included a reference to it in his commit message. But I think we
should
really keep up with this issue on its own, dedicated discussion:/messages/by-id/CAECtzeWHCA+6tTcm2Oh2+g7fURUJpLZb-=pRXgeWJ-Pi+VU=_w@mail.gmail.com
Thanks for the reminder. I didn't take the time to look into the third
issue. Please give me some to analyze it.
--
Thanks,
Tender Wang
On Mon, 2 Sep 2024 23:01:47 +0200
Jehan-Guillaume de Rorthais <jgdr@dalibo.com> wrote:
[…]
My proposal was to clean everything related to the old FK and use some
existing code path to create a fresh and cleaner one. This requires some
refactoring in existing code, but we would win a common path of code between
create/attach/detach, a cleaner catalog and easier code maintenance.I've finally been able to write a PoC that implement this by calling
addFkRecurseReferenced() from DetachPartitionFinalize(). I can't join
it here because it is currently an ugly draft and I still have some work
to do. But I would really like to have a little more time (one or two days)
to explore this avenue further before you commit yours, if you don't mind?
Or maybe you already have considered this avenue and rejected it?
Please, find in attachment a patch implementing this idea.
Regards,
Attachments:
v2-0001-Rework-foreign-key-mangling-during-ATTACH-DETACH.patchtext/x-patchDownload
From 0a235cc7040e9b33ba0bb03b0cff34a7281db137 Mon Sep 17 00:00:00 2001
From: Jehan-Guillaume de Rorthais <jgdr@dalibo.com>
Date: Wed, 5 Jul 2023 19:19:40 +0200
Subject: [PATCH] Rework foreign key mangling during ATTACH/DETACH
---
src/backend/commands/tablecmds.c | 363 +++++++++++++++++-----
src/test/regress/expected/foreign_key.out | 67 ++++
src/test/regress/sql/foreign_key.sql | 40 +++
3 files changed, 386 insertions(+), 84 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index b3cc6f8f69..f345d6f018 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -504,7 +504,13 @@ static ObjectAddress ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *
Relation rel, Constraint *fkconstraint,
bool recurse, bool recursing,
LOCKMODE lockmode);
-static ObjectAddress addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint,
+static ObjectAddress addFkConstraint(Constraint *fkconstraint, Relation rel,
+ Relation pkrel, Oid indexOid, Oid parentConstr,
+ int numfks, int16 *pkattnum, int16 *fkattnum,
+ Oid *pfeqoperators, Oid *ppeqoperators,
+ Oid *ffeqoperators, int numfkdelsetcols,
+ int16 *fkdelsetcols);
+static void addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint,
Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks, int16 *pkattnum, int16 *fkattnum,
Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
@@ -645,9 +651,11 @@ static void DropClonedTriggersFromPartition(Oid partitionId);
static ObjectAddress ATExecDetachPartition(List **wqueue, AlteredTableInfo *tab,
Relation rel, RangeVar *name,
bool concurrent);
-static void DetachPartitionFinalize(Relation rel, Relation partRel,
- bool concurrent, Oid defaultPartOid);
-static ObjectAddress ATExecDetachPartitionFinalize(Relation rel, RangeVar *name);
+static void DetachPartitionFinalize(List **wqueue, Relation rel,
+ Relation partRel, bool concurrent,
+ Oid defaultPartOid);
+static ObjectAddress ATExecDetachPartitionFinalize(List **wqueue, Relation rel,
+ RangeVar *name);
static ObjectAddress ATExecAttachPartitionIdx(List **wqueue, Relation parentIdx,
RangeVar *name);
static void validatePartitionedIndex(Relation partedIdx, Relation partedTbl);
@@ -5468,7 +5476,7 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
((PartitionCmd *) cmd->def)->concurrent);
break;
case AT_DetachPartitionFinalize:
- address = ATExecDetachPartitionFinalize(rel, ((PartitionCmd *) cmd->def)->name);
+ address = ATExecDetachPartitionFinalize(wqueue, rel, ((PartitionCmd *) cmd->def)->name);
break;
default: /* oops */
elog(ERROR, "unrecognized alter table type: %d",
@@ -9901,7 +9909,7 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
* Create all the constraint and trigger objects, recursing to partitions
* as necessary. First handle the referenced side.
*/
- address = addFkRecurseReferenced(wqueue, fkconstraint, rel, pkrel,
+ address = addFkConstraint(fkconstraint, rel, pkrel,
indexOid,
InvalidOid, /* no parent constraint */
numfks,
@@ -9911,6 +9919,18 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
ppeqoperators,
ffeqoperators,
numfkdelsetcols,
+ fkdelsetcols);
+
+ addFkRecurseReferenced(wqueue, fkconstraint, rel, pkrel,
+ indexOid,
+ address.objectId,
+ numfks,
+ pkattnum,
+ fkattnum,
+ pfeqoperators,
+ ppeqoperators,
+ ffeqoperators,
+ numfkdelsetcols,
fkdelsetcols,
old_check_ok,
InvalidOid, InvalidOid);
@@ -9974,47 +9994,20 @@ validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
}
}
-/*
- * addFkRecurseReferenced
- * subroutine for ATAddForeignKeyConstraint; recurses on the referenced
- * side of the constraint
- *
- * Create pg_constraint rows for the referenced side of the constraint,
- * referencing the parent of the referencing side; also create action triggers
- * on leaf partitions. If the table is partitioned, recurse to handle each
- * partition.
- *
- * wqueue is the ALTER TABLE work queue; can be NULL when not running as part
- * of an ALTER TABLE sequence.
- * fkconstraint is the constraint being added.
- * rel is the root referencing relation.
- * pkrel is the referenced relation; might be a partition, if recursing.
- * indexOid is the OID of the index (on pkrel) implementing this constraint.
- * parentConstr is the OID of a parent constraint; InvalidOid if this is a
- * top-level constraint.
- * numfks is the number of columns in the foreign key
- * pkattnum is the attnum array of referenced attributes.
- * fkattnum is the attnum array of referencing attributes.
- * numfkdelsetcols is the number of columns in the ON DELETE SET NULL/DEFAULT
- * (...) clause
- * fkdelsetcols is the attnum array of the columns in the ON DELETE SET
- * NULL/DEFAULT clause
- * pf/pp/ffeqoperators are OID array of operators between columns.
- * old_check_ok signals that this constraint replaces an existing one that
- * was already validated (thus this one doesn't need validation).
- * parentDelTrigger and parentUpdTrigger, when being recursively called on
- * a partition, are the OIDs of the parent action triggers for DELETE and
- * UPDATE respectively.
- */
static ObjectAddress
-addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
- Relation pkrel, Oid indexOid, Oid parentConstr,
- int numfks,
- int16 *pkattnum, int16 *fkattnum, Oid *pfeqoperators,
- Oid *ppeqoperators, Oid *ffeqoperators,
- int numfkdelsetcols, int16 *fkdelsetcols,
- bool old_check_ok,
- Oid parentDelTrigger, Oid parentUpdTrigger)
+addFkConstraint(Constraint *fkconstraint,
+ Relation rel,
+ Relation pkrel,
+ Oid indexOid,
+ Oid parentConstr,
+ int numfks,
+ int16 *pkattnum,
+ int16 *fkattnum,
+ Oid *pfeqoperators,
+ Oid *ppeqoperators,
+ Oid *ffeqoperators,
+ int numfkdelsetcols,
+ int16 *fkdelsetcols)
{
ObjectAddress address;
Oid constrOid;
@@ -10022,8 +10015,6 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
bool conislocal;
int coninhcount;
bool connoinherit;
- Oid deleteTriggerOid,
- updateTriggerOid;
/*
* Verify relkind for each referenced partition. At the top level, this
@@ -10121,12 +10112,60 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
/* make new constraint visible, in case we add more */
CommandCounterIncrement();
+ return address;
+}
+
+/*
+ * addFkRecurseReferenced
+ * subroutine for ATAddForeignKeyConstraint; recurses on the referenced
+ * side of the constraint
+ *
+ * Create pg_constraint rows for the referenced side of the constraint,
+ * referencing the parent of the referencing side; also create action triggers
+ * on leaf partitions. If the table is partitioned, recurse to handle each
+ * partition.
+ *
+ * wqueue is the ALTER TABLE work queue; can be NULL when not running as part
+ * of an ALTER TABLE sequence.
+ * fkconstraint is the constraint being added.
+ * rel is the root referencing relation.
+ * pkrel is the referenced relation; might be a partition, if recursing.
+ * indexOid is the OID of the index (on pkrel) implementing this constraint.
+ * parentConstr is the OID of a parent constraint; InvalidOid if this is a
+ * top-level constraint.
+ * numfks is the number of columns in the foreign key
+ * pkattnum is the attnum array of referenced attributes.
+ * fkattnum is the attnum array of referencing attributes.
+ * numfkdelsetcols is the number of columns in the ON DELETE SET NULL/DEFAULT
+ * (...) clause
+ * fkdelsetcols is the attnum array of the columns in the ON DELETE SET
+ * NULL/DEFAULT clause
+ * pf/pp/ffeqoperators are OID array of operators between columns.
+ * old_check_ok signals that this constraint replaces an existing one that
+ * was already validated (thus this one doesn't need validation).
+ * parentDelTrigger and parentUpdTrigger, when being recursively called on
+ * a partition, are the OIDs of the parent action triggers for DELETE and
+ * UPDATE respectively.
+ */
+static void
+addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
+ Relation pkrel, Oid indexOid, Oid parentConstr,
+ int numfks,
+ int16 *pkattnum, int16 *fkattnum, Oid *pfeqoperators,
+ Oid *ppeqoperators, Oid *ffeqoperators,
+ int numfkdelsetcols, int16 *fkdelsetcols,
+ bool old_check_ok,
+ Oid parentDelTrigger, Oid parentUpdTrigger)
+{
+ Oid deleteTriggerOid,
+ updateTriggerOid;
+
/*
* Create the action triggers that enforce the constraint.
*/
createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
fkconstraint,
- constrOid, indexOid,
+ parentConstr, indexOid,
parentDelTrigger, parentUpdTrigger,
&deleteTriggerOid, &updateTriggerOid);
@@ -10145,6 +10184,7 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
AttrMap *map;
AttrNumber *mapped_pkattnum;
Oid partIndexId;
+ ObjectAddress address;
partRel = table_open(pd->oids[i], ShareRowExclusiveLock);
@@ -10169,8 +10209,14 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
if (!OidIsValid(partIndexId))
elog(ERROR, "index for %u not found in partition %s",
indexOid, RelationGetRelationName(partRel));
+
+ address = addFkConstraint(fkconstraint, rel, partRel,
+ partIndexId, parentConstr, numfks,
+ mapped_pkattnum, fkattnum,
+ pfeqoperators, ppeqoperators, ffeqoperators,
+ numfkdelsetcols, fkdelsetcols);
addFkRecurseReferenced(wqueue, fkconstraint, rel, partRel,
- partIndexId, constrOid, numfks,
+ partIndexId, address.objectId, numfks,
mapped_pkattnum, fkattnum,
pfeqoperators, ppeqoperators, ffeqoperators,
numfkdelsetcols, fkdelsetcols,
@@ -10186,8 +10232,6 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
}
}
}
-
- return address;
}
/*
@@ -10549,6 +10593,7 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
int numfkdelsetcols;
AttrNumber confdelsetcols[INDEX_MAX_KEYS];
Constraint *fkconstraint;
+ ObjectAddress address;
Oid deleteTriggerOid,
updateTriggerOid;
@@ -10582,7 +10627,7 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
* Because we're only expanding the key space at the referenced side,
* we don't need to prevent any operation in the referencing table, so
* AccessShareLock suffices (assumes that dropping the constraint
- * acquires AEL).
+ * acquires AccessExclusiveLock).
*/
fkRel = table_open(constrForm->conrelid, AccessShareLock);
@@ -10648,12 +10693,16 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
constrForm->confrelid, constrForm->conrelid,
&deleteTriggerOid, &updateTriggerOid);
+ address = addFkConstraint(fkconstraint, fkRel, partitionRel, partIndexId,
+ constrOid, numfks, mapped_confkey, conkey,
+ conpfeqop, conppeqop, conffeqop,
+ numfkdelsetcols, confdelsetcols);
addFkRecurseReferenced(NULL,
fkconstraint,
fkRel,
partitionRel,
partIndexId,
- constrOid,
+ address.objectId,
numfks,
mapped_confkey,
conkey,
@@ -11105,6 +11154,83 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
partRelid);
+ /*
+ * If the referenced table is partitioned, then the partition we're
+ * attaching now has extra pg_constraint rows and action triggers that are
+ * no longer needed. Remove those.
+ */
+ if (get_rel_relkind(fk->confrelid) == RELKIND_PARTITIONED_TABLE)
+ {
+ Relation pg_constraint = table_open(ConstraintRelationId, RowShareLock);
+ ObjectAddresses *objs;
+ HeapTuple consttup;
+
+ ScanKeyInit(&key,
+ Anum_pg_constraint_conrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(fk->conrelid));
+
+ scan = systable_beginscan(pg_constraint,
+ ConstraintRelidTypidNameIndexId,
+ true, NULL, 1, &key);
+ objs = new_object_addresses();
+ while ((consttup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_constraint conform = (Form_pg_constraint) GETSTRUCT(consttup);
+
+ if (conform->conparentid != fk->conoid)
+ continue;
+ else
+ {
+ ObjectAddress addr;
+ int n;
+ SysScanDesc scan2;
+ ScanKeyData key2;
+
+ ObjectAddressSet(addr, ConstraintRelationId, conform->oid);
+ add_exact_object_address(&addr, objs);
+
+ /*
+ * First we must delete the dependency records that bind
+ * the constraint records together.
+ */
+ n = deleteDependencyRecordsForSpecific(ConstraintRelationId,
+ conform->oid,
+ DEPENDENCY_INTERNAL,
+ ConstraintRelationId,
+ fk->conoid);
+ if (n != 1)
+ elog(WARNING, "oops: found %d instead of 1 deps from %u to %u",
+ n, conform->oid, fk->conoid);
+
+ /*
+ * Now search for the triggers for this constraint and set
+ * them up for deletion too
+ */
+ ScanKeyInit(&key2,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conform->oid));
+ scan2 = systable_beginscan(trigrel, TriggerConstraintIndexId,
+ true, NULL, 1, &key2);
+ while ((trigtup = systable_getnext(scan2)) != NULL)
+ {
+ ObjectAddressSet(addr, TriggerRelationId,
+ ((Form_pg_trigger) GETSTRUCT(trigtup))->oid);
+ add_exact_object_address(&addr, objs);
+ }
+ systable_endscan(scan2);
+ }
+ }
+ /* make the dependency deletions visible */
+ CommandCounterIncrement();
+ performMultipleDeletions(objs, DROP_RESTRICT,
+ PERFORM_DELETION_INTERNAL);
+ systable_endscan(scan);
+
+ table_close(pg_constraint, RowShareLock);
+ }
+
CommandCounterIncrement();
return true;
}
@@ -19094,7 +19220,7 @@ ATExecDetachPartition(List **wqueue, AlteredTableInfo *tab, Relation rel,
}
/* Do the final part of detaching */
- DetachPartitionFinalize(rel, partRel, concurrent, defaultPartOid);
+ DetachPartitionFinalize(wqueue, rel, partRel, concurrent, defaultPartOid);
ObjectAddressSet(address, RelationRelationId, RelationGetRelid(partRel));
@@ -19111,8 +19237,8 @@ ATExecDetachPartition(List **wqueue, AlteredTableInfo *tab, Relation rel,
* transaction of the concurrent algorithm fails (crash or abort).
*/
static void
-DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
- Oid defaultPartOid)
+DetachPartitionFinalize(List **wqueue, Relation rel, Relation partRel,
+ bool concurrent, Oid defaultPartOid)
{
Relation classRel;
List *fks;
@@ -19148,8 +19274,11 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
{
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
+ HeapTuple parentConTup;
Form_pg_constraint conform;
+ Form_pg_constraint parentConForm;
Constraint *fkconstraint;
+ Oid parentConstrOid;
Oid insertTriggerOid,
updateTriggerOid;
@@ -19166,7 +19295,20 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
continue;
}
- /* unset conparentid and adjust conislocal, coninhcount, etc. */
+ parentConstrOid = conform->conparentid;
+
+ Assert(OidIsValid(conform->conparentid));
+ parentConTup = SearchSysCache1(CONSTROID,
+ ObjectIdGetDatum(parentConstrOid));
+ if (!HeapTupleIsValid(parentConTup))
+ elog(ERROR, "cache lookup failed for constraint %u",
+ conform->conparentid);
+ parentConForm = (Form_pg_constraint) GETSTRUCT(parentConTup);
+
+ /*
+ * The constraint on this table must be marked no longer a child of
+ * the parent's constraint, as do its check triggers.
+ */
ConstraintSetParentConstraint(fk->conoid, InvalidOid, InvalidOid);
/*
@@ -19184,35 +19326,88 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
RelationGetRelid(partRel));
/*
- * Make the action triggers on the referenced relation. When this was
- * a partition the action triggers pointed to the parent rel (they
- * still do), but now we need separate ones of our own.
+ * If the referenced side is partitioned (which we know because our
+ * parent's constraint points to a different relation than ours) then
+ * we must, in addition to the above, create pg_constraint rows that
+ * point to each partition, each with its own action triggers.
*/
- fkconstraint = makeNode(Constraint);
- fkconstraint->contype = CONSTRAINT_FOREIGN;
- fkconstraint->conname = pstrdup(NameStr(conform->conname));
- fkconstraint->deferrable = conform->condeferrable;
- fkconstraint->initdeferred = conform->condeferred;
- fkconstraint->location = -1;
- fkconstraint->pktable = NULL;
- fkconstraint->fk_attrs = NIL;
- fkconstraint->pk_attrs = NIL;
- fkconstraint->fk_matchtype = conform->confmatchtype;
- fkconstraint->fk_upd_action = conform->confupdtype;
- fkconstraint->fk_del_action = conform->confdeltype;
- fkconstraint->fk_del_set_cols = NIL;
- fkconstraint->old_conpfeqop = NIL;
- fkconstraint->old_pktable_oid = InvalidOid;
- fkconstraint->skip_validation = false;
- fkconstraint->initially_valid = true;
+ if (parentConForm->conrelid != conform->conrelid)
+ {
+ int numfks;
+ AttrNumber conkey[INDEX_MAX_KEYS];
+ AttrMap *attmap;
+ AttrNumber confkey[INDEX_MAX_KEYS];
+ Oid conpfeqop[INDEX_MAX_KEYS];
+ Oid conppeqop[INDEX_MAX_KEYS];
+ Oid conffeqop[INDEX_MAX_KEYS];
+ int numfkdelsetcols;
+ AttrNumber confdelsetcols[INDEX_MAX_KEYS];
+ Relation refdRel;
+
+ DeconstructFkConstraintRow(contup,
+ &numfks,
+ conkey,
+ confkey,
+ conpfeqop,
+ conppeqop,
+ conffeqop,
+ &numfkdelsetcols,
+ confdelsetcols);
+
+ /* Create a synthetic node we'll use throughout */
+ fkconstraint = makeNode(Constraint);
+ fkconstraint->contype = CONSTRAINT_FOREIGN;
+ fkconstraint->conname = pstrdup(NameStr(conform->conname));
+ fkconstraint->deferrable = conform->condeferrable;
+ fkconstraint->initdeferred = conform->condeferred;
+ fkconstraint->skip_validation = true;
+ fkconstraint->initially_valid = true;
+ /* a few irrelevant fields omitted here */
+ fkconstraint->pktable = NULL;
+ fkconstraint->fk_attrs = NIL;
+ fkconstraint->pk_attrs = NIL;
+ fkconstraint->fk_matchtype = conform->confmatchtype;
+ fkconstraint->fk_upd_action = conform->confupdtype;
+ fkconstraint->fk_del_action = conform->confdeltype;
+ fkconstraint->fk_del_set_cols = NIL;
+ fkconstraint->old_conpfeqop = NIL;
+ fkconstraint->old_pktable_oid = InvalidOid;
+ fkconstraint->location = -1;
+
+ attmap = build_attrmap_by_name(RelationGetDescr(partRel),
+ RelationGetDescr(rel),
+ false);
+ for (int i = 0; i < numfks; i++)
+ {
+ Form_pg_attribute att;
- createForeignKeyActionTriggers(partRel, conform->confrelid,
- fkconstraint, fk->conoid,
+ att = TupleDescAttr(RelationGetDescr(partRel),
+ attmap->attnums[conkey[i] - 1] - 1);
+ fkconstraint->fk_attrs = lappend(fkconstraint->fk_attrs,
+ makeString(NameStr(att->attname)));
+ }
+
+ refdRel = table_open(fk->confrelid, AccessShareLock);
+
+ addFkRecurseReferenced(wqueue, fkconstraint, partRel,
+ refdRel,
conform->conindid,
- InvalidOid, InvalidOid,
- NULL, NULL);
+ fk->conoid,
+ numfks,
+ confkey,
+ conkey,
+ conpfeqop,
+ conppeqop,
+ conffeqop,
+ numfkdelsetcols,
+ confdelsetcols,
+ true,
+ InvalidOid, InvalidOid);
+ table_close(refdRel, AccessShareLock);
+ }
ReleaseSysCache(contup);
+ ReleaseSysCache(parentConTup);
}
list_free_deep(fks);
if (trigrel)
@@ -19359,7 +19554,7 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
* completion; this completes the detaching process.
*/
static ObjectAddress
-ATExecDetachPartitionFinalize(Relation rel, RangeVar *name)
+ATExecDetachPartitionFinalize(List **wqueue, Relation rel, RangeVar *name)
{
Relation partRel;
ObjectAddress address;
@@ -19377,7 +19572,7 @@ ATExecDetachPartitionFinalize(Relation rel, RangeVar *name)
*/
WaitForOlderSnapshots(snap->xmin, false);
- DetachPartitionFinalize(rel, partRel, true, InvalidOid);
+ DetachPartitionFinalize(wqueue, rel, partRel, true, InvalidOid);
ObjectAddressSet(address, RelationRelationId, RelationGetRelid(partRel));
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 8c04a24b37..43ce77a835 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -2934,3 +2934,70 @@ DETAIL: drop cascades to table fkpart11.pk
drop cascades to table fkpart11.fk_parted
drop cascades to table fkpart11.fk_another
drop cascades to function fkpart11.print_row()
+-- When a table is attached as partition to a partitioned table that has
+-- a foreign key to another partitioned table, it acquires a clone of the
+-- FK. Upon detach, this clone is not removed, but instead becomes an
+-- independent FK. If it then attaches to the partitioned table again,
+-- the FK from the parent "takes over" ownership of the independent FK rather
+-- than creating a separate one.
+CREATE SCHEMA fkpart12
+ CREATE TABLE fk_p ( id bigint PRIMARY KEY ) PARTITION BY list (id)
+ CREATE TABLE fk_p_1 PARTITION OF fk_p FOR VALUES IN (1)
+ CREATE TABLE fk_p_2 PARTITION OF fk_p FOR VALUES IN (2)
+ CREATE TABLE fk_r_1 ( id bigint PRIMARY KEY, p_id bigint NOT NULL)
+ CREATE TABLE fk_r_2 ( id bigint PRIMARY KEY, p_id bigint NOT NULL)
+ CREATE TABLE fk_r ( id bigint PRIMARY KEY, p_id bigint NOT NULL,
+ FOREIGN KEY (p_id) REFERENCES fk_p (id)
+ ) PARTITION BY list (id);
+SET search_path TO fkpart12;
+INSERT INTO fk_p VALUES (1);
+ALTER TABLE fk_r ATTACH PARTITION fk_r_2 FOR VALUES IN (2);
+ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1);
+\d fk_r_1
+ Table "fkpart12.fk_r_1"
+ Column | Type | Collation | Nullable | Default
+--------+--------+-----------+----------+---------
+ id | bigint | | not null |
+ p_id | bigint | | not null |
+Partition of: fk_r FOR VALUES IN ('1')
+Indexes:
+ "fk_r_1_pkey" PRIMARY KEY, btree (id)
+Foreign-key constraints:
+ TABLE "fk_r" CONSTRAINT "fk_r_p_id_fkey" FOREIGN KEY (p_id) REFERENCES fk_p(id)
+
+INSERT INTO fk_r VALUES (1,1);
+ALTER TABLE fk_r DETACH PARTITION fk_r_1;
+\d fk_r_1
+ Table "fkpart12.fk_r_1"
+ Column | Type | Collation | Nullable | Default
+--------+--------+-----------+----------+---------
+ id | bigint | | not null |
+ p_id | bigint | | not null |
+Indexes:
+ "fk_r_1_pkey" PRIMARY KEY, btree (id)
+Foreign-key constraints:
+ "fk_r_p_id_fkey" FOREIGN KEY (p_id) REFERENCES fk_p(id)
+
+INSERT INTO fk_r_1 VALUES (2,2); -- fails as EXPECTED
+ERROR: insert or update on table "fk_r_1" violates foreign key constraint "fk_r_p_id_fkey"
+DETAIL: Key (p_id)=(2) is not present in table "fk_p".
+DELETE FROM fk_p; -- should fails but was buggy
+ERROR: update or delete on table "fk_p_1" violates foreign key constraint "fk_r_1_p_id_fkey" on table "fk_r_1"
+DETAIL: Key (id)=(1) is still referenced from table "fk_r_1".
+ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1);
+\d fk_r_1
+ Table "fkpart12.fk_r_1"
+ Column | Type | Collation | Nullable | Default
+--------+--------+-----------+----------+---------
+ id | bigint | | not null |
+ p_id | bigint | | not null |
+Partition of: fk_r FOR VALUES IN ('1')
+Indexes:
+ "fk_r_1_pkey" PRIMARY KEY, btree (id)
+Foreign-key constraints:
+ TABLE "fk_r" CONSTRAINT "fk_r_p_id_fkey" FOREIGN KEY (p_id) REFERENCES fk_p(id)
+
+SET client_min_messages TO warning;
+DROP SCHEMA fkpart12 CASCADE;
+RESET client_min_messages;
+RESET search_path;
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index d1aac5357f..5d8d2558c0 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -2086,3 +2086,43 @@ UPDATE fkpart11.pk SET a = 3 WHERE a = 4;
UPDATE fkpart11.pk SET a = 1 WHERE a = 2;
DROP SCHEMA fkpart11 CASCADE;
+
+-- When a table is attached as partition to a partitioned table that has
+-- a foreign key to another partitioned table, it acquires a clone of the
+-- FK. Upon detach, this clone is not removed, but instead becomes an
+-- independent FK. If it then attaches to the partitioned table again,
+-- the FK from the parent "takes over" ownership of the independent FK rather
+-- than creating a separate one.
+CREATE SCHEMA fkpart12
+ CREATE TABLE fk_p ( id bigint PRIMARY KEY ) PARTITION BY list (id)
+ CREATE TABLE fk_p_1 PARTITION OF fk_p FOR VALUES IN (1)
+ CREATE TABLE fk_p_2 PARTITION OF fk_p FOR VALUES IN (2)
+ CREATE TABLE fk_r_1 ( id bigint PRIMARY KEY, p_id bigint NOT NULL)
+ CREATE TABLE fk_r_2 ( id bigint PRIMARY KEY, p_id bigint NOT NULL)
+ CREATE TABLE fk_r ( id bigint PRIMARY KEY, p_id bigint NOT NULL,
+ FOREIGN KEY (p_id) REFERENCES fk_p (id)
+ ) PARTITION BY list (id);
+SET search_path TO fkpart12;
+
+INSERT INTO fk_p VALUES (1);
+
+ALTER TABLE fk_r ATTACH PARTITION fk_r_2 FOR VALUES IN (2);
+
+ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1);
+\d fk_r_1
+
+INSERT INTO fk_r VALUES (1,1);
+
+ALTER TABLE fk_r DETACH PARTITION fk_r_1;
+\d fk_r_1
+
+INSERT INTO fk_r_1 VALUES (2,2); -- fails as EXPECTED
+DELETE FROM fk_p; -- should fails but was buggy
+
+ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1);
+\d fk_r_1
+
+SET client_min_messages TO warning;
+DROP SCHEMA fkpart12 CASCADE;
+RESET client_min_messages;
+RESET search_path;
--
2.46.0
Alvaro Herrera <alvherre@alvh.no-ip.org> 于2024年8月8日周四 06:50写道:
On 2024-Jul-26, Tender Wang wrote:
Junwang Zhao <zhjwpku@gmail.com> 于2024年7月26日周五 14:57写道:
There is a bug report[0] Tender comments might be the same issue as
this one, but I tried Alvaro's and mine patch, neither could solve
that problem, I did not tried Tender's earlier patch thought. I post
the test script below in case you are interested.My earlier patch should handle Alexander reported case. But I did not
do more test. I'm not sure that wether or not has dismatching between
pg_constraint and pg_trigger.I aggred with Alvaro said that "requires a much more invasive
solution".Here's the patch which, as far as I can tell, fixes all the reported
problems (other than the one in bug 18541, for which I proposed an
unrelated fix in that thread[1]). If you can double-check, I would very
much appreciate that. Also, I think the test cases the patch adds
reflect the provided examples sufficiently, but if we're still failing
to cover some, please let me know.
When I review Jehan-Guillaume v2 patch, I found the below codes that need
a little tweak. In DetachPartitionFinalize()
/*
* If the referenced side is partitioned (which we know because our
* parent's constraint points to a different relation than ours) then
* we must, in addition to the above, create pg_constraint rows that
* point to each partition, each with its own action triggers.
*/
if (parentConForm->conrelid != conform->conrelid)
I found that the above IF was always true, regardless of whether the
referenced side is partitioned.
Although find_all_inheritors() can return an empty children list when the
referenced side is not partitioned,
we can avoid much useless work.
How about this way:
if (get_rel_relkind(conform->confrelid) == RELKIND_PARTITIONED_TABLE)
--
Thanks,
Tender Wang
On Thu, 5 Sep 2024 00:57:28 +0200
Jehan-Guillaume de Rorthais <jgdr@dalibo.com> wrote:
On Mon, 2 Sep 2024 23:01:47 +0200
Jehan-Guillaume de Rorthais <jgdr@dalibo.com> wrote:[…]
My proposal was to clean everything related to the old FK and use some
existing code path to create a fresh and cleaner one. This requires some
refactoring in existing code, but we would win a common path of code
between create/attach/detach, a cleaner catalog and easier code maintenance.I've finally been able to write a PoC that implement this by calling
addFkRecurseReferenced() from DetachPartitionFinalize(). I can't join
it here because it is currently an ugly draft and I still have some work
to do. But I would really like to have a little more time (one or two
days) to explore this avenue further before you commit yours, if you don't
mind? Or maybe you already have considered this avenue and rejected it?Please, find in attachment a patch implementing this idea.
Please, find in attachment a set of patch based on the previous one.
v3-0001-Add-tests-about-FK-between-partitionned-tables.patch:
This patch implement tests triggering the bugs discussed. Based on Michael
advice, I added one level sub-partitioning to stress test the recursive code
and some queries checking on the catalog objects.
v3-0002-Rework-foreign-key-mangling-during-ATTACH-DETACH.patch:
The main patch, similar to v2 in my previous patch with more comments
added/restored. I added some more explanations in the commit message about
the refactoring itself, making addFkRecurseReferencing() and
addFkRecurseReferenced() having the same logic.
v3-0003-Use-addFkConstraint-in-addFkRecurseReferencing.patch
A new patch refactoring the constraint creation in addFkRecurseReferencing()
to use the new addFkConstraint() function.
v3-0004-Use-addFkConstraint-in-CloneFkReferencing.patch
A new patch refactoring the constraint creation in CloneFkReferencing()
to use the new addFkConstraint() function.
TODO:
* I hadn't time to study last Tender Wang comment here:
/messages/by-id/CAHewXNkuU2V7GfgFkwd265RJ99+BfJueNEZhrHSk39o3thqxNA@mail.gmail.com
* I still think we should split v3-0002 in two different patch…
* backporting…
Regards,
Attachments:
v3-0001-Add-tests-about-FK-between-partitionned-tables.patchtext/x-patchDownload
From 250de46d4c9dbdfc9d6ae44288f863a4226edd1e Mon Sep 17 00:00:00 2001
From: Jehan-Guillaume de Rorthais <jgdr@dalibo.com>
Date: Mon, 23 Sep 2024 18:50:20 +0200
Subject: [PATCH v3 1/4] Add tests about FK between partitionned tables
These tests cover two reported bugs related to FK
between two partitionned tables.
co-author : Alvaro Herrera <alvherre@alvh.no-ip.org>
co-author : Jehan-Guillaume de Rorthais <jgdr@dalibo.com>
co-author : Tender Wang <tndrwang@gmail.com>
Discussion: https://postgr.es/m/20230420144344.40744130@karst
Discussion: https://postgr.es/m/20230705233028.2f554f73@karst
---
src/test/regress/expected/foreign_key.out | 266 ++++++++++++++++++++++
src/test/regress/sql/foreign_key.sql | 101 ++++++++
2 files changed, 367 insertions(+)
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 8c04a24b37..da67bbeb4f 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -2934,3 +2934,269 @@ DETAIL: drop cascades to table fkpart11.pk
drop cascades to table fkpart11.fk_parted
drop cascades to table fkpart11.fk_another
drop cascades to function fkpart11.print_row()
+-- When a table is attached as partition to a partitioned table that has
+-- a foreign key to another partitioned table, it acquires a clone of the
+-- FK. Upon detach, this clone is not removed, but instead becomes an
+-- independent FK. If it then attaches to the partitioned table again,
+-- the FK from the parent "takes over" ownership of the independent FK rather
+-- than creating a separate one.
+CREATE SCHEMA fkpart12
+ CREATE TABLE fk_p ( id int, jd int, PRIMARY KEY(id, jd)) PARTITION BY list (id)
+ CREATE TABLE fk_p_1 PARTITION OF fk_p FOR VALUES IN (1) PARTITION BY list (jd)
+ CREATE TABLE fk_p_1_1 PARTITION OF fk_p_1 FOR VALUES IN (1)
+ CREATE TABLE fk_p_1_2 PARTITION OF fk_p_1 FOR VALUES IN (2)
+ CREATE TABLE fk_p_2 PARTITION OF fk_p FOR VALUES IN (2) PARTITION BY list (jd)
+ CREATE TABLE fk_p_2_1 PARTITION OF fk_p_2 FOR VALUES IN (1)
+ CREATE TABLE fk_p_2_2 PARTITION OF fk_p_2 FOR VALUES IN (2)
+ CREATE TABLE fk_r_1 ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL)
+ CREATE TABLE fk_r_2 ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL)
+ CREATE TABLE fk_r ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL,
+ FOREIGN KEY (p_id, p_jd) REFERENCES fk_p (id, jd)
+ ) PARTITION BY list (id);
+SET search_path TO fkpart12;
+CREATE OR REPLACE FUNCTION
+fkpart12_constraints(OUT conname name, OUT conrel regclass,
+ OUT confrelid regclass, OUT conparent name) RETURNS SETOF record AS $$
+ WITH RECURSIVE r AS (
+ SELECT oid, conname, conrelid, confrelid, NULL::name AS conparent
+ FROM pg_constraint
+ WHERE conrelid = 'fk_r'::regclass
+ AND contype = 'f'
+ AND conparentid = 0
+ UNION ALL
+ SELECT c.oid, c.conname, c.conrelid, c.confrelid, r.conname
+ FROM pg_constraint c
+ JOIN r ON c.conparentid = r.oid
+ )
+ SELECT conname, conrelid::regclass, confrelid::regclass, conparent
+ FROM r
+ ORDER BY oid, conname, confrelid, conparent;
+$$
+LANGUAGE SQL;
+CREATE OR REPLACE FUNCTION
+fkpart12_triggers(OUT conname name, OUT conparent name,
+ OUT tgfoid regproc, OUT tgfparent regproc) RETURNS SETOF record AS $$
+
+ WITH RECURSIVE r AS (
+ SELECT t.oid, t.tgfoid::regproc, c.conname, NULL::name AS conparent, NULL::regproc AS tgfparent
+ FROM pg_trigger t
+ JOIN pg_constraint c ON c.oid = t.tgconstraint
+ WHERE c.conrelid = 'fk_r'::regclass
+ AND c.contype = 'f'
+ AND t.tgparentid = 0
+ UNION ALL
+ SELECT t2.oid, t2.tgfoid::regproc, c2.conname, c3.conname, r.tgfoid::regproc AS tgfparent
+ FROM pg_trigger t2
+ JOIN pg_constraint c2 ON c2.oid = t2.tgconstraint
+ LEFT JOIN pg_constraint c3 ON c3.oid = c2.conparentid
+ JOIN r ON r.oid = t2.tgparentid
+ )
+ SELECT conname, conparent, tgfoid, tgfparent
+ FROM r
+ ORDER BY conname, conparent, tgfoid, tgfparent;
+$$
+LANGUAGE SQL;
+SELECT * FROM fkpart12_constraints();
+ conname | conrel | confrelid | conparent
+----------------------+--------+-----------+----------------------
+ fk_r_p_id_p_jd_fkey | fk_r | fk_p |
+ fk_r_p_id_p_jd_fkey1 | fk_r | fk_p_1 | fk_r_p_id_p_jd_fkey
+ fk_r_p_id_p_jd_fkey2 | fk_r | fk_p_1_1 | fk_r_p_id_p_jd_fkey1
+ fk_r_p_id_p_jd_fkey3 | fk_r | fk_p_1_2 | fk_r_p_id_p_jd_fkey1
+ fk_r_p_id_p_jd_fkey4 | fk_r | fk_p_2 | fk_r_p_id_p_jd_fkey
+ fk_r_p_id_p_jd_fkey5 | fk_r | fk_p_2_1 | fk_r_p_id_p_jd_fkey4
+ fk_r_p_id_p_jd_fkey6 | fk_r | fk_p_2_2 | fk_r_p_id_p_jd_fkey4
+(7 rows)
+
+SELECT * FROM fkpart12_triggers();
+ conname | conparent | tgfoid | tgfparent
+----------------------+----------------------+------------------------+------------------------
+ fk_r_p_id_p_jd_fkey | | "RI_FKey_check_ins" |
+ fk_r_p_id_p_jd_fkey | | "RI_FKey_check_upd" |
+ fk_r_p_id_p_jd_fkey | | "RI_FKey_noaction_del" |
+ fk_r_p_id_p_jd_fkey | | "RI_FKey_noaction_upd" |
+ fk_r_p_id_p_jd_fkey1 | fk_r_p_id_p_jd_fkey | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey1 | fk_r_p_id_p_jd_fkey | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey2 | fk_r_p_id_p_jd_fkey1 | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey2 | fk_r_p_id_p_jd_fkey1 | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey3 | fk_r_p_id_p_jd_fkey1 | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey3 | fk_r_p_id_p_jd_fkey1 | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey4 | fk_r_p_id_p_jd_fkey | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey4 | fk_r_p_id_p_jd_fkey | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey5 | fk_r_p_id_p_jd_fkey4 | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey5 | fk_r_p_id_p_jd_fkey4 | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey6 | fk_r_p_id_p_jd_fkey4 | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey6 | fk_r_p_id_p_jd_fkey4 | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+(16 rows)
+
+INSERT INTO fk_p VALUES (1, 1);
+ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1);
+ALTER TABLE fk_r ATTACH PARTITION fk_r_2 FOR VALUES IN (2);
+\d fk_r_1
+ Table "fkpart12.fk_r_1"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ id | integer | | not null |
+ p_id | integer | | not null |
+ p_jd | integer | | not null |
+Partition of: fk_r FOR VALUES IN (1)
+Indexes:
+ "fk_r_1_pkey" PRIMARY KEY, btree (id)
+Foreign-key constraints:
+ TABLE "fk_r" CONSTRAINT "fk_r_p_id_p_jd_fkey" FOREIGN KEY (p_id, p_jd) REFERENCES fk_p(id, jd)
+
+SELECT * FROM fkpart12_constraints();
+ conname | conrel | confrelid | conparent
+----------------------+--------+-----------+----------------------
+ fk_r_p_id_p_jd_fkey | fk_r | fk_p |
+ fk_r_p_id_p_jd_fkey1 | fk_r | fk_p_1 | fk_r_p_id_p_jd_fkey
+ fk_r_p_id_p_jd_fkey2 | fk_r | fk_p_1_1 | fk_r_p_id_p_jd_fkey1
+ fk_r_p_id_p_jd_fkey3 | fk_r | fk_p_1_2 | fk_r_p_id_p_jd_fkey1
+ fk_r_p_id_p_jd_fkey4 | fk_r | fk_p_2 | fk_r_p_id_p_jd_fkey
+ fk_r_p_id_p_jd_fkey5 | fk_r | fk_p_2_1 | fk_r_p_id_p_jd_fkey4
+ fk_r_p_id_p_jd_fkey6 | fk_r | fk_p_2_2 | fk_r_p_id_p_jd_fkey4
+ fk_r_p_id_p_jd_fkey | fk_r_1 | fk_p | fk_r_p_id_p_jd_fkey
+ fk_r_p_id_p_jd_fkey | fk_r_2 | fk_p | fk_r_p_id_p_jd_fkey
+(9 rows)
+
+SELECT * FROM fkpart12_triggers();
+ conname | conparent | tgfoid | tgfparent
+----------------------+----------------------+------------------------+------------------------
+ fk_r_p_id_p_jd_fkey | fk_r_p_id_p_jd_fkey | "RI_FKey_check_ins" | "RI_FKey_check_ins"
+ fk_r_p_id_p_jd_fkey | fk_r_p_id_p_jd_fkey | "RI_FKey_check_ins" | "RI_FKey_check_ins"
+ fk_r_p_id_p_jd_fkey | fk_r_p_id_p_jd_fkey | "RI_FKey_check_upd" | "RI_FKey_check_upd"
+ fk_r_p_id_p_jd_fkey | fk_r_p_id_p_jd_fkey | "RI_FKey_check_upd" | "RI_FKey_check_upd"
+ fk_r_p_id_p_jd_fkey | | "RI_FKey_check_ins" |
+ fk_r_p_id_p_jd_fkey | | "RI_FKey_check_upd" |
+ fk_r_p_id_p_jd_fkey | | "RI_FKey_noaction_del" |
+ fk_r_p_id_p_jd_fkey | | "RI_FKey_noaction_upd" |
+ fk_r_p_id_p_jd_fkey1 | fk_r_p_id_p_jd_fkey | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey1 | fk_r_p_id_p_jd_fkey | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey2 | fk_r_p_id_p_jd_fkey1 | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey2 | fk_r_p_id_p_jd_fkey1 | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey3 | fk_r_p_id_p_jd_fkey1 | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey3 | fk_r_p_id_p_jd_fkey1 | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey4 | fk_r_p_id_p_jd_fkey | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey4 | fk_r_p_id_p_jd_fkey | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey5 | fk_r_p_id_p_jd_fkey4 | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey5 | fk_r_p_id_p_jd_fkey4 | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey6 | fk_r_p_id_p_jd_fkey4 | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey6 | fk_r_p_id_p_jd_fkey4 | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+(20 rows)
+
+INSERT INTO fk_r VALUES (1, 1, 1);
+ALTER TABLE fk_r DETACH PARTITION fk_r_1;
+\d fk_r_1
+ Table "fkpart12.fk_r_1"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ id | integer | | not null |
+ p_id | integer | | not null |
+ p_jd | integer | | not null |
+Indexes:
+ "fk_r_1_pkey" PRIMARY KEY, btree (id)
+Foreign-key constraints:
+ "fk_r_p_id_p_jd_fkey" FOREIGN KEY (p_id, p_jd) REFERENCES fk_p(id, jd)
+
+SELECT * FROM fkpart12_constraints();
+ conname | conrel | confrelid | conparent
+----------------------+--------+-----------+----------------------
+ fk_r_p_id_p_jd_fkey | fk_r | fk_p |
+ fk_r_p_id_p_jd_fkey1 | fk_r | fk_p_1 | fk_r_p_id_p_jd_fkey
+ fk_r_p_id_p_jd_fkey2 | fk_r | fk_p_1_1 | fk_r_p_id_p_jd_fkey1
+ fk_r_p_id_p_jd_fkey3 | fk_r | fk_p_1_2 | fk_r_p_id_p_jd_fkey1
+ fk_r_p_id_p_jd_fkey4 | fk_r | fk_p_2 | fk_r_p_id_p_jd_fkey
+ fk_r_p_id_p_jd_fkey5 | fk_r | fk_p_2_1 | fk_r_p_id_p_jd_fkey4
+ fk_r_p_id_p_jd_fkey6 | fk_r | fk_p_2_2 | fk_r_p_id_p_jd_fkey4
+ fk_r_p_id_p_jd_fkey | fk_r_2 | fk_p | fk_r_p_id_p_jd_fkey
+(8 rows)
+
+SELECT * FROM fkpart12_triggers();
+ conname | conparent | tgfoid | tgfparent
+----------------------+----------------------+------------------------+------------------------
+ fk_r_p_id_p_jd_fkey | fk_r_p_id_p_jd_fkey | "RI_FKey_check_ins" | "RI_FKey_check_ins"
+ fk_r_p_id_p_jd_fkey | fk_r_p_id_p_jd_fkey | "RI_FKey_check_upd" | "RI_FKey_check_upd"
+ fk_r_p_id_p_jd_fkey | | "RI_FKey_check_ins" |
+ fk_r_p_id_p_jd_fkey | | "RI_FKey_check_upd" |
+ fk_r_p_id_p_jd_fkey | | "RI_FKey_noaction_del" |
+ fk_r_p_id_p_jd_fkey | | "RI_FKey_noaction_upd" |
+ fk_r_p_id_p_jd_fkey1 | fk_r_p_id_p_jd_fkey | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey1 | fk_r_p_id_p_jd_fkey | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey2 | fk_r_p_id_p_jd_fkey1 | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey2 | fk_r_p_id_p_jd_fkey1 | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey3 | fk_r_p_id_p_jd_fkey1 | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey3 | fk_r_p_id_p_jd_fkey1 | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey4 | fk_r_p_id_p_jd_fkey | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey4 | fk_r_p_id_p_jd_fkey | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey5 | fk_r_p_id_p_jd_fkey4 | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey5 | fk_r_p_id_p_jd_fkey4 | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey6 | fk_r_p_id_p_jd_fkey4 | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey6 | fk_r_p_id_p_jd_fkey4 | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+(18 rows)
+
+INSERT INTO fk_r_1 VALUES (2, 1, 2); -- fails as EXPECTED
+ERROR: insert or update on table "fk_r_1" violates foreign key constraint "fk_r_p_id_p_jd_fkey"
+DETAIL: Key (p_id, p_jd)=(1, 2) is not present in table "fk_p".
+DELETE FROM fk_p; -- should fail but was buggy
+ERROR: update or delete on table "fk_p_1_1" violates foreign key constraint "fk_r_1_p_id_p_jd_fkey1" on table "fk_r_1"
+DETAIL: Key (id, jd)=(1, 1) is still referenced from table "fk_r_1".
+ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1);
+\d fk_r_1
+ Table "fkpart12.fk_r_1"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ id | integer | | not null |
+ p_id | integer | | not null |
+ p_jd | integer | | not null |
+Partition of: fk_r FOR VALUES IN (1)
+Indexes:
+ "fk_r_1_pkey" PRIMARY KEY, btree (id)
+Foreign-key constraints:
+ TABLE "fk_r" CONSTRAINT "fk_r_p_id_p_jd_fkey" FOREIGN KEY (p_id, p_jd) REFERENCES fk_p(id, jd)
+
+SELECT * FROM fkpart12_constraints();
+ conname | conrel | confrelid | conparent
+----------------------+--------+-----------+----------------------
+ fk_r_p_id_p_jd_fkey | fk_r | fk_p |
+ fk_r_p_id_p_jd_fkey1 | fk_r | fk_p_1 | fk_r_p_id_p_jd_fkey
+ fk_r_p_id_p_jd_fkey2 | fk_r | fk_p_1_1 | fk_r_p_id_p_jd_fkey1
+ fk_r_p_id_p_jd_fkey3 | fk_r | fk_p_1_2 | fk_r_p_id_p_jd_fkey1
+ fk_r_p_id_p_jd_fkey4 | fk_r | fk_p_2 | fk_r_p_id_p_jd_fkey
+ fk_r_p_id_p_jd_fkey5 | fk_r | fk_p_2_1 | fk_r_p_id_p_jd_fkey4
+ fk_r_p_id_p_jd_fkey6 | fk_r | fk_p_2_2 | fk_r_p_id_p_jd_fkey4
+ fk_r_p_id_p_jd_fkey | fk_r_1 | fk_p | fk_r_p_id_p_jd_fkey
+ fk_r_p_id_p_jd_fkey | fk_r_2 | fk_p | fk_r_p_id_p_jd_fkey
+(9 rows)
+
+SELECT * FROM fkpart12_triggers();
+ conname | conparent | tgfoid | tgfparent
+----------------------+----------------------+------------------------+------------------------
+ fk_r_p_id_p_jd_fkey | fk_r_p_id_p_jd_fkey | "RI_FKey_check_ins" | "RI_FKey_check_ins"
+ fk_r_p_id_p_jd_fkey | fk_r_p_id_p_jd_fkey | "RI_FKey_check_ins" | "RI_FKey_check_ins"
+ fk_r_p_id_p_jd_fkey | fk_r_p_id_p_jd_fkey | "RI_FKey_check_upd" | "RI_FKey_check_upd"
+ fk_r_p_id_p_jd_fkey | fk_r_p_id_p_jd_fkey | "RI_FKey_check_upd" | "RI_FKey_check_upd"
+ fk_r_p_id_p_jd_fkey | | "RI_FKey_check_ins" |
+ fk_r_p_id_p_jd_fkey | | "RI_FKey_check_upd" |
+ fk_r_p_id_p_jd_fkey | | "RI_FKey_noaction_del" |
+ fk_r_p_id_p_jd_fkey | | "RI_FKey_noaction_upd" |
+ fk_r_p_id_p_jd_fkey1 | fk_r_p_id_p_jd_fkey | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey1 | fk_r_p_id_p_jd_fkey | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey2 | fk_r_p_id_p_jd_fkey1 | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey2 | fk_r_p_id_p_jd_fkey1 | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey3 | fk_r_p_id_p_jd_fkey1 | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey3 | fk_r_p_id_p_jd_fkey1 | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey4 | fk_r_p_id_p_jd_fkey | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey4 | fk_r_p_id_p_jd_fkey | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey5 | fk_r_p_id_p_jd_fkey4 | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey5 | fk_r_p_id_p_jd_fkey4 | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey6 | fk_r_p_id_p_jd_fkey4 | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey6 | fk_r_p_id_p_jd_fkey4 | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+(20 rows)
+
+DELETE FROM fk_p; -- should fail but was buggy
+ERROR: update or delete on table "fk_p_1_1" violates foreign key constraint "fk_r_p_id_p_jd_fkey2" on table "fk_r"
+DETAIL: Key (id, jd)=(1, 1) is still referenced from table "fk_r".
+SET client_min_messages TO warning;
+DROP SCHEMA fkpart12 CASCADE;
+RESET client_min_messages;
+RESET search_path;
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index d1aac5357f..e96ab5c703 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -2086,3 +2086,104 @@ UPDATE fkpart11.pk SET a = 3 WHERE a = 4;
UPDATE fkpart11.pk SET a = 1 WHERE a = 2;
DROP SCHEMA fkpart11 CASCADE;
+
+-- When a table is attached as partition to a partitioned table that has
+-- a foreign key to another partitioned table, it acquires a clone of the
+-- FK. Upon detach, this clone is not removed, but instead becomes an
+-- independent FK. If it then attaches to the partitioned table again,
+-- the FK from the parent "takes over" ownership of the independent FK rather
+-- than creating a separate one.
+CREATE SCHEMA fkpart12
+ CREATE TABLE fk_p ( id int, jd int, PRIMARY KEY(id, jd)) PARTITION BY list (id)
+ CREATE TABLE fk_p_1 PARTITION OF fk_p FOR VALUES IN (1) PARTITION BY list (jd)
+ CREATE TABLE fk_p_1_1 PARTITION OF fk_p_1 FOR VALUES IN (1)
+ CREATE TABLE fk_p_1_2 PARTITION OF fk_p_1 FOR VALUES IN (2)
+ CREATE TABLE fk_p_2 PARTITION OF fk_p FOR VALUES IN (2) PARTITION BY list (jd)
+ CREATE TABLE fk_p_2_1 PARTITION OF fk_p_2 FOR VALUES IN (1)
+ CREATE TABLE fk_p_2_2 PARTITION OF fk_p_2 FOR VALUES IN (2)
+ CREATE TABLE fk_r_1 ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL)
+ CREATE TABLE fk_r_2 ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL)
+ CREATE TABLE fk_r ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL,
+ FOREIGN KEY (p_id, p_jd) REFERENCES fk_p (id, jd)
+ ) PARTITION BY list (id);
+SET search_path TO fkpart12;
+
+CREATE OR REPLACE FUNCTION
+fkpart12_constraints(OUT conname name, OUT conrel regclass,
+ OUT confrelid regclass, OUT conparent name) RETURNS SETOF record AS $$
+ WITH RECURSIVE r AS (
+ SELECT oid, conname, conrelid, confrelid, NULL::name AS conparent
+ FROM pg_constraint
+ WHERE conrelid = 'fk_r'::regclass
+ AND contype = 'f'
+ AND conparentid = 0
+ UNION ALL
+ SELECT c.oid, c.conname, c.conrelid, c.confrelid, r.conname
+ FROM pg_constraint c
+ JOIN r ON c.conparentid = r.oid
+ )
+ SELECT conname, conrelid::regclass, confrelid::regclass, conparent
+ FROM r
+ ORDER BY oid, conname, confrelid, conparent;
+$$
+LANGUAGE SQL;
+
+CREATE OR REPLACE FUNCTION
+fkpart12_triggers(OUT conname name, OUT conparent name,
+ OUT tgfoid regproc, OUT tgfparent regproc) RETURNS SETOF record AS $$
+
+ WITH RECURSIVE r AS (
+ SELECT t.oid, t.tgfoid::regproc, c.conname, NULL::name AS conparent, NULL::regproc AS tgfparent
+ FROM pg_trigger t
+ JOIN pg_constraint c ON c.oid = t.tgconstraint
+ WHERE c.conrelid = 'fk_r'::regclass
+ AND c.contype = 'f'
+ AND t.tgparentid = 0
+ UNION ALL
+ SELECT t2.oid, t2.tgfoid::regproc, c2.conname, c3.conname, r.tgfoid::regproc AS tgfparent
+ FROM pg_trigger t2
+ JOIN pg_constraint c2 ON c2.oid = t2.tgconstraint
+ LEFT JOIN pg_constraint c3 ON c3.oid = c2.conparentid
+ JOIN r ON r.oid = t2.tgparentid
+ )
+ SELECT conname, conparent, tgfoid, tgfparent
+ FROM r
+ ORDER BY conname, conparent, tgfoid, tgfparent;
+$$
+LANGUAGE SQL;
+
+SELECT * FROM fkpart12_constraints();
+SELECT * FROM fkpart12_triggers();
+
+INSERT INTO fk_p VALUES (1, 1);
+
+ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1);
+ALTER TABLE fk_r ATTACH PARTITION fk_r_2 FOR VALUES IN (2);
+
+\d fk_r_1
+SELECT * FROM fkpart12_constraints();
+SELECT * FROM fkpart12_triggers();
+
+INSERT INTO fk_r VALUES (1, 1, 1);
+
+ALTER TABLE fk_r DETACH PARTITION fk_r_1;
+
+\d fk_r_1
+SELECT * FROM fkpart12_constraints();
+SELECT * FROM fkpart12_triggers();
+
+INSERT INTO fk_r_1 VALUES (2, 1, 2); -- fails as EXPECTED
+DELETE FROM fk_p; -- should fail but was buggy
+
+ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1);
+
+\d fk_r_1
+SELECT * FROM fkpart12_constraints();
+SELECT * FROM fkpart12_triggers();
+
+DELETE FROM fk_p; -- should fail but was buggy
+
+SET client_min_messages TO warning;
+DROP SCHEMA fkpart12 CASCADE;
+RESET client_min_messages;
+RESET search_path;
--
2.46.0
v3-0002-Rework-foreign-key-mangling-during-ATTACH-DETACH.patchtext/x-patchDownload
From b1a81adac1a38f66d4ee6bc03846858eae26e3dd Mon Sep 17 00:00:00 2001
From: Jehan-Guillaume de Rorthais <jgdr@dalibo.com>
Date: Mon, 23 Sep 2024 18:53:12 +0200
Subject: [PATCH v3 2/4] Rework foreign key mangling during ATTACH/DETACH
... when the reference table is partitioned.
It turns out that the catalog representation we chose for foreign keys
connecting partitioned tables is suboptimal, particularly in the sense
that a standalone table has a different way to represent it when
referencing a partitioned table, than when the same table becomes a
partition (and vice versa). This difference means we need to spawn
additional catalog rows on detach, and remove them on attach.
As a very obvious symptom, we were missing action triggers after
detach, which means that you could update/delete rows from the
referenced partitioned table that still had referencing rows at the
other side previously detached, and fail to throw the required errors.
This means existing FKs might have rows that break relational
integrity.
To fix this bug, this patch rework addFkRecurseReferenced() so it
can be called from DetachPartitionFinalize(), so we can keep this
logic in a single function. The addFkRecurseReferenced() rework
leads to create addFkConstraint(), but it seems cleaner as
addFkRecurseReferenced() and addFkRecurseReferencing() have the same
logic now. Some other code factoring using addFkConstraint() are
added in next commits.
Another possible problem is that trying to reattach a table
that had been detached would fail indicating that internal triggers
cannot be found, which from the user's point of view is nonsensical.
We might want to rethink the representation in the future to avoid this
messiness, but the code now seems to do what's required to make the
constraints operate correctly.
co-author: Alvaro Herrera <alvherre@alvh.no-ip.org>
co-author: Jehan-Guillaume de Rorthais <jgdr@dalibo.com>
co-author: Tender Wang <tndrwang@gmail.com>
Reported-by: Guillaume Lelarge <guillaume@lelarge.info>
Reported-by: Jehan-Guillaume de Rorthais <jgdr@dalibo.com>
Reported-by: Thomas Baehler (SBB CFF FFS) <thomas.baehler2@sbb.ch>
Discussion: https://postgr.es/m/20230420144344.40744130@karst
Discussion: https://postgr.es/m/20230705233028.2f554f73@karst
Discussion: https://postgr.es/m/GVAP278MB02787E7134FD691861635A8BC9032@GVAP278MB0278.CHEP278.PROD.OUTLOOK.COM
Discussion: https://postgr.es/m/18541-628a61bc267cd2d3@postgresql.org
---
src/backend/commands/tablecmds.c | 376 ++++++++++++++++++++++++-------
1 file changed, 295 insertions(+), 81 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d27e6cf345..f7e0b8fa87 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -508,7 +508,13 @@ static ObjectAddress ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *
Relation rel, Constraint *fkconstraint,
bool recurse, bool recursing,
LOCKMODE lockmode);
-static ObjectAddress addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint,
+static ObjectAddress addFkConstraint(Constraint *fkconstraint, Relation rel,
+ Relation pkrel, Oid indexOid, Oid parentConstr,
+ int numfks, int16 *pkattnum, int16 *fkattnum,
+ Oid *pfeqoperators, Oid *ppeqoperators,
+ Oid *ffeqoperators, int numfkdelsetcols,
+ int16 *fkdelsetcols, bool with_period);
+static void addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint,
Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks, int16 *pkattnum, int16 *fkattnum,
Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
@@ -652,9 +658,11 @@ static void DropClonedTriggersFromPartition(Oid partitionId);
static ObjectAddress ATExecDetachPartition(List **wqueue, AlteredTableInfo *tab,
Relation rel, RangeVar *name,
bool concurrent);
-static void DetachPartitionFinalize(Relation rel, Relation partRel,
- bool concurrent, Oid defaultPartOid);
-static ObjectAddress ATExecDetachPartitionFinalize(Relation rel, RangeVar *name);
+static void DetachPartitionFinalize(List **wqueue, Relation rel,
+ Relation partRel, bool concurrent,
+ Oid defaultPartOid);
+static ObjectAddress ATExecDetachPartitionFinalize(List **wqueue, Relation rel,
+ RangeVar *name);
static ObjectAddress ATExecAttachPartitionIdx(List **wqueue, Relation parentIdx,
RangeVar *name);
static void validatePartitionedIndex(Relation partedIdx, Relation partedTbl);
@@ -5515,7 +5523,7 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
((PartitionCmd *) cmd->def)->concurrent);
break;
case AT_DetachPartitionFinalize:
- address = ATExecDetachPartitionFinalize(rel, ((PartitionCmd *) cmd->def)->name);
+ address = ATExecDetachPartitionFinalize(wqueue, rel, ((PartitionCmd *) cmd->def)->name);
break;
default: /* oops */
elog(ERROR, "unrecognized alter table type: %d",
@@ -10061,9 +10069,22 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
* Create all the constraint and trigger objects, recursing to partitions
* as necessary. First handle the referenced side.
*/
- address = addFkRecurseReferenced(wqueue, fkconstraint, rel, pkrel,
+ address = addFkConstraint(fkconstraint, rel, pkrel,
+ indexOid,
+ InvalidOid, /* no parent constraint */
+ numfks,
+ pkattnum,
+ fkattnum,
+ pfeqoperators,
+ ppeqoperators,
+ ffeqoperators,
+ numfkdelsetcols,
+ fkdelsetcols,
+ with_period);
+
+ addFkRecurseReferenced(wqueue, fkconstraint, rel, pkrel,
indexOid,
- InvalidOid, /* no parent constraint */
+ address.objectId,
numfks,
pkattnum,
fkattnum,
@@ -10137,47 +10158,33 @@ validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
}
/*
- * addFkRecurseReferenced
- * subroutine for ATAddForeignKeyConstraint; recurses on the referenced
- * side of the constraint
+ * addFkConstraint
+ * Add a new Foreign Key constraint.
*
- * Create pg_constraint rows for the referenced side of the constraint,
- * referencing the parent of the referencing side; also create action triggers
- * on leaf partitions. If the table is partitioned, recurse to handle each
- * partition.
+ * Creates a pg_constraint rows referencing a parent if given.
*
- * wqueue is the ALTER TABLE work queue; can be NULL when not running as part
- * of an ALTER TABLE sequence.
- * fkconstraint is the constraint being added.
- * rel is the root referencing relation.
- * pkrel is the referenced relation; might be a partition, if recursing.
- * indexOid is the OID of the index (on pkrel) implementing this constraint.
- * parentConstr is the OID of a parent constraint; InvalidOid if this is a
- * top-level constraint.
- * numfks is the number of columns in the foreign key
- * pkattnum is the attnum array of referenced attributes.
- * fkattnum is the attnum array of referencing attributes.
- * numfkdelsetcols is the number of columns in the ON DELETE SET NULL/DEFAULT
- * (...) clause
- * fkdelsetcols is the attnum array of the columns in the ON DELETE SET
- * NULL/DEFAULT clause
- * pf/pp/ffeqoperators are OID array of operators between columns.
- * old_check_ok signals that this constraint replaces an existing one that
- * was already validated (thus this one doesn't need validation).
- * parentDelTrigger and parentUpdTrigger, when being recursively called on
- * a partition, are the OIDs of the parent action triggers for DELETE and
- * UPDATE respectively.
+ * * fkconstraint is the constraint being added.
+ * * rel is the root referencing relation.
+ * * pkrel is the referenced relation; might be a partition, if recursing.
+ * * indexOid is the OID of the index (on pkrel) implementing this constraint.
+ * * parentConstr is the OID of a parent constraint; InvalidOid if this is a
+ * top-level constraint.
+ * * numfks is the number of columns in the foreign key.
+ * * pkattnum is the attnum array of referenced attributes.
+ * * fkattnum is the attnum array of referencing attributes.
+ * * pf/pp/ffeqoperators are OID array of operators between columns.
+ * * numfkdelsetcols is the number of columns in the ON DELETE SET NULL/DEFAULT
+ * (...) clause.
+ * * fkdelsetcols is the attnum array of the columns in the ON DELETE SET
+ * NULL/DEFAULT clause.
+ * - with_period: true if this is a temporal FKs
*/
static ObjectAddress
-addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
- Relation pkrel, Oid indexOid, Oid parentConstr,
- int numfks,
- int16 *pkattnum, int16 *fkattnum, Oid *pfeqoperators,
- Oid *ppeqoperators, Oid *ffeqoperators,
- int numfkdelsetcols, int16 *fkdelsetcols,
- bool old_check_ok,
- Oid parentDelTrigger, Oid parentUpdTrigger,
- bool with_period)
+addFkConstraint(Constraint *fkconstraint, Relation rel, Relation pkrel,
+ Oid indexOid, Oid parentConstr, int numfks, int16 *pkattnum,
+ int16 *fkattnum, Oid *pfeqoperators, Oid *ppeqoperators,
+ Oid *ffeqoperators, int numfkdelsetcols, int16 *fkdelsetcols,
+ bool with_period)
{
ObjectAddress address;
Oid constrOid;
@@ -10185,8 +10192,6 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
bool conislocal;
int coninhcount;
bool connoinherit;
- Oid deleteTriggerOid,
- updateTriggerOid;
/*
* Verify relkind for each referenced partition. At the top level, this
@@ -10285,12 +10290,63 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
/* make new constraint visible, in case we add more */
CommandCounterIncrement();
+ return address;
+}
+
+/*
+ * addFkRecurseReferenced
+ * subroutine for ATAddForeignKeyConstraint; recurses on the referenced
+ * side of the constraint
+ *
+ * If the referenced relation is a plain relation, create the necessary check
+ * triggers that implement the constraint. If the referenced relation is a
+ * partitioned table, then we create a pg_constraint row referencing the parent
+ * of the referencing side for it and recurse on this routine for each
+ * partition.
+ *
+ * wqueue is the ALTER TABLE work queue; can be NULL when not running as part
+ * of an ALTER TABLE sequence.
+ * fkconstraint is the constraint being added.
+ * rel is the root referencing relation.
+ * pkrel is the referenced relation; might be a partition, if recursing.
+ * indexOid is the OID of the index (on pkrel) implementing this constraint.
+ * parentConstr is the OID of a parent constraint; InvalidOid if this is a
+ * top-level constraint.
+ * numfks is the number of columns in the foreign key
+ * pkattnum is the attnum array of referenced attributes.
+ * fkattnum is the attnum array of referencing attributes.
+ * numfkdelsetcols is the number of columns in the ON DELETE SET NULL/DEFAULT
+ * (...) clause
+ * fkdelsetcols is the attnum array of the columns in the ON DELETE SET
+ * NULL/DEFAULT clause
+ * pf/pp/ffeqoperators are OID array of operators between columns.
+ * old_check_ok signals that this constraint replaces an existing one that
+ * was already validated (thus this one doesn't need validation).
+ * parentDelTrigger and parentUpdTrigger, when being recursively called on
+ * a partition, are the OIDs of the parent action triggers for DELETE and
+ * UPDATE respectively.
+ * with_period: true if this is a temporal FKs
+ */
+static void
+addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
+ Relation pkrel, Oid indexOid, Oid parentConstr,
+ int numfks,
+ int16 *pkattnum, int16 *fkattnum, Oid *pfeqoperators,
+ Oid *ppeqoperators, Oid *ffeqoperators,
+ int numfkdelsetcols, int16 *fkdelsetcols,
+ bool old_check_ok,
+ Oid parentDelTrigger, Oid parentUpdTrigger,
+ bool with_period)
+{
+ Oid deleteTriggerOid,
+ updateTriggerOid;
+
/*
* Create the action triggers that enforce the constraint.
*/
createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
fkconstraint,
- constrOid, indexOid,
+ parentConstr, indexOid,
parentDelTrigger, parentUpdTrigger,
&deleteTriggerOid, &updateTriggerOid);
@@ -10309,6 +10365,7 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
AttrMap *map;
AttrNumber *mapped_pkattnum;
Oid partIndexId;
+ ObjectAddress address;
partRel = table_open(pd->oids[i], ShareRowExclusiveLock);
@@ -10333,8 +10390,14 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
if (!OidIsValid(partIndexId))
elog(ERROR, "index for %u not found in partition %s",
indexOid, RelationGetRelationName(partRel));
+
+ address = addFkConstraint(fkconstraint, rel, partRel,
+ partIndexId, parentConstr, numfks,
+ mapped_pkattnum, fkattnum,
+ pfeqoperators, ppeqoperators, ffeqoperators,
+ numfkdelsetcols, fkdelsetcols, with_period);
addFkRecurseReferenced(wqueue, fkconstraint, rel, partRel,
- partIndexId, constrOid, numfks,
+ partIndexId, address.objectId, numfks,
mapped_pkattnum, fkattnum,
pfeqoperators, ppeqoperators, ffeqoperators,
numfkdelsetcols, fkdelsetcols,
@@ -10351,8 +10414,6 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
}
}
}
-
- return address;
}
/*
@@ -10718,6 +10779,7 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
int numfkdelsetcols;
AttrNumber confdelsetcols[INDEX_MAX_KEYS];
Constraint *fkconstraint;
+ ObjectAddress address;
Oid deleteTriggerOid,
updateTriggerOid;
@@ -10751,7 +10813,7 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
* Because we're only expanding the key space at the referenced side,
* we don't need to prevent any operation in the referencing table, so
* AccessShareLock suffices (assumes that dropping the constraint
- * acquires AEL).
+ * acquires AccessExclusiveLock).
*/
fkRel = table_open(constrForm->conrelid, AccessShareLock);
@@ -10817,12 +10879,17 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
constrForm->confrelid, constrForm->conrelid,
&deleteTriggerOid, &updateTriggerOid);
+ address = addFkConstraint(fkconstraint, fkRel, partitionRel, partIndexId,
+ constrOid, numfks, mapped_confkey, conkey,
+ conpfeqop, conppeqop, conffeqop,
+ numfkdelsetcols, confdelsetcols,
+ constrForm->conperiod);
addFkRecurseReferenced(NULL,
fkconstraint,
fkRel,
partitionRel,
partIndexId,
- constrOid,
+ address.objectId,
numfks,
mapped_confkey,
conkey,
@@ -11279,6 +11346,83 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
partRelid);
+ /*
+ * If the referenced table is partitioned, then the partition we're
+ * attaching now has extra pg_constraint rows and action triggers that are
+ * no longer needed. Remove those.
+ */
+ if (get_rel_relkind(fk->confrelid) == RELKIND_PARTITIONED_TABLE)
+ {
+ Relation pg_constraint = table_open(ConstraintRelationId, RowShareLock);
+ ObjectAddresses *objs;
+ HeapTuple consttup;
+
+ ScanKeyInit(&key,
+ Anum_pg_constraint_conrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(fk->conrelid));
+
+ scan = systable_beginscan(pg_constraint,
+ ConstraintRelidTypidNameIndexId,
+ true, NULL, 1, &key);
+ objs = new_object_addresses();
+ while ((consttup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_constraint conform = (Form_pg_constraint) GETSTRUCT(consttup);
+
+ if (conform->conparentid != fk->conoid)
+ continue;
+ else
+ {
+ ObjectAddress addr;
+ int n;
+ SysScanDesc scan2;
+ ScanKeyData key2;
+
+ ObjectAddressSet(addr, ConstraintRelationId, conform->oid);
+ add_exact_object_address(&addr, objs);
+
+ /*
+ * First we must delete the dependency records that bind
+ * the constraint records together.
+ */
+ n = deleteDependencyRecordsForSpecific(ConstraintRelationId,
+ conform->oid,
+ DEPENDENCY_INTERNAL,
+ ConstraintRelationId,
+ fk->conoid);
+ if (n != 1)
+ elog(WARNING, "oops: found %d instead of 1 deps from %u to %u",
+ n, conform->oid, fk->conoid);
+
+ /*
+ * Now search for the triggers for this constraint and set
+ * them up for deletion too
+ */
+ ScanKeyInit(&key2,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conform->oid));
+ scan2 = systable_beginscan(trigrel, TriggerConstraintIndexId,
+ true, NULL, 1, &key2);
+ while ((trigtup = systable_getnext(scan2)) != NULL)
+ {
+ ObjectAddressSet(addr, TriggerRelationId,
+ ((Form_pg_trigger) GETSTRUCT(trigtup))->oid);
+ add_exact_object_address(&addr, objs);
+ }
+ systable_endscan(scan2);
+ }
+ }
+ /* make the dependency deletions visible */
+ CommandCounterIncrement();
+ performMultipleDeletions(objs, DROP_RESTRICT,
+ PERFORM_DELETION_INTERNAL);
+ systable_endscan(scan);
+
+ table_close(pg_constraint, RowShareLock);
+ }
+
CommandCounterIncrement();
return true;
}
@@ -19293,7 +19437,7 @@ ATExecDetachPartition(List **wqueue, AlteredTableInfo *tab, Relation rel,
}
/* Do the final part of detaching */
- DetachPartitionFinalize(rel, partRel, concurrent, defaultPartOid);
+ DetachPartitionFinalize(wqueue, rel, partRel, concurrent, defaultPartOid);
ObjectAddressSet(address, RelationRelationId, RelationGetRelid(partRel));
@@ -19310,8 +19454,8 @@ ATExecDetachPartition(List **wqueue, AlteredTableInfo *tab, Relation rel,
* transaction of the concurrent algorithm fails (crash or abort).
*/
static void
-DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
- Oid defaultPartOid)
+DetachPartitionFinalize(List **wqueue, Relation rel, Relation partRel,
+ bool concurrent, Oid defaultPartOid)
{
Relation classRel;
List *fks;
@@ -19347,8 +19491,11 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
{
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
+ HeapTuple parentConTup;
Form_pg_constraint conform;
+ Form_pg_constraint parentConForm;
Constraint *fkconstraint;
+ Oid parentConstrOid;
Oid insertTriggerOid,
updateTriggerOid;
@@ -19365,7 +19512,20 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
continue;
}
- /* unset conparentid and adjust conislocal, coninhcount, etc. */
+ parentConstrOid = conform->conparentid;
+
+ Assert(OidIsValid(conform->conparentid));
+ parentConTup = SearchSysCache1(CONSTROID,
+ ObjectIdGetDatum(parentConstrOid));
+ if (!HeapTupleIsValid(parentConTup))
+ elog(ERROR, "cache lookup failed for constraint %u",
+ conform->conparentid);
+ parentConForm = (Form_pg_constraint) GETSTRUCT(parentConTup);
+
+ /*
+ * The constraint on this table must be marked no longer a child of
+ * the parent's constraint, as do its check triggers.
+ */
ConstraintSetParentConstraint(fk->conoid, InvalidOid, InvalidOid);
/*
@@ -19383,35 +19543,89 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
RelationGetRelid(partRel));
/*
- * Make the action triggers on the referenced relation. When this was
- * a partition the action triggers pointed to the parent rel (they
- * still do), but now we need separate ones of our own.
+ * If the referenced side is partitioned (which we know because our
+ * parent's constraint points to a different relation than ours) then
+ * we must, in addition to the above, create pg_constraint rows that
+ * point to each partition, each with its own action triggers.
*/
- fkconstraint = makeNode(Constraint);
- fkconstraint->contype = CONSTRAINT_FOREIGN;
- fkconstraint->conname = pstrdup(NameStr(conform->conname));
- fkconstraint->deferrable = conform->condeferrable;
- fkconstraint->initdeferred = conform->condeferred;
- fkconstraint->location = -1;
- fkconstraint->pktable = NULL;
- fkconstraint->fk_attrs = NIL;
- fkconstraint->pk_attrs = NIL;
- fkconstraint->fk_matchtype = conform->confmatchtype;
- fkconstraint->fk_upd_action = conform->confupdtype;
- fkconstraint->fk_del_action = conform->confdeltype;
- fkconstraint->fk_del_set_cols = NIL;
- fkconstraint->old_conpfeqop = NIL;
- fkconstraint->old_pktable_oid = InvalidOid;
- fkconstraint->skip_validation = false;
- fkconstraint->initially_valid = true;
+ if (parentConForm->conrelid != conform->conrelid)
+ {
+ int numfks;
+ AttrNumber conkey[INDEX_MAX_KEYS];
+ AttrMap *attmap;
+ AttrNumber confkey[INDEX_MAX_KEYS];
+ Oid conpfeqop[INDEX_MAX_KEYS];
+ Oid conppeqop[INDEX_MAX_KEYS];
+ Oid conffeqop[INDEX_MAX_KEYS];
+ int numfkdelsetcols;
+ AttrNumber confdelsetcols[INDEX_MAX_KEYS];
+ Relation refdRel;
+
+ DeconstructFkConstraintRow(contup,
+ &numfks,
+ conkey,
+ confkey,
+ conpfeqop,
+ conppeqop,
+ conffeqop,
+ &numfkdelsetcols,
+ confdelsetcols);
+
+ /* Create a synthetic node we'll use throughout */
+ fkconstraint = makeNode(Constraint);
+ fkconstraint->contype = CONSTRAINT_FOREIGN;
+ fkconstraint->conname = pstrdup(NameStr(conform->conname));
+ fkconstraint->deferrable = conform->condeferrable;
+ fkconstraint->initdeferred = conform->condeferred;
+ fkconstraint->skip_validation = true;
+ fkconstraint->initially_valid = true;
+ /* a few irrelevant fields omitted here */
+ fkconstraint->pktable = NULL;
+ fkconstraint->fk_attrs = NIL;
+ fkconstraint->pk_attrs = NIL;
+ fkconstraint->fk_matchtype = conform->confmatchtype;
+ fkconstraint->fk_upd_action = conform->confupdtype;
+ fkconstraint->fk_del_action = conform->confdeltype;
+ fkconstraint->fk_del_set_cols = NIL;
+ fkconstraint->old_conpfeqop = NIL;
+ fkconstraint->old_pktable_oid = InvalidOid;
+ fkconstraint->location = -1;
+
+ attmap = build_attrmap_by_name(RelationGetDescr(partRel),
+ RelationGetDescr(rel),
+ false);
+ for (int i = 0; i < numfks; i++)
+ {
+ Form_pg_attribute att;
- createForeignKeyActionTriggers(partRel, conform->confrelid,
- fkconstraint, fk->conoid,
+ att = TupleDescAttr(RelationGetDescr(partRel),
+ attmap->attnums[conkey[i] - 1] - 1);
+ fkconstraint->fk_attrs = lappend(fkconstraint->fk_attrs,
+ makeString(NameStr(att->attname)));
+ }
+
+ refdRel = table_open(fk->confrelid, AccessShareLock);
+
+ addFkRecurseReferenced(wqueue, fkconstraint, partRel,
+ refdRel,
conform->conindid,
+ fk->conoid,
+ numfks,
+ confkey,
+ conkey,
+ conpfeqop,
+ conppeqop,
+ conffeqop,
+ numfkdelsetcols,
+ confdelsetcols,
+ true,
InvalidOid, InvalidOid,
- NULL, NULL);
+ conform->conperiod);
+ table_close(refdRel, AccessShareLock);
+ }
ReleaseSysCache(contup);
+ ReleaseSysCache(parentConTup);
}
list_free_deep(fks);
if (trigrel)
@@ -19558,7 +19772,7 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
* completion; this completes the detaching process.
*/
static ObjectAddress
-ATExecDetachPartitionFinalize(Relation rel, RangeVar *name)
+ATExecDetachPartitionFinalize(List **wqueue, Relation rel, RangeVar *name)
{
Relation partRel;
ObjectAddress address;
@@ -19576,7 +19790,7 @@ ATExecDetachPartitionFinalize(Relation rel, RangeVar *name)
*/
WaitForOlderSnapshots(snap->xmin, false);
- DetachPartitionFinalize(rel, partRel, true, InvalidOid);
+ DetachPartitionFinalize(wqueue, rel, partRel, true, InvalidOid);
ObjectAddressSet(address, RelationRelationId, RelationGetRelid(partRel));
--
2.46.0
v3-0003-Use-addFkConstraint-in-addFkRecurseReferencing.patchtext/x-patchDownload
From a5a79e9046e5b527144a0b725e1edb5a58546334 Mon Sep 17 00:00:00 2001
From: Jehan-Guillaume de Rorthais <jgdr@dalibo.com>
Date: Tue, 24 Sep 2024 18:10:33 +0200
Subject: [PATCH v3 3/4] Use addFkConstraint() in addFkRecurseReferencing()
---
src/backend/commands/tablecmds.c | 124 ++++++++++++-------------------
1 file changed, 49 insertions(+), 75 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index f7e0b8fa87..9dd4f2e94f 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -348,6 +348,14 @@ typedef struct ForeignTruncateInfo
List *rels;
} ForeignTruncateInfo;
+typedef enum FKConstraintSide
+{
+ FK_BOTH_SIDE,
+ FK_REFERENCED_SIDE,
+ FK_REFERENCING_SIDE
+}
+FKConstraintSide;
+
/*
* Partition tables are expected to be dropped when the parent partitioned
* table gets dropped. Hence for partitioning we use AUTO dependency.
@@ -509,7 +517,8 @@ static ObjectAddress ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *
bool recurse, bool recursing,
LOCKMODE lockmode);
static ObjectAddress addFkConstraint(Constraint *fkconstraint, Relation rel,
- Relation pkrel, Oid indexOid, Oid parentConstr,
+ Relation pkrel, Oid indexOid,
+ Oid parentConstr, FKConstraintSide fkside,
int numfks, int16 *pkattnum, int16 *fkattnum,
Oid *pfeqoperators, Oid *ppeqoperators,
Oid *ffeqoperators, int numfkdelsetcols,
@@ -10072,6 +10081,7 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
address = addFkConstraint(fkconstraint, rel, pkrel,
indexOid,
InvalidOid, /* no parent constraint */
+ FK_BOTH_SIDE,
numfks,
pkattnum,
fkattnum,
@@ -10169,6 +10179,7 @@ validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
* * indexOid is the OID of the index (on pkrel) implementing this constraint.
* * parentConstr is the OID of a parent constraint; InvalidOid if this is a
* top-level constraint.
+ * * fkside is the side of FK related to this inherited constraint.
* * numfks is the number of columns in the foreign key.
* * pkattnum is the attnum array of referenced attributes.
* * fkattnum is the attnum array of referencing attributes.
@@ -10181,8 +10192,9 @@ validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
*/
static ObjectAddress
addFkConstraint(Constraint *fkconstraint, Relation rel, Relation pkrel,
- Oid indexOid, Oid parentConstr, int numfks, int16 *pkattnum,
- int16 *fkattnum, Oid *pfeqoperators, Oid *ppeqoperators,
+ Oid indexOid, Oid parentConstr, FKConstraintSide fkside,
+ int numfks, int16 *pkattnum, int16 *fkattnum,
+ Oid *pfeqoperators, Oid *ppeqoperators,
Oid *ffeqoperators, int numfkdelsetcols, int16 *fkdelsetcols,
bool with_period)
{
@@ -10273,18 +10285,31 @@ addFkConstraint(Constraint *fkconstraint, Relation rel, Relation pkrel,
ObjectAddressSet(address, ConstraintRelationId, constrOid);
- /*
- * Mark the child constraint as part of the parent constraint; it must not
- * be dropped on its own. (This constraint is deleted when the partition
- * is detached, but a special check needs to occur that the partition
- * contains no referenced values.)
- */
if (OidIsValid(parentConstr))
{
ObjectAddress referenced;
ObjectAddressSet(referenced, ConstraintRelationId, parentConstr);
- recordDependencyOn(&address, &referenced, DEPENDENCY_INTERNAL);
+ if (fkside == FK_REFERENCED_SIDE)
+ {
+ /*
+ * Mark the child constraint as part of the parent constraint; it must not
+ * be dropped on its own. (This constraint is deleted when the partition
+ * is detached, but a special check needs to occur that the partition
+ * contains no referenced values.)
+ */
+ recordDependencyOn(&address, &referenced, DEPENDENCY_INTERNAL);
+ }
+ else
+ {
+ /*
+ * Give this constraint partition-type dependencies on the parent
+ * constraint as well as the table.
+ */
+ recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_PRI);
+ ObjectAddressSet(referenced, RelationRelationId, RelationGetRelid(rel));
+ recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_SEC);
+ }
}
/* make new constraint visible, in case we add more */
@@ -10392,8 +10417,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
indexOid, RelationGetRelationName(partRel));
address = addFkConstraint(fkconstraint, rel, partRel,
- partIndexId, parentConstr, numfks,
- mapped_pkattnum, fkattnum,
+ partIndexId, parentConstr, FK_REFERENCED_SIDE,
+ numfks, mapped_pkattnum, fkattnum,
pfeqoperators, ppeqoperators, ffeqoperators,
numfkdelsetcols, fkdelsetcols, with_period);
addFkRecurseReferenced(wqueue, fkconstraint, rel, partRel,
@@ -10540,10 +10565,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
AttrMap *attmap;
AttrNumber mapped_fkattnum[INDEX_MAX_KEYS];
bool attached;
- char *conname;
- Oid constrOid;
- ObjectAddress address,
- referenced;
+ ObjectAddress address;
ListCell *cell;
CheckAlterTableIsSafe(partition);
@@ -10586,66 +10608,18 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
/*
* No luck finding a good constraint to reuse; create our own.
*/
- if (ConstraintNameIsUsed(CONSTRAINT_RELATION,
- RelationGetRelid(partition),
- fkconstraint->conname))
- conname = ChooseConstraintName(RelationGetRelationName(partition),
- ChooseForeignKeyConstraintNameAddition(fkconstraint->fk_attrs),
- "fkey",
- RelationGetNamespace(partition), NIL);
- else
- conname = fkconstraint->conname;
- constrOid =
- CreateConstraintEntry(conname,
- RelationGetNamespace(partition),
- CONSTRAINT_FOREIGN,
- fkconstraint->deferrable,
- fkconstraint->initdeferred,
- fkconstraint->initially_valid,
- parentConstr,
- partitionId,
- mapped_fkattnum,
- numfks,
- numfks,
- InvalidOid,
- indexOid,
- RelationGetRelid(pkrel),
- pkattnum,
- pfeqoperators,
- ppeqoperators,
- ffeqoperators,
- numfks,
- fkconstraint->fk_upd_action,
- fkconstraint->fk_del_action,
- fkdelsetcols,
- numfkdelsetcols,
- fkconstraint->fk_matchtype,
- NULL,
- NULL,
- NULL,
- false,
- 1,
- false,
- with_period, /* conPeriod */
- false);
-
- /*
- * Give this constraint partition-type dependencies on the parent
- * constraint as well as the table.
- */
- ObjectAddressSet(address, ConstraintRelationId, constrOid);
- ObjectAddressSet(referenced, ConstraintRelationId, parentConstr);
- recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_PRI);
- ObjectAddressSet(referenced, RelationRelationId, partitionId);
- recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_SEC);
-
- /* Make all this visible before recursing */
- CommandCounterIncrement();
+ address = addFkConstraint(fkconstraint,
+ partition, pkrel, indexOid, parentConstr,
+ FK_REFERENCING_SIDE, numfks, pkattnum,
+ mapped_fkattnum, pfeqoperators,
+ ppeqoperators, ffeqoperators,
+ numfkdelsetcols, fkdelsetcols,
+ with_period);
/* call ourselves to finalize the creation and we're done */
addFkRecurseReferencing(wqueue, fkconstraint, partition, pkrel,
indexOid,
- constrOid,
+ address.objectId,
numfks,
pkattnum,
mapped_fkattnum,
@@ -10880,9 +10854,9 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
&deleteTriggerOid, &updateTriggerOid);
address = addFkConstraint(fkconstraint, fkRel, partitionRel, partIndexId,
- constrOid, numfks, mapped_confkey, conkey,
- conpfeqop, conppeqop, conffeqop,
- numfkdelsetcols, confdelsetcols,
+ constrOid, FK_REFERENCED_SIDE, numfks,
+ mapped_confkey, conkey, conpfeqop, conppeqop,
+ conffeqop, numfkdelsetcols, confdelsetcols,
constrForm->conperiod);
addFkRecurseReferenced(NULL,
fkconstraint,
--
2.46.0
v3-0004-Use-addFkConstraint-in-CloneFkReferencing.patchtext/x-patchDownload
From abea3cc9c2236d7c2b2d973317dcde31b6553e31 Mon Sep 17 00:00:00 2001
From: Jehan-Guillaume de Rorthais <jgdr@dalibo.com>
Date: Wed, 25 Sep 2024 13:31:07 +0200
Subject: [PATCH v3 4/4] Use addFkConstraint() in CloneFkReferencing()
---
src/backend/commands/tablecmds.c | 124 +++++++++++--------------------
1 file changed, 43 insertions(+), 81 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 9dd4f2e94f..6a1d06ecd5 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -516,13 +516,15 @@ static ObjectAddress ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *
Relation rel, Constraint *fkconstraint,
bool recurse, bool recursing,
LOCKMODE lockmode);
-static ObjectAddress addFkConstraint(Constraint *fkconstraint, Relation rel,
+static ObjectAddress addFkConstraint(char *constraintname,
+ Constraint *fkconstraint, Relation rel,
Relation pkrel, Oid indexOid,
Oid parentConstr, FKConstraintSide fkside,
int numfks, int16 *pkattnum, int16 *fkattnum,
Oid *pfeqoperators, Oid *ppeqoperators,
Oid *ffeqoperators, int numfkdelsetcols,
- int16 *fkdelsetcols, bool with_period);
+ int16 *fkdelsetcols, bool is_internal,
+ bool with_period);
static void addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint,
Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks, int16 *pkattnum, int16 *fkattnum,
@@ -10078,7 +10080,7 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
* Create all the constraint and trigger objects, recursing to partitions
* as necessary. First handle the referenced side.
*/
- address = addFkConstraint(fkconstraint, rel, pkrel,
+ address = addFkConstraint(fkconstraint->conname, fkconstraint, rel, pkrel,
indexOid,
InvalidOid, /* no parent constraint */
FK_BOTH_SIDE,
@@ -10090,6 +10092,7 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
ffeqoperators,
numfkdelsetcols,
fkdelsetcols,
+ false,
with_period);
addFkRecurseReferenced(wqueue, fkconstraint, rel, pkrel,
@@ -10173,6 +10176,8 @@ validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
*
* Creates a pg_constraint rows referencing a parent if given.
*
+ * * constraintname the base name for the constraint being added. This is
+ * copied to fkconstraint->conname if the later is not set.
* * fkconstraint is the constraint being added.
* * rel is the root referencing relation.
* * pkrel is the referenced relation; might be a partition, if recursing.
@@ -10191,12 +10196,12 @@ validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
* - with_period: true if this is a temporal FKs
*/
static ObjectAddress
-addFkConstraint(Constraint *fkconstraint, Relation rel, Relation pkrel,
- Oid indexOid, Oid parentConstr, FKConstraintSide fkside,
- int numfks, int16 *pkattnum, int16 *fkattnum,
- Oid *pfeqoperators, Oid *ppeqoperators,
+addFkConstraint(char *constraintname, Constraint *fkconstraint, Relation rel,
+ Relation pkrel, Oid indexOid, Oid parentConstr,
+ FKConstraintSide fkside, int numfks, int16 *pkattnum,
+ int16 *fkattnum, Oid *pfeqoperators, Oid *ppeqoperators,
Oid *ffeqoperators, int numfkdelsetcols, int16 *fkdelsetcols,
- bool with_period)
+ bool is_internal, bool with_period)
{
ObjectAddress address;
Oid constrOid;
@@ -10222,13 +10227,16 @@ addFkConstraint(Constraint *fkconstraint, Relation rel, Relation pkrel,
*/
if (ConstraintNameIsUsed(CONSTRAINT_RELATION,
RelationGetRelid(rel),
- fkconstraint->conname))
+ constraintname))
conname = ChooseConstraintName(RelationGetRelationName(rel),
ChooseForeignKeyConstraintNameAddition(fkconstraint->fk_attrs),
"fkey",
RelationGetNamespace(rel), NIL);
else
- conname = fkconstraint->conname;
+ conname = constraintname;
+
+ if (fkconstraint->conname == NULL)
+ fkconstraint->conname = pstrdup(conname);
if (OidIsValid(parentConstr))
{
@@ -10281,7 +10289,7 @@ addFkConstraint(Constraint *fkconstraint, Relation rel, Relation pkrel,
coninhcount, /* inhcount */
connoinherit, /* conNoInherit */
with_period, /* conPeriod */
- false); /* is_internal */
+ is_internal); /* is_internal */
ObjectAddressSet(address, ConstraintRelationId, constrOid);
@@ -10416,11 +10424,12 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
elog(ERROR, "index for %u not found in partition %s",
indexOid, RelationGetRelationName(partRel));
- address = addFkConstraint(fkconstraint, rel, partRel,
- partIndexId, parentConstr, FK_REFERENCED_SIDE,
- numfks, mapped_pkattnum, fkattnum,
- pfeqoperators, ppeqoperators, ffeqoperators,
- numfkdelsetcols, fkdelsetcols, with_period);
+ address = addFkConstraint(fkconstraint->conname, fkconstraint, rel,
+ partRel, partIndexId, parentConstr,
+ FK_REFERENCED_SIDE, numfks, mapped_pkattnum,
+ fkattnum, pfeqoperators, ppeqoperators,
+ ffeqoperators, numfkdelsetcols,
+ fkdelsetcols, false, with_period);
addFkRecurseReferenced(wqueue, fkconstraint, rel, partRel,
partIndexId, address.objectId, numfks,
mapped_pkattnum, fkattnum,
@@ -10608,12 +10617,12 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
/*
* No luck finding a good constraint to reuse; create our own.
*/
- address = addFkConstraint(fkconstraint,
+ address = addFkConstraint(fkconstraint->conname, fkconstraint,
partition, pkrel, indexOid, parentConstr,
FK_REFERENCING_SIDE, numfks, pkattnum,
mapped_fkattnum, pfeqoperators,
ppeqoperators, ffeqoperators,
- numfkdelsetcols, fkdelsetcols,
+ numfkdelsetcols, fkdelsetcols, false,
with_period);
/* call ourselves to finalize the creation and we're done */
@@ -10853,10 +10862,11 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
constrForm->confrelid, constrForm->conrelid,
&deleteTriggerOid, &updateTriggerOid);
- address = addFkConstraint(fkconstraint, fkRel, partitionRel, partIndexId,
- constrOid, FK_REFERENCED_SIDE, numfks,
- mapped_confkey, conkey, conpfeqop, conppeqop,
- conffeqop, numfkdelsetcols, confdelsetcols,
+ address = addFkConstraint(fkconstraint->conname, fkconstraint, fkRel,
+ partitionRel, partIndexId, constrOid,
+ FK_REFERENCED_SIDE, numfks, mapped_confkey,
+ conkey, conpfeqop, conppeqop, conffeqop,
+ numfkdelsetcols, confdelsetcols, false,
constrForm->conperiod);
addFkRecurseReferenced(NULL,
fkconstraint,
@@ -10979,9 +10989,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
Constraint *fkconstraint;
bool attached;
Oid indexOid;
- Oid constrOid;
- ObjectAddress address,
- referenced;
+ ObjectAddress address;
ListCell *lc;
Oid insertTriggerOid,
updateTriggerOid;
@@ -11079,7 +11087,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
fkconstraint->old_conpfeqop = NIL;
fkconstraint->old_pktable_oid = InvalidOid;
fkconstraint->skip_validation = false;
- fkconstraint->initially_valid = true;
+ fkconstraint->initially_valid = constrForm->convalidated;
for (int i = 0; i < numfks; i++)
{
Form_pg_attribute att;
@@ -11089,73 +11097,27 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
fkconstraint->fk_attrs = lappend(fkconstraint->fk_attrs,
makeString(NameStr(att->attname)));
}
- if (ConstraintNameIsUsed(CONSTRAINT_RELATION,
- RelationGetRelid(partRel),
- NameStr(constrForm->conname)))
- fkconstraint->conname =
- ChooseConstraintName(RelationGetRelationName(partRel),
- ChooseForeignKeyConstraintNameAddition(fkconstraint->fk_attrs),
- "fkey",
- RelationGetNamespace(partRel), NIL);
- else
- fkconstraint->conname = pstrdup(NameStr(constrForm->conname));
indexOid = constrForm->conindid;
with_period = constrForm->conperiod;
- constrOid =
- CreateConstraintEntry(fkconstraint->conname,
- constrForm->connamespace,
- CONSTRAINT_FOREIGN,
- fkconstraint->deferrable,
- fkconstraint->initdeferred,
- constrForm->convalidated,
- parentConstrOid,
- RelationGetRelid(partRel),
- mapped_conkey,
- numfks,
- numfks,
- InvalidOid, /* not a domain constraint */
- indexOid,
- constrForm->confrelid, /* same foreign rel */
- confkey,
- conpfeqop,
- conppeqop,
- conffeqop,
- numfks,
- fkconstraint->fk_upd_action,
- fkconstraint->fk_del_action,
- confdelsetcols,
- numfkdelsetcols,
- fkconstraint->fk_matchtype,
- NULL,
- NULL,
- NULL,
- false, /* islocal */
- 1, /* inhcount */
- false, /* conNoInherit */
- with_period, /* conPeriod */
- true);
-
- /* Set up partition dependencies for the new constraint */
- ObjectAddressSet(address, ConstraintRelationId, constrOid);
- ObjectAddressSet(referenced, ConstraintRelationId, parentConstrOid);
- recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_PRI);
- ObjectAddressSet(referenced, RelationRelationId,
- RelationGetRelid(partRel));
- recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_SEC);
+
+ address = addFkConstraint(NameStr(constrForm->conname), fkconstraint,
+ partRel, pkrel, indexOid, parentConstrOid,
+ FK_REFERENCING_SIDE, numfks, confkey,
+ mapped_conkey, conpfeqop,
+ conppeqop, conffeqop,
+ numfkdelsetcols, confdelsetcols,
+ true, with_period);
/* Done with the cloned constraint's tuple */
ReleaseSysCache(tuple);
- /* Make all this visible before recursing */
- CommandCounterIncrement();
-
addFkRecurseReferencing(wqueue,
fkconstraint,
partRel,
pkrel,
indexOid,
- constrOid,
+ address.objectId,
numfks,
confkey,
mapped_conkey,
--
2.46.0
On Wed, 25 Sep 2024 14:42:40 +0200
Jehan-Guillaume de Rorthais <jgdr@dalibo.com> wrote:
On Thu, 5 Sep 2024 00:57:28 +0200
Jehan-Guillaume de Rorthais <jgdr@dalibo.com> wrote:
[…]
Please, find in attachment a patch implementing this idea.
Please, find in attachment a set of patch based on the previous one.
Please, find in attachment the same set of patch for REL_17_STABLE.
Regards,
Attachments:
v3-17-0001-Add-tests-about-FK-between-partitionned-tables.patchtext/x-patchDownload
From 976fa1abff9da882ddf63636e3cb3ceec105dc57 Mon Sep 17 00:00:00 2001
From: Jehan-Guillaume de Rorthais <jgdr@dalibo.com>
Date: Mon, 23 Sep 2024 18:50:20 +0200
Subject: [PATCH v3-17 1/4] Add tests about FK between partitionned tables
These tests cover two reported bugs related to FK
between two partitionned tables.
co-author : Alvaro Herrera <alvherre@alvh.no-ip.org>
co-author : Jehan-Guillaume de Rorthais <jgdr@dalibo.com>
co-author : Tender Wang <tndrwang@gmail.com>
Discussion: https://postgr.es/m/20230420144344.40744130@karst
Discussion: https://postgr.es/m/20230705233028.2f554f73@karst
---
src/test/regress/expected/foreign_key.out | 266 ++++++++++++++++++++++
src/test/regress/sql/foreign_key.sql | 101 ++++++++
2 files changed, 367 insertions(+)
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 8c04a24b37..da67bbeb4f 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -2934,3 +2934,269 @@ DETAIL: drop cascades to table fkpart11.pk
drop cascades to table fkpart11.fk_parted
drop cascades to table fkpart11.fk_another
drop cascades to function fkpart11.print_row()
+-- When a table is attached as partition to a partitioned table that has
+-- a foreign key to another partitioned table, it acquires a clone of the
+-- FK. Upon detach, this clone is not removed, but instead becomes an
+-- independent FK. If it then attaches to the partitioned table again,
+-- the FK from the parent "takes over" ownership of the independent FK rather
+-- than creating a separate one.
+CREATE SCHEMA fkpart12
+ CREATE TABLE fk_p ( id int, jd int, PRIMARY KEY(id, jd)) PARTITION BY list (id)
+ CREATE TABLE fk_p_1 PARTITION OF fk_p FOR VALUES IN (1) PARTITION BY list (jd)
+ CREATE TABLE fk_p_1_1 PARTITION OF fk_p_1 FOR VALUES IN (1)
+ CREATE TABLE fk_p_1_2 PARTITION OF fk_p_1 FOR VALUES IN (2)
+ CREATE TABLE fk_p_2 PARTITION OF fk_p FOR VALUES IN (2) PARTITION BY list (jd)
+ CREATE TABLE fk_p_2_1 PARTITION OF fk_p_2 FOR VALUES IN (1)
+ CREATE TABLE fk_p_2_2 PARTITION OF fk_p_2 FOR VALUES IN (2)
+ CREATE TABLE fk_r_1 ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL)
+ CREATE TABLE fk_r_2 ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL)
+ CREATE TABLE fk_r ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL,
+ FOREIGN KEY (p_id, p_jd) REFERENCES fk_p (id, jd)
+ ) PARTITION BY list (id);
+SET search_path TO fkpart12;
+CREATE OR REPLACE FUNCTION
+fkpart12_constraints(OUT conname name, OUT conrel regclass,
+ OUT confrelid regclass, OUT conparent name) RETURNS SETOF record AS $$
+ WITH RECURSIVE r AS (
+ SELECT oid, conname, conrelid, confrelid, NULL::name AS conparent
+ FROM pg_constraint
+ WHERE conrelid = 'fk_r'::regclass
+ AND contype = 'f'
+ AND conparentid = 0
+ UNION ALL
+ SELECT c.oid, c.conname, c.conrelid, c.confrelid, r.conname
+ FROM pg_constraint c
+ JOIN r ON c.conparentid = r.oid
+ )
+ SELECT conname, conrelid::regclass, confrelid::regclass, conparent
+ FROM r
+ ORDER BY oid, conname, confrelid, conparent;
+$$
+LANGUAGE SQL;
+CREATE OR REPLACE FUNCTION
+fkpart12_triggers(OUT conname name, OUT conparent name,
+ OUT tgfoid regproc, OUT tgfparent regproc) RETURNS SETOF record AS $$
+
+ WITH RECURSIVE r AS (
+ SELECT t.oid, t.tgfoid::regproc, c.conname, NULL::name AS conparent, NULL::regproc AS tgfparent
+ FROM pg_trigger t
+ JOIN pg_constraint c ON c.oid = t.tgconstraint
+ WHERE c.conrelid = 'fk_r'::regclass
+ AND c.contype = 'f'
+ AND t.tgparentid = 0
+ UNION ALL
+ SELECT t2.oid, t2.tgfoid::regproc, c2.conname, c3.conname, r.tgfoid::regproc AS tgfparent
+ FROM pg_trigger t2
+ JOIN pg_constraint c2 ON c2.oid = t2.tgconstraint
+ LEFT JOIN pg_constraint c3 ON c3.oid = c2.conparentid
+ JOIN r ON r.oid = t2.tgparentid
+ )
+ SELECT conname, conparent, tgfoid, tgfparent
+ FROM r
+ ORDER BY conname, conparent, tgfoid, tgfparent;
+$$
+LANGUAGE SQL;
+SELECT * FROM fkpart12_constraints();
+ conname | conrel | confrelid | conparent
+----------------------+--------+-----------+----------------------
+ fk_r_p_id_p_jd_fkey | fk_r | fk_p |
+ fk_r_p_id_p_jd_fkey1 | fk_r | fk_p_1 | fk_r_p_id_p_jd_fkey
+ fk_r_p_id_p_jd_fkey2 | fk_r | fk_p_1_1 | fk_r_p_id_p_jd_fkey1
+ fk_r_p_id_p_jd_fkey3 | fk_r | fk_p_1_2 | fk_r_p_id_p_jd_fkey1
+ fk_r_p_id_p_jd_fkey4 | fk_r | fk_p_2 | fk_r_p_id_p_jd_fkey
+ fk_r_p_id_p_jd_fkey5 | fk_r | fk_p_2_1 | fk_r_p_id_p_jd_fkey4
+ fk_r_p_id_p_jd_fkey6 | fk_r | fk_p_2_2 | fk_r_p_id_p_jd_fkey4
+(7 rows)
+
+SELECT * FROM fkpart12_triggers();
+ conname | conparent | tgfoid | tgfparent
+----------------------+----------------------+------------------------+------------------------
+ fk_r_p_id_p_jd_fkey | | "RI_FKey_check_ins" |
+ fk_r_p_id_p_jd_fkey | | "RI_FKey_check_upd" |
+ fk_r_p_id_p_jd_fkey | | "RI_FKey_noaction_del" |
+ fk_r_p_id_p_jd_fkey | | "RI_FKey_noaction_upd" |
+ fk_r_p_id_p_jd_fkey1 | fk_r_p_id_p_jd_fkey | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey1 | fk_r_p_id_p_jd_fkey | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey2 | fk_r_p_id_p_jd_fkey1 | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey2 | fk_r_p_id_p_jd_fkey1 | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey3 | fk_r_p_id_p_jd_fkey1 | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey3 | fk_r_p_id_p_jd_fkey1 | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey4 | fk_r_p_id_p_jd_fkey | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey4 | fk_r_p_id_p_jd_fkey | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey5 | fk_r_p_id_p_jd_fkey4 | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey5 | fk_r_p_id_p_jd_fkey4 | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey6 | fk_r_p_id_p_jd_fkey4 | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey6 | fk_r_p_id_p_jd_fkey4 | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+(16 rows)
+
+INSERT INTO fk_p VALUES (1, 1);
+ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1);
+ALTER TABLE fk_r ATTACH PARTITION fk_r_2 FOR VALUES IN (2);
+\d fk_r_1
+ Table "fkpart12.fk_r_1"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ id | integer | | not null |
+ p_id | integer | | not null |
+ p_jd | integer | | not null |
+Partition of: fk_r FOR VALUES IN (1)
+Indexes:
+ "fk_r_1_pkey" PRIMARY KEY, btree (id)
+Foreign-key constraints:
+ TABLE "fk_r" CONSTRAINT "fk_r_p_id_p_jd_fkey" FOREIGN KEY (p_id, p_jd) REFERENCES fk_p(id, jd)
+
+SELECT * FROM fkpart12_constraints();
+ conname | conrel | confrelid | conparent
+----------------------+--------+-----------+----------------------
+ fk_r_p_id_p_jd_fkey | fk_r | fk_p |
+ fk_r_p_id_p_jd_fkey1 | fk_r | fk_p_1 | fk_r_p_id_p_jd_fkey
+ fk_r_p_id_p_jd_fkey2 | fk_r | fk_p_1_1 | fk_r_p_id_p_jd_fkey1
+ fk_r_p_id_p_jd_fkey3 | fk_r | fk_p_1_2 | fk_r_p_id_p_jd_fkey1
+ fk_r_p_id_p_jd_fkey4 | fk_r | fk_p_2 | fk_r_p_id_p_jd_fkey
+ fk_r_p_id_p_jd_fkey5 | fk_r | fk_p_2_1 | fk_r_p_id_p_jd_fkey4
+ fk_r_p_id_p_jd_fkey6 | fk_r | fk_p_2_2 | fk_r_p_id_p_jd_fkey4
+ fk_r_p_id_p_jd_fkey | fk_r_1 | fk_p | fk_r_p_id_p_jd_fkey
+ fk_r_p_id_p_jd_fkey | fk_r_2 | fk_p | fk_r_p_id_p_jd_fkey
+(9 rows)
+
+SELECT * FROM fkpart12_triggers();
+ conname | conparent | tgfoid | tgfparent
+----------------------+----------------------+------------------------+------------------------
+ fk_r_p_id_p_jd_fkey | fk_r_p_id_p_jd_fkey | "RI_FKey_check_ins" | "RI_FKey_check_ins"
+ fk_r_p_id_p_jd_fkey | fk_r_p_id_p_jd_fkey | "RI_FKey_check_ins" | "RI_FKey_check_ins"
+ fk_r_p_id_p_jd_fkey | fk_r_p_id_p_jd_fkey | "RI_FKey_check_upd" | "RI_FKey_check_upd"
+ fk_r_p_id_p_jd_fkey | fk_r_p_id_p_jd_fkey | "RI_FKey_check_upd" | "RI_FKey_check_upd"
+ fk_r_p_id_p_jd_fkey | | "RI_FKey_check_ins" |
+ fk_r_p_id_p_jd_fkey | | "RI_FKey_check_upd" |
+ fk_r_p_id_p_jd_fkey | | "RI_FKey_noaction_del" |
+ fk_r_p_id_p_jd_fkey | | "RI_FKey_noaction_upd" |
+ fk_r_p_id_p_jd_fkey1 | fk_r_p_id_p_jd_fkey | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey1 | fk_r_p_id_p_jd_fkey | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey2 | fk_r_p_id_p_jd_fkey1 | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey2 | fk_r_p_id_p_jd_fkey1 | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey3 | fk_r_p_id_p_jd_fkey1 | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey3 | fk_r_p_id_p_jd_fkey1 | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey4 | fk_r_p_id_p_jd_fkey | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey4 | fk_r_p_id_p_jd_fkey | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey5 | fk_r_p_id_p_jd_fkey4 | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey5 | fk_r_p_id_p_jd_fkey4 | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey6 | fk_r_p_id_p_jd_fkey4 | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey6 | fk_r_p_id_p_jd_fkey4 | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+(20 rows)
+
+INSERT INTO fk_r VALUES (1, 1, 1);
+ALTER TABLE fk_r DETACH PARTITION fk_r_1;
+\d fk_r_1
+ Table "fkpart12.fk_r_1"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ id | integer | | not null |
+ p_id | integer | | not null |
+ p_jd | integer | | not null |
+Indexes:
+ "fk_r_1_pkey" PRIMARY KEY, btree (id)
+Foreign-key constraints:
+ "fk_r_p_id_p_jd_fkey" FOREIGN KEY (p_id, p_jd) REFERENCES fk_p(id, jd)
+
+SELECT * FROM fkpart12_constraints();
+ conname | conrel | confrelid | conparent
+----------------------+--------+-----------+----------------------
+ fk_r_p_id_p_jd_fkey | fk_r | fk_p |
+ fk_r_p_id_p_jd_fkey1 | fk_r | fk_p_1 | fk_r_p_id_p_jd_fkey
+ fk_r_p_id_p_jd_fkey2 | fk_r | fk_p_1_1 | fk_r_p_id_p_jd_fkey1
+ fk_r_p_id_p_jd_fkey3 | fk_r | fk_p_1_2 | fk_r_p_id_p_jd_fkey1
+ fk_r_p_id_p_jd_fkey4 | fk_r | fk_p_2 | fk_r_p_id_p_jd_fkey
+ fk_r_p_id_p_jd_fkey5 | fk_r | fk_p_2_1 | fk_r_p_id_p_jd_fkey4
+ fk_r_p_id_p_jd_fkey6 | fk_r | fk_p_2_2 | fk_r_p_id_p_jd_fkey4
+ fk_r_p_id_p_jd_fkey | fk_r_2 | fk_p | fk_r_p_id_p_jd_fkey
+(8 rows)
+
+SELECT * FROM fkpart12_triggers();
+ conname | conparent | tgfoid | tgfparent
+----------------------+----------------------+------------------------+------------------------
+ fk_r_p_id_p_jd_fkey | fk_r_p_id_p_jd_fkey | "RI_FKey_check_ins" | "RI_FKey_check_ins"
+ fk_r_p_id_p_jd_fkey | fk_r_p_id_p_jd_fkey | "RI_FKey_check_upd" | "RI_FKey_check_upd"
+ fk_r_p_id_p_jd_fkey | | "RI_FKey_check_ins" |
+ fk_r_p_id_p_jd_fkey | | "RI_FKey_check_upd" |
+ fk_r_p_id_p_jd_fkey | | "RI_FKey_noaction_del" |
+ fk_r_p_id_p_jd_fkey | | "RI_FKey_noaction_upd" |
+ fk_r_p_id_p_jd_fkey1 | fk_r_p_id_p_jd_fkey | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey1 | fk_r_p_id_p_jd_fkey | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey2 | fk_r_p_id_p_jd_fkey1 | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey2 | fk_r_p_id_p_jd_fkey1 | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey3 | fk_r_p_id_p_jd_fkey1 | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey3 | fk_r_p_id_p_jd_fkey1 | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey4 | fk_r_p_id_p_jd_fkey | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey4 | fk_r_p_id_p_jd_fkey | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey5 | fk_r_p_id_p_jd_fkey4 | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey5 | fk_r_p_id_p_jd_fkey4 | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey6 | fk_r_p_id_p_jd_fkey4 | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey6 | fk_r_p_id_p_jd_fkey4 | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+(18 rows)
+
+INSERT INTO fk_r_1 VALUES (2, 1, 2); -- fails as EXPECTED
+ERROR: insert or update on table "fk_r_1" violates foreign key constraint "fk_r_p_id_p_jd_fkey"
+DETAIL: Key (p_id, p_jd)=(1, 2) is not present in table "fk_p".
+DELETE FROM fk_p; -- should fail but was buggy
+ERROR: update or delete on table "fk_p_1_1" violates foreign key constraint "fk_r_1_p_id_p_jd_fkey1" on table "fk_r_1"
+DETAIL: Key (id, jd)=(1, 1) is still referenced from table "fk_r_1".
+ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1);
+\d fk_r_1
+ Table "fkpart12.fk_r_1"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ id | integer | | not null |
+ p_id | integer | | not null |
+ p_jd | integer | | not null |
+Partition of: fk_r FOR VALUES IN (1)
+Indexes:
+ "fk_r_1_pkey" PRIMARY KEY, btree (id)
+Foreign-key constraints:
+ TABLE "fk_r" CONSTRAINT "fk_r_p_id_p_jd_fkey" FOREIGN KEY (p_id, p_jd) REFERENCES fk_p(id, jd)
+
+SELECT * FROM fkpart12_constraints();
+ conname | conrel | confrelid | conparent
+----------------------+--------+-----------+----------------------
+ fk_r_p_id_p_jd_fkey | fk_r | fk_p |
+ fk_r_p_id_p_jd_fkey1 | fk_r | fk_p_1 | fk_r_p_id_p_jd_fkey
+ fk_r_p_id_p_jd_fkey2 | fk_r | fk_p_1_1 | fk_r_p_id_p_jd_fkey1
+ fk_r_p_id_p_jd_fkey3 | fk_r | fk_p_1_2 | fk_r_p_id_p_jd_fkey1
+ fk_r_p_id_p_jd_fkey4 | fk_r | fk_p_2 | fk_r_p_id_p_jd_fkey
+ fk_r_p_id_p_jd_fkey5 | fk_r | fk_p_2_1 | fk_r_p_id_p_jd_fkey4
+ fk_r_p_id_p_jd_fkey6 | fk_r | fk_p_2_2 | fk_r_p_id_p_jd_fkey4
+ fk_r_p_id_p_jd_fkey | fk_r_1 | fk_p | fk_r_p_id_p_jd_fkey
+ fk_r_p_id_p_jd_fkey | fk_r_2 | fk_p | fk_r_p_id_p_jd_fkey
+(9 rows)
+
+SELECT * FROM fkpart12_triggers();
+ conname | conparent | tgfoid | tgfparent
+----------------------+----------------------+------------------------+------------------------
+ fk_r_p_id_p_jd_fkey | fk_r_p_id_p_jd_fkey | "RI_FKey_check_ins" | "RI_FKey_check_ins"
+ fk_r_p_id_p_jd_fkey | fk_r_p_id_p_jd_fkey | "RI_FKey_check_ins" | "RI_FKey_check_ins"
+ fk_r_p_id_p_jd_fkey | fk_r_p_id_p_jd_fkey | "RI_FKey_check_upd" | "RI_FKey_check_upd"
+ fk_r_p_id_p_jd_fkey | fk_r_p_id_p_jd_fkey | "RI_FKey_check_upd" | "RI_FKey_check_upd"
+ fk_r_p_id_p_jd_fkey | | "RI_FKey_check_ins" |
+ fk_r_p_id_p_jd_fkey | | "RI_FKey_check_upd" |
+ fk_r_p_id_p_jd_fkey | | "RI_FKey_noaction_del" |
+ fk_r_p_id_p_jd_fkey | | "RI_FKey_noaction_upd" |
+ fk_r_p_id_p_jd_fkey1 | fk_r_p_id_p_jd_fkey | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey1 | fk_r_p_id_p_jd_fkey | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey2 | fk_r_p_id_p_jd_fkey1 | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey2 | fk_r_p_id_p_jd_fkey1 | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey3 | fk_r_p_id_p_jd_fkey1 | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey3 | fk_r_p_id_p_jd_fkey1 | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey4 | fk_r_p_id_p_jd_fkey | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey4 | fk_r_p_id_p_jd_fkey | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey5 | fk_r_p_id_p_jd_fkey4 | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey5 | fk_r_p_id_p_jd_fkey4 | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey6 | fk_r_p_id_p_jd_fkey4 | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey6 | fk_r_p_id_p_jd_fkey4 | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+(20 rows)
+
+DELETE FROM fk_p; -- should fail but was buggy
+ERROR: update or delete on table "fk_p_1_1" violates foreign key constraint "fk_r_p_id_p_jd_fkey2" on table "fk_r"
+DETAIL: Key (id, jd)=(1, 1) is still referenced from table "fk_r".
+SET client_min_messages TO warning;
+DROP SCHEMA fkpart12 CASCADE;
+RESET client_min_messages;
+RESET search_path;
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index d1aac5357f..e96ab5c703 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -2086,3 +2086,104 @@ UPDATE fkpart11.pk SET a = 3 WHERE a = 4;
UPDATE fkpart11.pk SET a = 1 WHERE a = 2;
DROP SCHEMA fkpart11 CASCADE;
+
+-- When a table is attached as partition to a partitioned table that has
+-- a foreign key to another partitioned table, it acquires a clone of the
+-- FK. Upon detach, this clone is not removed, but instead becomes an
+-- independent FK. If it then attaches to the partitioned table again,
+-- the FK from the parent "takes over" ownership of the independent FK rather
+-- than creating a separate one.
+CREATE SCHEMA fkpart12
+ CREATE TABLE fk_p ( id int, jd int, PRIMARY KEY(id, jd)) PARTITION BY list (id)
+ CREATE TABLE fk_p_1 PARTITION OF fk_p FOR VALUES IN (1) PARTITION BY list (jd)
+ CREATE TABLE fk_p_1_1 PARTITION OF fk_p_1 FOR VALUES IN (1)
+ CREATE TABLE fk_p_1_2 PARTITION OF fk_p_1 FOR VALUES IN (2)
+ CREATE TABLE fk_p_2 PARTITION OF fk_p FOR VALUES IN (2) PARTITION BY list (jd)
+ CREATE TABLE fk_p_2_1 PARTITION OF fk_p_2 FOR VALUES IN (1)
+ CREATE TABLE fk_p_2_2 PARTITION OF fk_p_2 FOR VALUES IN (2)
+ CREATE TABLE fk_r_1 ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL)
+ CREATE TABLE fk_r_2 ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL)
+ CREATE TABLE fk_r ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL,
+ FOREIGN KEY (p_id, p_jd) REFERENCES fk_p (id, jd)
+ ) PARTITION BY list (id);
+SET search_path TO fkpart12;
+
+CREATE OR REPLACE FUNCTION
+fkpart12_constraints(OUT conname name, OUT conrel regclass,
+ OUT confrelid regclass, OUT conparent name) RETURNS SETOF record AS $$
+ WITH RECURSIVE r AS (
+ SELECT oid, conname, conrelid, confrelid, NULL::name AS conparent
+ FROM pg_constraint
+ WHERE conrelid = 'fk_r'::regclass
+ AND contype = 'f'
+ AND conparentid = 0
+ UNION ALL
+ SELECT c.oid, c.conname, c.conrelid, c.confrelid, r.conname
+ FROM pg_constraint c
+ JOIN r ON c.conparentid = r.oid
+ )
+ SELECT conname, conrelid::regclass, confrelid::regclass, conparent
+ FROM r
+ ORDER BY oid, conname, confrelid, conparent;
+$$
+LANGUAGE SQL;
+
+CREATE OR REPLACE FUNCTION
+fkpart12_triggers(OUT conname name, OUT conparent name,
+ OUT tgfoid regproc, OUT tgfparent regproc) RETURNS SETOF record AS $$
+
+ WITH RECURSIVE r AS (
+ SELECT t.oid, t.tgfoid::regproc, c.conname, NULL::name AS conparent, NULL::regproc AS tgfparent
+ FROM pg_trigger t
+ JOIN pg_constraint c ON c.oid = t.tgconstraint
+ WHERE c.conrelid = 'fk_r'::regclass
+ AND c.contype = 'f'
+ AND t.tgparentid = 0
+ UNION ALL
+ SELECT t2.oid, t2.tgfoid::regproc, c2.conname, c3.conname, r.tgfoid::regproc AS tgfparent
+ FROM pg_trigger t2
+ JOIN pg_constraint c2 ON c2.oid = t2.tgconstraint
+ LEFT JOIN pg_constraint c3 ON c3.oid = c2.conparentid
+ JOIN r ON r.oid = t2.tgparentid
+ )
+ SELECT conname, conparent, tgfoid, tgfparent
+ FROM r
+ ORDER BY conname, conparent, tgfoid, tgfparent;
+$$
+LANGUAGE SQL;
+
+SELECT * FROM fkpart12_constraints();
+SELECT * FROM fkpart12_triggers();
+
+INSERT INTO fk_p VALUES (1, 1);
+
+ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1);
+ALTER TABLE fk_r ATTACH PARTITION fk_r_2 FOR VALUES IN (2);
+
+\d fk_r_1
+SELECT * FROM fkpart12_constraints();
+SELECT * FROM fkpart12_triggers();
+
+INSERT INTO fk_r VALUES (1, 1, 1);
+
+ALTER TABLE fk_r DETACH PARTITION fk_r_1;
+
+\d fk_r_1
+SELECT * FROM fkpart12_constraints();
+SELECT * FROM fkpart12_triggers();
+
+INSERT INTO fk_r_1 VALUES (2, 1, 2); -- fails as EXPECTED
+DELETE FROM fk_p; -- should fail but was buggy
+
+ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1);
+
+\d fk_r_1
+SELECT * FROM fkpart12_constraints();
+SELECT * FROM fkpart12_triggers();
+
+DELETE FROM fk_p; -- should fail but was buggy
+
+SET client_min_messages TO warning;
+DROP SCHEMA fkpart12 CASCADE;
+RESET client_min_messages;
+RESET search_path;
--
2.46.0
v3-17-0002-Rework-foreign-key-mangling-during-ATTACH-DETA.patchtext/x-patchDownload
From 1d09803a3d1f0240b205ea6c05669f6f70638deb Mon Sep 17 00:00:00 2001
From: Jehan-Guillaume de Rorthais <jgdr@dalibo.com>
Date: Wed, 25 Sep 2024 15:38:42 +0200
Subject: [PATCH v3-17 2/4] Rework foreign key mangling during ATTACH/DETACH
... when the reference table is partitioned.
It turns out that the catalog representation we chose for foreign keys
connecting partitioned tables is suboptimal, particularly in the sense
that a standalone table has a different way to represent it when
referencing a partitioned table, than when the same table becomes a
partition (and vice versa). This difference means we need to spawn
additional catalog rows on detach, and remove them on attach.
As a very obvious symptom, we were missing action triggers after
detach, which means that you could update/delete rows from the
referenced partitioned table that still had referencing rows at the
other side previously detached, and fail to throw the required errors.
This means existing FKs might have rows that break relational
integrity.
To fix this bug, this patch rework addFkRecurseReferenced() so it
can be called from DetachPartitionFinalize(), so we can keep this
logic in a single function. The addFkRecurseReferenced() rework
leads to create addFkConstraint(), but it seems cleaner as
addFkRecurseReferenced() and addFkRecurseReferencing() have the same
logic now. Some other code factoring using addFkConstraint() are
added in next commits.
Another possible problem is that trying to reattach a table
that had been detached would fail indicating that internal triggers
cannot be found, which from the user's point of view is nonsensical.
We might want to rethink the representation in the future to avoid this
messiness, but the code now seems to do what's required to make the
constraints operate correctly.
co-author: Alvaro Herrera <alvherre@alvh.no-ip.org>
co-author: Jehan-Guillaume de Rorthais <jgdr@dalibo.com>
co-author: Tender Wang <tndrwang@gmail.com>
Reported-by: Guillaume Lelarge <guillaume@lelarge.info>
Reported-by: Jehan-Guillaume de Rorthais <jgdr@dalibo.com>
Reported-by: Thomas Baehler (SBB CFF FFS) <thomas.baehler2@sbb.ch>
Discussion: https://postgr.es/m/20230420144344.40744130@karst
Discussion: https://postgr.es/m/20230705233028.2f554f73@karst
Discussion: https://postgr.es/m/GVAP278MB02787E7134FD691861635A8BC9032@GVAP278MB0278.CHEP278.PROD.OUTLOOK.COM
Discussion: https://postgr.es/m/18541-628a61bc267cd2d3@postgresql.org
---
src/backend/commands/tablecmds.c | 370 ++++++++++++++++++++++++-------
1 file changed, 289 insertions(+), 81 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 9432fd6587..83e0a0652e 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -504,7 +504,13 @@ static ObjectAddress ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *
Relation rel, Constraint *fkconstraint,
bool recurse, bool recursing,
LOCKMODE lockmode);
-static ObjectAddress addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint,
+static ObjectAddress addFkConstraint(Constraint *fkconstraint, Relation rel,
+ Relation pkrel, Oid indexOid, Oid parentConstr,
+ int numfks, int16 *pkattnum, int16 *fkattnum,
+ Oid *pfeqoperators, Oid *ppeqoperators,
+ Oid *ffeqoperators, int numfkdelsetcols,
+ int16 *fkdelsetcols);
+static void addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint,
Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks, int16 *pkattnum, int16 *fkattnum,
Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
@@ -644,9 +650,11 @@ static void DropClonedTriggersFromPartition(Oid partitionId);
static ObjectAddress ATExecDetachPartition(List **wqueue, AlteredTableInfo *tab,
Relation rel, RangeVar *name,
bool concurrent);
-static void DetachPartitionFinalize(Relation rel, Relation partRel,
- bool concurrent, Oid defaultPartOid);
-static ObjectAddress ATExecDetachPartitionFinalize(Relation rel, RangeVar *name);
+static void DetachPartitionFinalize(List **wqueue, Relation rel,
+ Relation partRel, bool concurrent,
+ Oid defaultPartOid);
+static ObjectAddress ATExecDetachPartitionFinalize(List **wqueue, Relation rel,
+ RangeVar *name);
static ObjectAddress ATExecAttachPartitionIdx(List **wqueue, Relation parentIdx,
RangeVar *name);
static void validatePartitionedIndex(Relation partedIdx, Relation partedTbl);
@@ -5512,7 +5520,7 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
((PartitionCmd *) cmd->def)->concurrent);
break;
case AT_DetachPartitionFinalize:
- address = ATExecDetachPartitionFinalize(rel, ((PartitionCmd *) cmd->def)->name);
+ address = ATExecDetachPartitionFinalize(wqueue, rel, ((PartitionCmd *) cmd->def)->name);
break;
default: /* oops */
elog(ERROR, "unrecognized alter table type: %d",
@@ -9935,9 +9943,21 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
* Create all the constraint and trigger objects, recursing to partitions
* as necessary. First handle the referenced side.
*/
- address = addFkRecurseReferenced(wqueue, fkconstraint, rel, pkrel,
+ address = addFkConstraint(fkconstraint, rel, pkrel,
+ indexOid,
+ InvalidOid, /* no parent constraint */
+ numfks,
+ pkattnum,
+ fkattnum,
+ pfeqoperators,
+ ppeqoperators,
+ ffeqoperators,
+ numfkdelsetcols,
+ fkdelsetcols);
+
+ addFkRecurseReferenced(wqueue, fkconstraint, rel, pkrel,
indexOid,
- InvalidOid, /* no parent constraint */
+ address.objectId,
numfks,
pkattnum,
fkattnum,
@@ -10009,46 +10029,31 @@ validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
}
/*
- * addFkRecurseReferenced
- * subroutine for ATAddForeignKeyConstraint; recurses on the referenced
- * side of the constraint
+ * addFkConstraint
+ * Add a new Foreign Key constraint.
*
- * Create pg_constraint rows for the referenced side of the constraint,
- * referencing the parent of the referencing side; also create action triggers
- * on leaf partitions. If the table is partitioned, recurse to handle each
- * partition.
+ * Creates a pg_constraint rows referencing a parent if given.
*
- * wqueue is the ALTER TABLE work queue; can be NULL when not running as part
- * of an ALTER TABLE sequence.
- * fkconstraint is the constraint being added.
- * rel is the root referencing relation.
- * pkrel is the referenced relation; might be a partition, if recursing.
- * indexOid is the OID of the index (on pkrel) implementing this constraint.
- * parentConstr is the OID of a parent constraint; InvalidOid if this is a
- * top-level constraint.
- * numfks is the number of columns in the foreign key
- * pkattnum is the attnum array of referenced attributes.
- * fkattnum is the attnum array of referencing attributes.
- * numfkdelsetcols is the number of columns in the ON DELETE SET NULL/DEFAULT
- * (...) clause
- * fkdelsetcols is the attnum array of the columns in the ON DELETE SET
- * NULL/DEFAULT clause
- * pf/pp/ffeqoperators are OID array of operators between columns.
- * old_check_ok signals that this constraint replaces an existing one that
- * was already validated (thus this one doesn't need validation).
- * parentDelTrigger and parentUpdTrigger, when being recursively called on
- * a partition, are the OIDs of the parent action triggers for DELETE and
- * UPDATE respectively.
+ * * fkconstraint is the constraint being added.
+ * * rel is the root referencing relation.
+ * * pkrel is the referenced relation; might be a partition, if recursing.
+ * * indexOid is the OID of the index (on pkrel) implementing this constraint.
+ * * parentConstr is the OID of a parent constraint; InvalidOid if this is a
+ * top-level constraint.
+ * * numfks is the number of columns in the foreign key.
+ * * pkattnum is the attnum array of referenced attributes.
+ * * fkattnum is the attnum array of referencing attributes.
+ * * pf/pp/ffeqoperators are OID array of operators between columns.
+ * * numfkdelsetcols is the number of columns in the ON DELETE SET NULL/DEFAULT
+ * (...) clause.
+ * * fkdelsetcols is the attnum array of the columns in the ON DELETE SET
+ * NULL/DEFAULT clause.
*/
static ObjectAddress
-addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
- Relation pkrel, Oid indexOid, Oid parentConstr,
- int numfks,
- int16 *pkattnum, int16 *fkattnum, Oid *pfeqoperators,
- Oid *ppeqoperators, Oid *ffeqoperators,
- int numfkdelsetcols, int16 *fkdelsetcols,
- bool old_check_ok,
- Oid parentDelTrigger, Oid parentUpdTrigger)
+addFkConstraint(Constraint *fkconstraint, Relation rel, Relation pkrel,
+ Oid indexOid, Oid parentConstr, int numfks, int16 *pkattnum,
+ int16 *fkattnum, Oid *pfeqoperators, Oid *ppeqoperators,
+ Oid *ffeqoperators, int numfkdelsetcols, int16 *fkdelsetcols)
{
ObjectAddress address;
Oid constrOid;
@@ -10056,8 +10061,6 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
bool conislocal;
int coninhcount;
bool connoinherit;
- Oid deleteTriggerOid,
- updateTriggerOid;
/*
* Verify relkind for each referenced partition. At the top level, this
@@ -10155,12 +10158,61 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
/* make new constraint visible, in case we add more */
CommandCounterIncrement();
+ return address;
+}
+
+/*
+ * addFkRecurseReferenced
+ * subroutine for ATAddForeignKeyConstraint; recurses on the referenced
+ * side of the constraint
+ *
+ * If the referenced relation is a plain relation, create the necessary check
+ * triggers that implement the constraint. If the referenced relation is a
+ * partitioned table, then we create a pg_constraint row referencing the parent
+ * of the referencing side for it and recurse on this routine for each
+ * partition.
+ *
+ * wqueue is the ALTER TABLE work queue; can be NULL when not running as part
+ * of an ALTER TABLE sequence.
+ * fkconstraint is the constraint being added.
+ * rel is the root referencing relation.
+ * pkrel is the referenced relation; might be a partition, if recursing.
+ * indexOid is the OID of the index (on pkrel) implementing this constraint.
+ * parentConstr is the OID of a parent constraint; InvalidOid if this is a
+ * top-level constraint.
+ * numfks is the number of columns in the foreign key
+ * pkattnum is the attnum array of referenced attributes.
+ * fkattnum is the attnum array of referencing attributes.
+ * numfkdelsetcols is the number of columns in the ON DELETE SET NULL/DEFAULT
+ * (...) clause
+ * fkdelsetcols is the attnum array of the columns in the ON DELETE SET
+ * NULL/DEFAULT clause
+ * pf/pp/ffeqoperators are OID array of operators between columns.
+ * old_check_ok signals that this constraint replaces an existing one that
+ * was already validated (thus this one doesn't need validation).
+ * parentDelTrigger and parentUpdTrigger, when being recursively called on
+ * a partition, are the OIDs of the parent action triggers for DELETE and
+ * UPDATE respectively.
+ */
+static void
+addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
+ Relation pkrel, Oid indexOid, Oid parentConstr,
+ int numfks,
+ int16 *pkattnum, int16 *fkattnum, Oid *pfeqoperators,
+ Oid *ppeqoperators, Oid *ffeqoperators,
+ int numfkdelsetcols, int16 *fkdelsetcols,
+ bool old_check_ok,
+ Oid parentDelTrigger, Oid parentUpdTrigger)
+{
+ Oid deleteTriggerOid,
+ updateTriggerOid;
+
/*
* Create the action triggers that enforce the constraint.
*/
createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
fkconstraint,
- constrOid, indexOid,
+ parentConstr, indexOid,
parentDelTrigger, parentUpdTrigger,
&deleteTriggerOid, &updateTriggerOid);
@@ -10179,6 +10231,7 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
AttrMap *map;
AttrNumber *mapped_pkattnum;
Oid partIndexId;
+ ObjectAddress address;
partRel = table_open(pd->oids[i], ShareRowExclusiveLock);
@@ -10203,8 +10256,14 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
if (!OidIsValid(partIndexId))
elog(ERROR, "index for %u not found in partition %s",
indexOid, RelationGetRelationName(partRel));
+
+ address = addFkConstraint(fkconstraint, rel, partRel,
+ partIndexId, parentConstr, numfks,
+ mapped_pkattnum, fkattnum,
+ pfeqoperators, ppeqoperators, ffeqoperators,
+ numfkdelsetcols, fkdelsetcols);
addFkRecurseReferenced(wqueue, fkconstraint, rel, partRel,
- partIndexId, constrOid, numfks,
+ partIndexId, address.objectId, numfks,
mapped_pkattnum, fkattnum,
pfeqoperators, ppeqoperators, ffeqoperators,
numfkdelsetcols, fkdelsetcols,
@@ -10220,8 +10279,6 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
}
}
}
-
- return address;
}
/*
@@ -10583,6 +10640,7 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
int numfkdelsetcols;
AttrNumber confdelsetcols[INDEX_MAX_KEYS];
Constraint *fkconstraint;
+ ObjectAddress address;
Oid deleteTriggerOid,
updateTriggerOid;
@@ -10616,7 +10674,7 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
* Because we're only expanding the key space at the referenced side,
* we don't need to prevent any operation in the referencing table, so
* AccessShareLock suffices (assumes that dropping the constraint
- * acquires AEL).
+ * acquires AccessExclusiveLock).
*/
fkRel = table_open(constrForm->conrelid, AccessShareLock);
@@ -10682,12 +10740,16 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
constrForm->confrelid, constrForm->conrelid,
&deleteTriggerOid, &updateTriggerOid);
+ address = addFkConstraint(fkconstraint, fkRel, partitionRel, partIndexId,
+ constrOid, numfks, mapped_confkey, conkey,
+ conpfeqop, conppeqop, conffeqop,
+ numfkdelsetcols, confdelsetcols);
addFkRecurseReferenced(NULL,
fkconstraint,
fkRel,
partitionRel,
partIndexId,
- constrOid,
+ address.objectId,
numfks,
mapped_confkey,
conkey,
@@ -11139,6 +11201,83 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
partRelid);
+ /*
+ * If the referenced table is partitioned, then the partition we're
+ * attaching now has extra pg_constraint rows and action triggers that are
+ * no longer needed. Remove those.
+ */
+ if (get_rel_relkind(fk->confrelid) == RELKIND_PARTITIONED_TABLE)
+ {
+ Relation pg_constraint = table_open(ConstraintRelationId, RowShareLock);
+ ObjectAddresses *objs;
+ HeapTuple consttup;
+
+ ScanKeyInit(&key,
+ Anum_pg_constraint_conrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(fk->conrelid));
+
+ scan = systable_beginscan(pg_constraint,
+ ConstraintRelidTypidNameIndexId,
+ true, NULL, 1, &key);
+ objs = new_object_addresses();
+ while ((consttup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_constraint conform = (Form_pg_constraint) GETSTRUCT(consttup);
+
+ if (conform->conparentid != fk->conoid)
+ continue;
+ else
+ {
+ ObjectAddress addr;
+ int n;
+ SysScanDesc scan2;
+ ScanKeyData key2;
+
+ ObjectAddressSet(addr, ConstraintRelationId, conform->oid);
+ add_exact_object_address(&addr, objs);
+
+ /*
+ * First we must delete the dependency records that bind
+ * the constraint records together.
+ */
+ n = deleteDependencyRecordsForSpecific(ConstraintRelationId,
+ conform->oid,
+ DEPENDENCY_INTERNAL,
+ ConstraintRelationId,
+ fk->conoid);
+ if (n != 1)
+ elog(WARNING, "oops: found %d instead of 1 deps from %u to %u",
+ n, conform->oid, fk->conoid);
+
+ /*
+ * Now search for the triggers for this constraint and set
+ * them up for deletion too
+ */
+ ScanKeyInit(&key2,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conform->oid));
+ scan2 = systable_beginscan(trigrel, TriggerConstraintIndexId,
+ true, NULL, 1, &key2);
+ while ((trigtup = systable_getnext(scan2)) != NULL)
+ {
+ ObjectAddressSet(addr, TriggerRelationId,
+ ((Form_pg_trigger) GETSTRUCT(trigtup))->oid);
+ add_exact_object_address(&addr, objs);
+ }
+ systable_endscan(scan2);
+ }
+ }
+ /* make the dependency deletions visible */
+ CommandCounterIncrement();
+ performMultipleDeletions(objs, DROP_RESTRICT,
+ PERFORM_DELETION_INTERNAL);
+ systable_endscan(scan);
+
+ table_close(pg_constraint, RowShareLock);
+ }
+
CommandCounterIncrement();
return true;
}
@@ -19132,7 +19271,7 @@ ATExecDetachPartition(List **wqueue, AlteredTableInfo *tab, Relation rel,
}
/* Do the final part of detaching */
- DetachPartitionFinalize(rel, partRel, concurrent, defaultPartOid);
+ DetachPartitionFinalize(wqueue, rel, partRel, concurrent, defaultPartOid);
ObjectAddressSet(address, RelationRelationId, RelationGetRelid(partRel));
@@ -19149,8 +19288,8 @@ ATExecDetachPartition(List **wqueue, AlteredTableInfo *tab, Relation rel,
* transaction of the concurrent algorithm fails (crash or abort).
*/
static void
-DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
- Oid defaultPartOid)
+DetachPartitionFinalize(List **wqueue, Relation rel, Relation partRel,
+ bool concurrent, Oid defaultPartOid)
{
Relation classRel;
List *fks;
@@ -19186,8 +19325,11 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
{
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
+ HeapTuple parentConTup;
Form_pg_constraint conform;
+ Form_pg_constraint parentConForm;
Constraint *fkconstraint;
+ Oid parentConstrOid;
Oid insertTriggerOid,
updateTriggerOid;
@@ -19204,7 +19346,20 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
continue;
}
- /* unset conparentid and adjust conislocal, coninhcount, etc. */
+ parentConstrOid = conform->conparentid;
+
+ Assert(OidIsValid(conform->conparentid));
+ parentConTup = SearchSysCache1(CONSTROID,
+ ObjectIdGetDatum(parentConstrOid));
+ if (!HeapTupleIsValid(parentConTup))
+ elog(ERROR, "cache lookup failed for constraint %u",
+ conform->conparentid);
+ parentConForm = (Form_pg_constraint) GETSTRUCT(parentConTup);
+
+ /*
+ * The constraint on this table must be marked no longer a child of
+ * the parent's constraint, as do its check triggers.
+ */
ConstraintSetParentConstraint(fk->conoid, InvalidOid, InvalidOid);
/*
@@ -19222,35 +19377,88 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
RelationGetRelid(partRel));
/*
- * Make the action triggers on the referenced relation. When this was
- * a partition the action triggers pointed to the parent rel (they
- * still do), but now we need separate ones of our own.
+ * If the referenced side is partitioned (which we know because our
+ * parent's constraint points to a different relation than ours) then
+ * we must, in addition to the above, create pg_constraint rows that
+ * point to each partition, each with its own action triggers.
*/
- fkconstraint = makeNode(Constraint);
- fkconstraint->contype = CONSTRAINT_FOREIGN;
- fkconstraint->conname = pstrdup(NameStr(conform->conname));
- fkconstraint->deferrable = conform->condeferrable;
- fkconstraint->initdeferred = conform->condeferred;
- fkconstraint->location = -1;
- fkconstraint->pktable = NULL;
- fkconstraint->fk_attrs = NIL;
- fkconstraint->pk_attrs = NIL;
- fkconstraint->fk_matchtype = conform->confmatchtype;
- fkconstraint->fk_upd_action = conform->confupdtype;
- fkconstraint->fk_del_action = conform->confdeltype;
- fkconstraint->fk_del_set_cols = NIL;
- fkconstraint->old_conpfeqop = NIL;
- fkconstraint->old_pktable_oid = InvalidOid;
- fkconstraint->skip_validation = false;
- fkconstraint->initially_valid = true;
+ if (parentConForm->conrelid != conform->conrelid)
+ {
+ int numfks;
+ AttrNumber conkey[INDEX_MAX_KEYS];
+ AttrMap *attmap;
+ AttrNumber confkey[INDEX_MAX_KEYS];
+ Oid conpfeqop[INDEX_MAX_KEYS];
+ Oid conppeqop[INDEX_MAX_KEYS];
+ Oid conffeqop[INDEX_MAX_KEYS];
+ int numfkdelsetcols;
+ AttrNumber confdelsetcols[INDEX_MAX_KEYS];
+ Relation refdRel;
+
+ DeconstructFkConstraintRow(contup,
+ &numfks,
+ conkey,
+ confkey,
+ conpfeqop,
+ conppeqop,
+ conffeqop,
+ &numfkdelsetcols,
+ confdelsetcols);
+
+ /* Create a synthetic node we'll use throughout */
+ fkconstraint = makeNode(Constraint);
+ fkconstraint->contype = CONSTRAINT_FOREIGN;
+ fkconstraint->conname = pstrdup(NameStr(conform->conname));
+ fkconstraint->deferrable = conform->condeferrable;
+ fkconstraint->initdeferred = conform->condeferred;
+ fkconstraint->skip_validation = true;
+ fkconstraint->initially_valid = true;
+ /* a few irrelevant fields omitted here */
+ fkconstraint->pktable = NULL;
+ fkconstraint->fk_attrs = NIL;
+ fkconstraint->pk_attrs = NIL;
+ fkconstraint->fk_matchtype = conform->confmatchtype;
+ fkconstraint->fk_upd_action = conform->confupdtype;
+ fkconstraint->fk_del_action = conform->confdeltype;
+ fkconstraint->fk_del_set_cols = NIL;
+ fkconstraint->old_conpfeqop = NIL;
+ fkconstraint->old_pktable_oid = InvalidOid;
+ fkconstraint->location = -1;
+
+ attmap = build_attrmap_by_name(RelationGetDescr(partRel),
+ RelationGetDescr(rel),
+ false);
+ for (int i = 0; i < numfks; i++)
+ {
+ Form_pg_attribute att;
- createForeignKeyActionTriggers(partRel, conform->confrelid,
- fkconstraint, fk->conoid,
+ att = TupleDescAttr(RelationGetDescr(partRel),
+ attmap->attnums[conkey[i] - 1] - 1);
+ fkconstraint->fk_attrs = lappend(fkconstraint->fk_attrs,
+ makeString(NameStr(att->attname)));
+ }
+
+ refdRel = table_open(fk->confrelid, AccessShareLock);
+
+ addFkRecurseReferenced(wqueue, fkconstraint, partRel,
+ refdRel,
conform->conindid,
- InvalidOid, InvalidOid,
- NULL, NULL);
+ fk->conoid,
+ numfks,
+ confkey,
+ conkey,
+ conpfeqop,
+ conppeqop,
+ conffeqop,
+ numfkdelsetcols,
+ confdelsetcols,
+ true,
+ InvalidOid, InvalidOid);
+ table_close(refdRel, AccessShareLock);
+ }
ReleaseSysCache(contup);
+ ReleaseSysCache(parentConTup);
}
list_free_deep(fks);
if (trigrel)
@@ -19397,7 +19605,7 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
* completion; this completes the detaching process.
*/
static ObjectAddress
-ATExecDetachPartitionFinalize(Relation rel, RangeVar *name)
+ATExecDetachPartitionFinalize(List **wqueue, Relation rel, RangeVar *name)
{
Relation partRel;
ObjectAddress address;
@@ -19415,7 +19623,7 @@ ATExecDetachPartitionFinalize(Relation rel, RangeVar *name)
*/
WaitForOlderSnapshots(snap->xmin, false);
- DetachPartitionFinalize(rel, partRel, true, InvalidOid);
+ DetachPartitionFinalize(wqueue, rel, partRel, true, InvalidOid);
ObjectAddressSet(address, RelationRelationId, RelationGetRelid(partRel));
--
2.46.0
v3-17-0003-Use-addFkConstraint-in-addFkRecurseReferencing.patchtext/x-patchDownload
From 885bae49e8494fc1465f78001ba87de34c654165 Mon Sep 17 00:00:00 2001
From: Jehan-Guillaume de Rorthais <jgdr@dalibo.com>
Date: Tue, 24 Sep 2024 18:10:33 +0200
Subject: [PATCH v3-17 3/4] Use addFkConstraint() in addFkRecurseReferencing()
---
src/backend/commands/tablecmds.c | 121 ++++++++++++-------------------
1 file changed, 48 insertions(+), 73 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 83e0a0652e..ca9fb3818b 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -345,6 +345,14 @@ typedef struct ForeignTruncateInfo
List *rels;
} ForeignTruncateInfo;
+typedef enum FKConstraintSide
+{
+ FK_BOTH_SIDE,
+ FK_REFERENCED_SIDE,
+ FK_REFERENCING_SIDE
+}
+FKConstraintSide;
+
/*
* Partition tables are expected to be dropped when the parent partitioned
* table gets dropped. Hence for partitioning we use AUTO dependency.
@@ -505,7 +513,8 @@ static ObjectAddress ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *
bool recurse, bool recursing,
LOCKMODE lockmode);
static ObjectAddress addFkConstraint(Constraint *fkconstraint, Relation rel,
- Relation pkrel, Oid indexOid, Oid parentConstr,
+ Relation pkrel, Oid indexOid,
+ Oid parentConstr, FKConstraintSide fkside,
int numfks, int16 *pkattnum, int16 *fkattnum,
Oid *pfeqoperators, Oid *ppeqoperators,
Oid *ffeqoperators, int numfkdelsetcols,
@@ -9946,6 +9955,7 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
address = addFkConstraint(fkconstraint, rel, pkrel,
indexOid,
InvalidOid, /* no parent constraint */
+ FK_BOTH_SIDE,
numfks,
pkattnum,
fkattnum,
@@ -10040,6 +10050,7 @@ validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
* * indexOid is the OID of the index (on pkrel) implementing this constraint.
* * parentConstr is the OID of a parent constraint; InvalidOid if this is a
* top-level constraint.
+ * * fkside is the side of FK related to this inherited constraint.
* * numfks is the number of columns in the foreign key.
* * pkattnum is the attnum array of referenced attributes.
* * fkattnum is the attnum array of referencing attributes.
@@ -10051,8 +10062,9 @@ validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
*/
static ObjectAddress
addFkConstraint(Constraint *fkconstraint, Relation rel, Relation pkrel,
- Oid indexOid, Oid parentConstr, int numfks, int16 *pkattnum,
- int16 *fkattnum, Oid *pfeqoperators, Oid *ppeqoperators,
+ Oid indexOid, Oid parentConstr, FKConstraintSide fkside,
+ int numfks, int16 *pkattnum, int16 *fkattnum,
+ Oid *pfeqoperators, Oid *ppeqoperators,
Oid *ffeqoperators, int numfkdelsetcols, int16 *fkdelsetcols)
{
ObjectAddress address;
@@ -10141,18 +10153,31 @@ addFkConstraint(Constraint *fkconstraint, Relation rel, Relation pkrel,
ObjectAddressSet(address, ConstraintRelationId, constrOid);
- /*
- * Mark the child constraint as part of the parent constraint; it must not
- * be dropped on its own. (This constraint is deleted when the partition
- * is detached, but a special check needs to occur that the partition
- * contains no referenced values.)
- */
if (OidIsValid(parentConstr))
{
ObjectAddress referenced;
ObjectAddressSet(referenced, ConstraintRelationId, parentConstr);
- recordDependencyOn(&address, &referenced, DEPENDENCY_INTERNAL);
+ if (fkside == FK_REFERENCED_SIDE)
+ {
+ /*
+ * Mark the child constraint as part of the parent constraint; it must not
+ * be dropped on its own. (This constraint is deleted when the partition
+ * is detached, but a special check needs to occur that the partition
+ * contains no referenced values.)
+ */
+ recordDependencyOn(&address, &referenced, DEPENDENCY_INTERNAL);
+ }
+ else
+ {
+ /*
+ * Give this constraint partition-type dependencies on the parent
+ * constraint as well as the table.
+ */
+ recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_PRI);
+ ObjectAddressSet(referenced, RelationRelationId, RelationGetRelid(rel));
+ recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_SEC);
+ }
}
/* make new constraint visible, in case we add more */
@@ -10258,8 +10283,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
indexOid, RelationGetRelationName(partRel));
address = addFkConstraint(fkconstraint, rel, partRel,
- partIndexId, parentConstr, numfks,
- mapped_pkattnum, fkattnum,
+ partIndexId, parentConstr, FK_REFERENCED_SIDE,
+ numfks, mapped_pkattnum, fkattnum,
pfeqoperators, ppeqoperators, ffeqoperators,
numfkdelsetcols, fkdelsetcols);
addFkRecurseReferenced(wqueue, fkconstraint, rel, partRel,
@@ -10403,10 +10428,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
AttrMap *attmap;
AttrNumber mapped_fkattnum[INDEX_MAX_KEYS];
bool attached;
- char *conname;
- Oid constrOid;
- ObjectAddress address,
- referenced;
+ ObjectAddress address;
ListCell *cell;
CheckAlterTableIsSafe(partition);
@@ -10449,65 +10471,18 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
/*
* No luck finding a good constraint to reuse; create our own.
*/
- if (ConstraintNameIsUsed(CONSTRAINT_RELATION,
- RelationGetRelid(partition),
- fkconstraint->conname))
- conname = ChooseConstraintName(RelationGetRelationName(partition),
- ChooseForeignKeyConstraintNameAddition(fkconstraint->fk_attrs),
- "fkey",
- RelationGetNamespace(partition), NIL);
- else
- conname = fkconstraint->conname;
- constrOid =
- CreateConstraintEntry(conname,
- RelationGetNamespace(partition),
- CONSTRAINT_FOREIGN,
- fkconstraint->deferrable,
- fkconstraint->initdeferred,
- fkconstraint->initially_valid,
- parentConstr,
- partitionId,
- mapped_fkattnum,
- numfks,
- numfks,
- InvalidOid,
- indexOid,
- RelationGetRelid(pkrel),
- pkattnum,
- pfeqoperators,
- ppeqoperators,
- ffeqoperators,
- numfks,
- fkconstraint->fk_upd_action,
- fkconstraint->fk_del_action,
- fkdelsetcols,
- numfkdelsetcols,
- fkconstraint->fk_matchtype,
- NULL,
- NULL,
- NULL,
- false,
- 1,
- false,
- false);
-
- /*
- * Give this constraint partition-type dependencies on the parent
- * constraint as well as the table.
- */
- ObjectAddressSet(address, ConstraintRelationId, constrOid);
- ObjectAddressSet(referenced, ConstraintRelationId, parentConstr);
- recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_PRI);
- ObjectAddressSet(referenced, RelationRelationId, partitionId);
- recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_SEC);
- /* Make all this visible before recursing */
- CommandCounterIncrement();
+ address = addFkConstraint(fkconstraint,
+ partition, pkrel, indexOid, parentConstr,
+ FK_REFERENCING_SIDE, numfks, pkattnum,
+ mapped_fkattnum, pfeqoperators,
+ ppeqoperators, ffeqoperators,
+ numfkdelsetcols, fkdelsetcols);
/* call ourselves to finalize the creation and we're done */
addFkRecurseReferencing(wqueue, fkconstraint, partition, pkrel,
indexOid,
- constrOid,
+ address.objectId,
numfks,
pkattnum,
mapped_fkattnum,
@@ -10741,9 +10716,9 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
&deleteTriggerOid, &updateTriggerOid);
address = addFkConstraint(fkconstraint, fkRel, partitionRel, partIndexId,
- constrOid, numfks, mapped_confkey, conkey,
- conpfeqop, conppeqop, conffeqop,
- numfkdelsetcols, confdelsetcols);
+ constrOid, FK_REFERENCED_SIDE, numfks,
+ mapped_confkey, conkey, conpfeqop, conppeqop,
+ conffeqop, numfkdelsetcols, confdelsetcols);
addFkRecurseReferenced(NULL,
fkconstraint,
fkRel,
--
2.46.0
v3-17-0004-Use-addFkConstraint-in-CloneFkReferencing.patchtext/x-patchDownload
From 2fd83ff14e0a3880c1ff5f6458cfd52638724a7b Mon Sep 17 00:00:00 2001
From: Jehan-Guillaume de Rorthais <jgdr@dalibo.com>
Date: Wed, 25 Sep 2024 13:31:07 +0200
Subject: [PATCH v3-17 4/4] Use addFkConstraint() in CloneFkReferencing()
---
src/backend/commands/tablecmds.c | 123 +++++++++++--------------------
1 file changed, 43 insertions(+), 80 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index ca9fb3818b..23be2eefd0 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -512,13 +512,14 @@ static ObjectAddress ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *
Relation rel, Constraint *fkconstraint,
bool recurse, bool recursing,
LOCKMODE lockmode);
-static ObjectAddress addFkConstraint(Constraint *fkconstraint, Relation rel,
+static ObjectAddress addFkConstraint(char *constraintname,
+ Constraint *fkconstraint, Relation rel,
Relation pkrel, Oid indexOid,
Oid parentConstr, FKConstraintSide fkside,
int numfks, int16 *pkattnum, int16 *fkattnum,
Oid *pfeqoperators, Oid *ppeqoperators,
Oid *ffeqoperators, int numfkdelsetcols,
- int16 *fkdelsetcols);
+ int16 *fkdelsetcols, bool is_internal);
static void addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint,
Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks, int16 *pkattnum, int16 *fkattnum,
@@ -9952,7 +9953,7 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
* Create all the constraint and trigger objects, recursing to partitions
* as necessary. First handle the referenced side.
*/
- address = addFkConstraint(fkconstraint, rel, pkrel,
+ address = addFkConstraint(fkconstraint->conname, fkconstraint, rel, pkrel,
indexOid,
InvalidOid, /* no parent constraint */
FK_BOTH_SIDE,
@@ -9963,7 +9964,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
ppeqoperators,
ffeqoperators,
numfkdelsetcols,
- fkdelsetcols);
+ fkdelsetcols,
+ false);
addFkRecurseReferenced(wqueue, fkconstraint, rel, pkrel,
indexOid,
@@ -10044,6 +10046,8 @@ validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
*
* Creates a pg_constraint rows referencing a parent if given.
*
+ * * constraintname the base name for the constraint being added. This is
+ * copied to fkconstraint->conname if the later is not set.
* * fkconstraint is the constraint being added.
* * rel is the root referencing relation.
* * pkrel is the referenced relation; might be a partition, if recursing.
@@ -10061,11 +10065,12 @@ validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
* NULL/DEFAULT clause.
*/
static ObjectAddress
-addFkConstraint(Constraint *fkconstraint, Relation rel, Relation pkrel,
- Oid indexOid, Oid parentConstr, FKConstraintSide fkside,
- int numfks, int16 *pkattnum, int16 *fkattnum,
- Oid *pfeqoperators, Oid *ppeqoperators,
- Oid *ffeqoperators, int numfkdelsetcols, int16 *fkdelsetcols)
+addFkConstraint(char *constraintname, Constraint *fkconstraint, Relation rel,
+ Relation pkrel, Oid indexOid, Oid parentConstr,
+ FKConstraintSide fkside, int numfks, int16 *pkattnum,
+ int16 *fkattnum, Oid *pfeqoperators, Oid *ppeqoperators,
+ Oid *ffeqoperators, int numfkdelsetcols, int16 *fkdelsetcols,
+ bool is_internal)
{
ObjectAddress address;
Oid constrOid;
@@ -10091,13 +10096,16 @@ addFkConstraint(Constraint *fkconstraint, Relation rel, Relation pkrel,
*/
if (ConstraintNameIsUsed(CONSTRAINT_RELATION,
RelationGetRelid(rel),
- fkconstraint->conname))
+ constraintname))
conname = ChooseConstraintName(RelationGetRelationName(rel),
ChooseForeignKeyConstraintNameAddition(fkconstraint->fk_attrs),
"fkey",
RelationGetNamespace(rel), NIL);
else
- conname = fkconstraint->conname;
+ conname = constraintname;
+
+ if (fkconstraint->conname == NULL)
+ fkconstraint->conname = pstrdup(conname);
if (OidIsValid(parentConstr))
{
@@ -10149,7 +10157,7 @@ addFkConstraint(Constraint *fkconstraint, Relation rel, Relation pkrel,
conislocal, /* islocal */
coninhcount, /* inhcount */
connoinherit, /* conNoInherit */
- false); /* is_internal */
+ is_internal); /* is_internal */
ObjectAddressSet(address, ConstraintRelationId, constrOid);
@@ -10282,11 +10290,12 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
elog(ERROR, "index for %u not found in partition %s",
indexOid, RelationGetRelationName(partRel));
- address = addFkConstraint(fkconstraint, rel, partRel,
- partIndexId, parentConstr, FK_REFERENCED_SIDE,
- numfks, mapped_pkattnum, fkattnum,
- pfeqoperators, ppeqoperators, ffeqoperators,
- numfkdelsetcols, fkdelsetcols);
+ address = addFkConstraint(fkconstraint->conname, fkconstraint, rel,
+ partRel, partIndexId, parentConstr,
+ FK_REFERENCED_SIDE, numfks, mapped_pkattnum,
+ fkattnum, pfeqoperators, ppeqoperators,
+ ffeqoperators, numfkdelsetcols,
+ fkdelsetcols, false);
addFkRecurseReferenced(wqueue, fkconstraint, rel, partRel,
partIndexId, address.objectId, numfks,
mapped_pkattnum, fkattnum,
@@ -10472,12 +10481,12 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
* No luck finding a good constraint to reuse; create our own.
*/
- address = addFkConstraint(fkconstraint,
+ address = addFkConstraint(fkconstraint->conname, fkconstraint,
partition, pkrel, indexOid, parentConstr,
FK_REFERENCING_SIDE, numfks, pkattnum,
mapped_fkattnum, pfeqoperators,
ppeqoperators, ffeqoperators,
- numfkdelsetcols, fkdelsetcols);
+ numfkdelsetcols, fkdelsetcols, false);
/* call ourselves to finalize the creation and we're done */
addFkRecurseReferencing(wqueue, fkconstraint, partition, pkrel,
@@ -10715,10 +10724,11 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
constrForm->confrelid, constrForm->conrelid,
&deleteTriggerOid, &updateTriggerOid);
- address = addFkConstraint(fkconstraint, fkRel, partitionRel, partIndexId,
- constrOid, FK_REFERENCED_SIDE, numfks,
- mapped_confkey, conkey, conpfeqop, conppeqop,
- conffeqop, numfkdelsetcols, confdelsetcols);
+ address = addFkConstraint(fkconstraint->conname, fkconstraint, fkRel,
+ partitionRel, partIndexId, constrOid,
+ FK_REFERENCED_SIDE, numfks, mapped_confkey,
+ conkey, conpfeqop, conppeqop, conffeqop,
+ numfkdelsetcols, confdelsetcols, false);
addFkRecurseReferenced(NULL,
fkconstraint,
fkRel,
@@ -10839,9 +10849,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
Constraint *fkconstraint;
bool attached;
Oid indexOid;
- Oid constrOid;
- ObjectAddress address,
- referenced;
+ ObjectAddress address;
ListCell *lc;
Oid insertTriggerOid,
updateTriggerOid;
@@ -10938,7 +10946,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
fkconstraint->old_conpfeqop = NIL;
fkconstraint->old_pktable_oid = InvalidOid;
fkconstraint->skip_validation = false;
- fkconstraint->initially_valid = true;
+ fkconstraint->initially_valid = constrForm->convalidated;
for (int i = 0; i < numfks; i++)
{
Form_pg_attribute att;
@@ -10948,71 +10956,26 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
fkconstraint->fk_attrs = lappend(fkconstraint->fk_attrs,
makeString(NameStr(att->attname)));
}
- if (ConstraintNameIsUsed(CONSTRAINT_RELATION,
- RelationGetRelid(partRel),
- NameStr(constrForm->conname)))
- fkconstraint->conname =
- ChooseConstraintName(RelationGetRelationName(partRel),
- ChooseForeignKeyConstraintNameAddition(fkconstraint->fk_attrs),
- "fkey",
- RelationGetNamespace(partRel), NIL);
- else
- fkconstraint->conname = pstrdup(NameStr(constrForm->conname));
indexOid = constrForm->conindid;
- constrOid =
- CreateConstraintEntry(fkconstraint->conname,
- constrForm->connamespace,
- CONSTRAINT_FOREIGN,
- fkconstraint->deferrable,
- fkconstraint->initdeferred,
- constrForm->convalidated,
- parentConstrOid,
- RelationGetRelid(partRel),
- mapped_conkey,
- numfks,
- numfks,
- InvalidOid, /* not a domain constraint */
- indexOid,
- constrForm->confrelid, /* same foreign rel */
- confkey,
- conpfeqop,
- conppeqop,
- conffeqop,
- numfks,
- fkconstraint->fk_upd_action,
- fkconstraint->fk_del_action,
- confdelsetcols,
- numfkdelsetcols,
- fkconstraint->fk_matchtype,
- NULL,
- NULL,
- NULL,
- false, /* islocal */
- 1, /* inhcount */
- false, /* conNoInherit */
- true);
- /* Set up partition dependencies for the new constraint */
- ObjectAddressSet(address, ConstraintRelationId, constrOid);
- ObjectAddressSet(referenced, ConstraintRelationId, parentConstrOid);
- recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_PRI);
- ObjectAddressSet(referenced, RelationRelationId,
- RelationGetRelid(partRel));
- recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_SEC);
+ address = addFkConstraint(NameStr(constrForm->conname), fkconstraint,
+ partRel, pkrel, indexOid, parentConstrOid,
+ FK_REFERENCING_SIDE, numfks, confkey,
+ mapped_conkey, conpfeqop,
+ conppeqop, conffeqop,
+ numfkdelsetcols, confdelsetcols,
+ true);
/* Done with the cloned constraint's tuple */
ReleaseSysCache(tuple);
- /* Make all this visible before recursing */
- CommandCounterIncrement();
-
addFkRecurseReferencing(wqueue,
fkconstraint,
partRel,
pkrel,
indexOid,
- constrOid,
+ address.objectId,
numfks,
confkey,
mapped_conkey,
--
2.46.0
On Wed, 25 Sep 2024 16:14:07 +0200
Jehan-Guillaume de Rorthais <jgdr@dalibo.com> wrote:
On Wed, 25 Sep 2024 14:42:40 +0200
Jehan-Guillaume de Rorthais <jgdr@dalibo.com> wrote:On Thu, 5 Sep 2024 00:57:28 +0200
Jehan-Guillaume de Rorthais <jgdr@dalibo.com> wrote:[…]
Please, find in attachment a patch implementing this idea.
Please, find in attachment a set of patch based on the previous one.
Please, find in attachment the same set of patch for REL_17_STABLE.
The set of patch for REL_17_STABLE apply on REL_16_STABLE with no effort.
I've been able to backpatch on REL_15_STABLE with minimal effort. See
attachments.
REL_14_STABLE backport doesn't seem trivial, so I'll wait for some feedback,
review & decision before going further down in backpatching.
Regards,
Attachments:
v3-15-0001-Add-tests-about-FK-between-partitionned-tables.patchtext/x-patchDownload
From ffdc7bad063a9092afbc92ff7d5871d5d17e2ff7 Mon Sep 17 00:00:00 2001
From: Jehan-Guillaume de Rorthais <jgdr@dalibo.com>
Date: Mon, 23 Sep 2024 18:50:20 +0200
Subject: [PATCH v3-15 1/4] Add tests about FK between partitionned tables
These tests cover two reported bugs related to FK
between two partitionned tables.
co-author : Alvaro Herrera <alvherre@alvh.no-ip.org>
co-author : Jehan-Guillaume de Rorthais <jgdr@dalibo.com>
co-author : Tender Wang <tndrwang@gmail.com>
Discussion: https://postgr.es/m/20230420144344.40744130@karst
Discussion: https://postgr.es/m/20230705233028.2f554f73@karst
---
src/test/regress/expected/foreign_key.out | 266 ++++++++++++++++++++++
src/test/regress/sql/foreign_key.sql | 101 ++++++++
2 files changed, 367 insertions(+)
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 5f4d6f84ed..b8fc6f1d7c 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -2934,3 +2934,269 @@ DETAIL: drop cascades to table fkpart11.pk
drop cascades to table fkpart11.fk_parted
drop cascades to table fkpart11.fk_another
drop cascades to function fkpart11.print_row()
+-- When a table is attached as partition to a partitioned table that has
+-- a foreign key to another partitioned table, it acquires a clone of the
+-- FK. Upon detach, this clone is not removed, but instead becomes an
+-- independent FK. If it then attaches to the partitioned table again,
+-- the FK from the parent "takes over" ownership of the independent FK rather
+-- than creating a separate one.
+CREATE SCHEMA fkpart12
+ CREATE TABLE fk_p ( id int, jd int, PRIMARY KEY(id, jd)) PARTITION BY list (id)
+ CREATE TABLE fk_p_1 PARTITION OF fk_p FOR VALUES IN (1) PARTITION BY list (jd)
+ CREATE TABLE fk_p_1_1 PARTITION OF fk_p_1 FOR VALUES IN (1)
+ CREATE TABLE fk_p_1_2 PARTITION OF fk_p_1 FOR VALUES IN (2)
+ CREATE TABLE fk_p_2 PARTITION OF fk_p FOR VALUES IN (2) PARTITION BY list (jd)
+ CREATE TABLE fk_p_2_1 PARTITION OF fk_p_2 FOR VALUES IN (1)
+ CREATE TABLE fk_p_2_2 PARTITION OF fk_p_2 FOR VALUES IN (2)
+ CREATE TABLE fk_r_1 ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL)
+ CREATE TABLE fk_r_2 ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL)
+ CREATE TABLE fk_r ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL,
+ FOREIGN KEY (p_id, p_jd) REFERENCES fk_p (id, jd)
+ ) PARTITION BY list (id);
+SET search_path TO fkpart12;
+CREATE OR REPLACE FUNCTION
+fkpart12_constraints(OUT conname name, OUT conrel regclass,
+ OUT confrelid regclass, OUT conparent name) RETURNS SETOF record AS $$
+ WITH RECURSIVE r AS (
+ SELECT oid, conname, conrelid, confrelid, NULL::name AS conparent
+ FROM pg_constraint
+ WHERE conrelid = 'fk_r'::regclass
+ AND contype = 'f'
+ AND conparentid = 0
+ UNION ALL
+ SELECT c.oid, c.conname, c.conrelid, c.confrelid, r.conname
+ FROM pg_constraint c
+ JOIN r ON c.conparentid = r.oid
+ )
+ SELECT conname, conrelid::regclass, confrelid::regclass, conparent
+ FROM r
+ ORDER BY oid, conname, confrelid, conparent;
+$$
+LANGUAGE SQL;
+CREATE OR REPLACE FUNCTION
+fkpart12_triggers(OUT conname name, OUT conparent name,
+ OUT tgfoid regproc, OUT tgfparent regproc) RETURNS SETOF record AS $$
+
+ WITH RECURSIVE r AS (
+ SELECT t.oid, t.tgfoid::regproc, c.conname, NULL::name AS conparent, NULL::regproc AS tgfparent
+ FROM pg_trigger t
+ JOIN pg_constraint c ON c.oid = t.tgconstraint
+ WHERE c.conrelid = 'fk_r'::regclass
+ AND c.contype = 'f'
+ AND t.tgparentid = 0
+ UNION ALL
+ SELECT t2.oid, t2.tgfoid::regproc, c2.conname, c3.conname, r.tgfoid::regproc AS tgfparent
+ FROM pg_trigger t2
+ JOIN pg_constraint c2 ON c2.oid = t2.tgconstraint
+ LEFT JOIN pg_constraint c3 ON c3.oid = c2.conparentid
+ JOIN r ON r.oid = t2.tgparentid
+ )
+ SELECT conname, conparent, tgfoid, tgfparent
+ FROM r
+ ORDER BY conname, conparent, tgfoid, tgfparent;
+$$
+LANGUAGE SQL;
+SELECT * FROM fkpart12_constraints();
+ conname | conrel | confrelid | conparent
+----------------------+--------+-----------+----------------------
+ fk_r_p_id_p_jd_fkey | fk_r | fk_p |
+ fk_r_p_id_p_jd_fkey1 | fk_r | fk_p_1 | fk_r_p_id_p_jd_fkey
+ fk_r_p_id_p_jd_fkey2 | fk_r | fk_p_1_1 | fk_r_p_id_p_jd_fkey1
+ fk_r_p_id_p_jd_fkey3 | fk_r | fk_p_1_2 | fk_r_p_id_p_jd_fkey1
+ fk_r_p_id_p_jd_fkey4 | fk_r | fk_p_2 | fk_r_p_id_p_jd_fkey
+ fk_r_p_id_p_jd_fkey5 | fk_r | fk_p_2_1 | fk_r_p_id_p_jd_fkey4
+ fk_r_p_id_p_jd_fkey6 | fk_r | fk_p_2_2 | fk_r_p_id_p_jd_fkey4
+(7 rows)
+
+SELECT * FROM fkpart12_triggers();
+ conname | conparent | tgfoid | tgfparent
+----------------------+----------------------+------------------------+------------------------
+ fk_r_p_id_p_jd_fkey | | "RI_FKey_check_ins" |
+ fk_r_p_id_p_jd_fkey | | "RI_FKey_check_upd" |
+ fk_r_p_id_p_jd_fkey | | "RI_FKey_noaction_del" |
+ fk_r_p_id_p_jd_fkey | | "RI_FKey_noaction_upd" |
+ fk_r_p_id_p_jd_fkey1 | fk_r_p_id_p_jd_fkey | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey1 | fk_r_p_id_p_jd_fkey | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey2 | fk_r_p_id_p_jd_fkey1 | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey2 | fk_r_p_id_p_jd_fkey1 | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey3 | fk_r_p_id_p_jd_fkey1 | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey3 | fk_r_p_id_p_jd_fkey1 | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey4 | fk_r_p_id_p_jd_fkey | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey4 | fk_r_p_id_p_jd_fkey | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey5 | fk_r_p_id_p_jd_fkey4 | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey5 | fk_r_p_id_p_jd_fkey4 | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey6 | fk_r_p_id_p_jd_fkey4 | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey6 | fk_r_p_id_p_jd_fkey4 | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+(16 rows)
+
+INSERT INTO fk_p VALUES (1, 1);
+ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1);
+ALTER TABLE fk_r ATTACH PARTITION fk_r_2 FOR VALUES IN (2);
+\d fk_r_1
+ Table "fkpart12.fk_r_1"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ id | integer | | not null |
+ p_id | integer | | not null |
+ p_jd | integer | | not null |
+Partition of: fk_r FOR VALUES IN (1)
+Indexes:
+ "fk_r_1_pkey" PRIMARY KEY, btree (id)
+Foreign-key constraints:
+ TABLE "fk_r" CONSTRAINT "fk_r_p_id_p_jd_fkey" FOREIGN KEY (p_id, p_jd) REFERENCES fk_p(id, jd)
+
+SELECT * FROM fkpart12_constraints();
+ conname | conrel | confrelid | conparent
+----------------------+--------+-----------+----------------------
+ fk_r_p_id_p_jd_fkey | fk_r | fk_p |
+ fk_r_p_id_p_jd_fkey1 | fk_r | fk_p_1 | fk_r_p_id_p_jd_fkey
+ fk_r_p_id_p_jd_fkey2 | fk_r | fk_p_1_1 | fk_r_p_id_p_jd_fkey1
+ fk_r_p_id_p_jd_fkey3 | fk_r | fk_p_1_2 | fk_r_p_id_p_jd_fkey1
+ fk_r_p_id_p_jd_fkey4 | fk_r | fk_p_2 | fk_r_p_id_p_jd_fkey
+ fk_r_p_id_p_jd_fkey5 | fk_r | fk_p_2_1 | fk_r_p_id_p_jd_fkey4
+ fk_r_p_id_p_jd_fkey6 | fk_r | fk_p_2_2 | fk_r_p_id_p_jd_fkey4
+ fk_r_p_id_p_jd_fkey | fk_r_1 | fk_p | fk_r_p_id_p_jd_fkey
+ fk_r_p_id_p_jd_fkey | fk_r_2 | fk_p | fk_r_p_id_p_jd_fkey
+(9 rows)
+
+SELECT * FROM fkpart12_triggers();
+ conname | conparent | tgfoid | tgfparent
+----------------------+----------------------+------------------------+------------------------
+ fk_r_p_id_p_jd_fkey | fk_r_p_id_p_jd_fkey | "RI_FKey_check_ins" | "RI_FKey_check_ins"
+ fk_r_p_id_p_jd_fkey | fk_r_p_id_p_jd_fkey | "RI_FKey_check_ins" | "RI_FKey_check_ins"
+ fk_r_p_id_p_jd_fkey | fk_r_p_id_p_jd_fkey | "RI_FKey_check_upd" | "RI_FKey_check_upd"
+ fk_r_p_id_p_jd_fkey | fk_r_p_id_p_jd_fkey | "RI_FKey_check_upd" | "RI_FKey_check_upd"
+ fk_r_p_id_p_jd_fkey | | "RI_FKey_check_ins" |
+ fk_r_p_id_p_jd_fkey | | "RI_FKey_check_upd" |
+ fk_r_p_id_p_jd_fkey | | "RI_FKey_noaction_del" |
+ fk_r_p_id_p_jd_fkey | | "RI_FKey_noaction_upd" |
+ fk_r_p_id_p_jd_fkey1 | fk_r_p_id_p_jd_fkey | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey1 | fk_r_p_id_p_jd_fkey | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey2 | fk_r_p_id_p_jd_fkey1 | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey2 | fk_r_p_id_p_jd_fkey1 | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey3 | fk_r_p_id_p_jd_fkey1 | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey3 | fk_r_p_id_p_jd_fkey1 | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey4 | fk_r_p_id_p_jd_fkey | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey4 | fk_r_p_id_p_jd_fkey | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey5 | fk_r_p_id_p_jd_fkey4 | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey5 | fk_r_p_id_p_jd_fkey4 | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey6 | fk_r_p_id_p_jd_fkey4 | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey6 | fk_r_p_id_p_jd_fkey4 | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+(20 rows)
+
+INSERT INTO fk_r VALUES (1, 1, 1);
+ALTER TABLE fk_r DETACH PARTITION fk_r_1;
+\d fk_r_1
+ Table "fkpart12.fk_r_1"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ id | integer | | not null |
+ p_id | integer | | not null |
+ p_jd | integer | | not null |
+Indexes:
+ "fk_r_1_pkey" PRIMARY KEY, btree (id)
+Foreign-key constraints:
+ "fk_r_p_id_p_jd_fkey" FOREIGN KEY (p_id, p_jd) REFERENCES fk_p(id, jd)
+
+SELECT * FROM fkpart12_constraints();
+ conname | conrel | confrelid | conparent
+----------------------+--------+-----------+----------------------
+ fk_r_p_id_p_jd_fkey | fk_r | fk_p |
+ fk_r_p_id_p_jd_fkey1 | fk_r | fk_p_1 | fk_r_p_id_p_jd_fkey
+ fk_r_p_id_p_jd_fkey2 | fk_r | fk_p_1_1 | fk_r_p_id_p_jd_fkey1
+ fk_r_p_id_p_jd_fkey3 | fk_r | fk_p_1_2 | fk_r_p_id_p_jd_fkey1
+ fk_r_p_id_p_jd_fkey4 | fk_r | fk_p_2 | fk_r_p_id_p_jd_fkey
+ fk_r_p_id_p_jd_fkey5 | fk_r | fk_p_2_1 | fk_r_p_id_p_jd_fkey4
+ fk_r_p_id_p_jd_fkey6 | fk_r | fk_p_2_2 | fk_r_p_id_p_jd_fkey4
+ fk_r_p_id_p_jd_fkey | fk_r_2 | fk_p | fk_r_p_id_p_jd_fkey
+(8 rows)
+
+SELECT * FROM fkpart12_triggers();
+ conname | conparent | tgfoid | tgfparent
+----------------------+----------------------+------------------------+------------------------
+ fk_r_p_id_p_jd_fkey | fk_r_p_id_p_jd_fkey | "RI_FKey_check_ins" | "RI_FKey_check_ins"
+ fk_r_p_id_p_jd_fkey | fk_r_p_id_p_jd_fkey | "RI_FKey_check_upd" | "RI_FKey_check_upd"
+ fk_r_p_id_p_jd_fkey | | "RI_FKey_check_ins" |
+ fk_r_p_id_p_jd_fkey | | "RI_FKey_check_upd" |
+ fk_r_p_id_p_jd_fkey | | "RI_FKey_noaction_del" |
+ fk_r_p_id_p_jd_fkey | | "RI_FKey_noaction_upd" |
+ fk_r_p_id_p_jd_fkey1 | fk_r_p_id_p_jd_fkey | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey1 | fk_r_p_id_p_jd_fkey | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey2 | fk_r_p_id_p_jd_fkey1 | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey2 | fk_r_p_id_p_jd_fkey1 | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey3 | fk_r_p_id_p_jd_fkey1 | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey3 | fk_r_p_id_p_jd_fkey1 | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey4 | fk_r_p_id_p_jd_fkey | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey4 | fk_r_p_id_p_jd_fkey | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey5 | fk_r_p_id_p_jd_fkey4 | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey5 | fk_r_p_id_p_jd_fkey4 | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey6 | fk_r_p_id_p_jd_fkey4 | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey6 | fk_r_p_id_p_jd_fkey4 | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+(18 rows)
+
+INSERT INTO fk_r_1 VALUES (2, 1, 2); -- fails as EXPECTED
+ERROR: insert or update on table "fk_r_1" violates foreign key constraint "fk_r_p_id_p_jd_fkey"
+DETAIL: Key (p_id, p_jd)=(1, 2) is not present in table "fk_p".
+DELETE FROM fk_p; -- should fail but was buggy
+ERROR: update or delete on table "fk_p_1_1" violates foreign key constraint "fk_r_1_p_id_p_jd_fkey1" on table "fk_r_1"
+DETAIL: Key (id, jd)=(1, 1) is still referenced from table "fk_r_1".
+ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1);
+\d fk_r_1
+ Table "fkpart12.fk_r_1"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ id | integer | | not null |
+ p_id | integer | | not null |
+ p_jd | integer | | not null |
+Partition of: fk_r FOR VALUES IN (1)
+Indexes:
+ "fk_r_1_pkey" PRIMARY KEY, btree (id)
+Foreign-key constraints:
+ TABLE "fk_r" CONSTRAINT "fk_r_p_id_p_jd_fkey" FOREIGN KEY (p_id, p_jd) REFERENCES fk_p(id, jd)
+
+SELECT * FROM fkpart12_constraints();
+ conname | conrel | confrelid | conparent
+----------------------+--------+-----------+----------------------
+ fk_r_p_id_p_jd_fkey | fk_r | fk_p |
+ fk_r_p_id_p_jd_fkey1 | fk_r | fk_p_1 | fk_r_p_id_p_jd_fkey
+ fk_r_p_id_p_jd_fkey2 | fk_r | fk_p_1_1 | fk_r_p_id_p_jd_fkey1
+ fk_r_p_id_p_jd_fkey3 | fk_r | fk_p_1_2 | fk_r_p_id_p_jd_fkey1
+ fk_r_p_id_p_jd_fkey4 | fk_r | fk_p_2 | fk_r_p_id_p_jd_fkey
+ fk_r_p_id_p_jd_fkey5 | fk_r | fk_p_2_1 | fk_r_p_id_p_jd_fkey4
+ fk_r_p_id_p_jd_fkey6 | fk_r | fk_p_2_2 | fk_r_p_id_p_jd_fkey4
+ fk_r_p_id_p_jd_fkey | fk_r_1 | fk_p | fk_r_p_id_p_jd_fkey
+ fk_r_p_id_p_jd_fkey | fk_r_2 | fk_p | fk_r_p_id_p_jd_fkey
+(9 rows)
+
+SELECT * FROM fkpart12_triggers();
+ conname | conparent | tgfoid | tgfparent
+----------------------+----------------------+------------------------+------------------------
+ fk_r_p_id_p_jd_fkey | fk_r_p_id_p_jd_fkey | "RI_FKey_check_ins" | "RI_FKey_check_ins"
+ fk_r_p_id_p_jd_fkey | fk_r_p_id_p_jd_fkey | "RI_FKey_check_ins" | "RI_FKey_check_ins"
+ fk_r_p_id_p_jd_fkey | fk_r_p_id_p_jd_fkey | "RI_FKey_check_upd" | "RI_FKey_check_upd"
+ fk_r_p_id_p_jd_fkey | fk_r_p_id_p_jd_fkey | "RI_FKey_check_upd" | "RI_FKey_check_upd"
+ fk_r_p_id_p_jd_fkey | | "RI_FKey_check_ins" |
+ fk_r_p_id_p_jd_fkey | | "RI_FKey_check_upd" |
+ fk_r_p_id_p_jd_fkey | | "RI_FKey_noaction_del" |
+ fk_r_p_id_p_jd_fkey | | "RI_FKey_noaction_upd" |
+ fk_r_p_id_p_jd_fkey1 | fk_r_p_id_p_jd_fkey | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey1 | fk_r_p_id_p_jd_fkey | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey2 | fk_r_p_id_p_jd_fkey1 | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey2 | fk_r_p_id_p_jd_fkey1 | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey3 | fk_r_p_id_p_jd_fkey1 | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey3 | fk_r_p_id_p_jd_fkey1 | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey4 | fk_r_p_id_p_jd_fkey | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey4 | fk_r_p_id_p_jd_fkey | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey5 | fk_r_p_id_p_jd_fkey4 | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey5 | fk_r_p_id_p_jd_fkey4 | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+ fk_r_p_id_p_jd_fkey6 | fk_r_p_id_p_jd_fkey4 | "RI_FKey_noaction_del" | "RI_FKey_noaction_del"
+ fk_r_p_id_p_jd_fkey6 | fk_r_p_id_p_jd_fkey4 | "RI_FKey_noaction_upd" | "RI_FKey_noaction_upd"
+(20 rows)
+
+DELETE FROM fk_p; -- should fail but was buggy
+ERROR: update or delete on table "fk_p_1_1" violates foreign key constraint "fk_r_p_id_p_jd_fkey2" on table "fk_r"
+DETAIL: Key (id, jd)=(1, 1) is still referenced from table "fk_r".
+SET client_min_messages TO warning;
+DROP SCHEMA fkpart12 CASCADE;
+RESET client_min_messages;
+RESET search_path;
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index bb39c9719a..f922e32594 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -2086,3 +2086,104 @@ UPDATE fkpart11.pk SET a = 3 WHERE a = 4;
UPDATE fkpart11.pk SET a = 1 WHERE a = 2;
DROP SCHEMA fkpart11 CASCADE;
+
+-- When a table is attached as partition to a partitioned table that has
+-- a foreign key to another partitioned table, it acquires a clone of the
+-- FK. Upon detach, this clone is not removed, but instead becomes an
+-- independent FK. If it then attaches to the partitioned table again,
+-- the FK from the parent "takes over" ownership of the independent FK rather
+-- than creating a separate one.
+CREATE SCHEMA fkpart12
+ CREATE TABLE fk_p ( id int, jd int, PRIMARY KEY(id, jd)) PARTITION BY list (id)
+ CREATE TABLE fk_p_1 PARTITION OF fk_p FOR VALUES IN (1) PARTITION BY list (jd)
+ CREATE TABLE fk_p_1_1 PARTITION OF fk_p_1 FOR VALUES IN (1)
+ CREATE TABLE fk_p_1_2 PARTITION OF fk_p_1 FOR VALUES IN (2)
+ CREATE TABLE fk_p_2 PARTITION OF fk_p FOR VALUES IN (2) PARTITION BY list (jd)
+ CREATE TABLE fk_p_2_1 PARTITION OF fk_p_2 FOR VALUES IN (1)
+ CREATE TABLE fk_p_2_2 PARTITION OF fk_p_2 FOR VALUES IN (2)
+ CREATE TABLE fk_r_1 ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL)
+ CREATE TABLE fk_r_2 ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL)
+ CREATE TABLE fk_r ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL,
+ FOREIGN KEY (p_id, p_jd) REFERENCES fk_p (id, jd)
+ ) PARTITION BY list (id);
+SET search_path TO fkpart12;
+
+CREATE OR REPLACE FUNCTION
+fkpart12_constraints(OUT conname name, OUT conrel regclass,
+ OUT confrelid regclass, OUT conparent name) RETURNS SETOF record AS $$
+ WITH RECURSIVE r AS (
+ SELECT oid, conname, conrelid, confrelid, NULL::name AS conparent
+ FROM pg_constraint
+ WHERE conrelid = 'fk_r'::regclass
+ AND contype = 'f'
+ AND conparentid = 0
+ UNION ALL
+ SELECT c.oid, c.conname, c.conrelid, c.confrelid, r.conname
+ FROM pg_constraint c
+ JOIN r ON c.conparentid = r.oid
+ )
+ SELECT conname, conrelid::regclass, confrelid::regclass, conparent
+ FROM r
+ ORDER BY oid, conname, confrelid, conparent;
+$$
+LANGUAGE SQL;
+
+CREATE OR REPLACE FUNCTION
+fkpart12_triggers(OUT conname name, OUT conparent name,
+ OUT tgfoid regproc, OUT tgfparent regproc) RETURNS SETOF record AS $$
+
+ WITH RECURSIVE r AS (
+ SELECT t.oid, t.tgfoid::regproc, c.conname, NULL::name AS conparent, NULL::regproc AS tgfparent
+ FROM pg_trigger t
+ JOIN pg_constraint c ON c.oid = t.tgconstraint
+ WHERE c.conrelid = 'fk_r'::regclass
+ AND c.contype = 'f'
+ AND t.tgparentid = 0
+ UNION ALL
+ SELECT t2.oid, t2.tgfoid::regproc, c2.conname, c3.conname, r.tgfoid::regproc AS tgfparent
+ FROM pg_trigger t2
+ JOIN pg_constraint c2 ON c2.oid = t2.tgconstraint
+ LEFT JOIN pg_constraint c3 ON c3.oid = c2.conparentid
+ JOIN r ON r.oid = t2.tgparentid
+ )
+ SELECT conname, conparent, tgfoid, tgfparent
+ FROM r
+ ORDER BY conname, conparent, tgfoid, tgfparent;
+$$
+LANGUAGE SQL;
+
+SELECT * FROM fkpart12_constraints();
+SELECT * FROM fkpart12_triggers();
+
+INSERT INTO fk_p VALUES (1, 1);
+
+ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1);
+ALTER TABLE fk_r ATTACH PARTITION fk_r_2 FOR VALUES IN (2);
+
+\d fk_r_1
+SELECT * FROM fkpart12_constraints();
+SELECT * FROM fkpart12_triggers();
+
+INSERT INTO fk_r VALUES (1, 1, 1);
+
+ALTER TABLE fk_r DETACH PARTITION fk_r_1;
+
+\d fk_r_1
+SELECT * FROM fkpart12_constraints();
+SELECT * FROM fkpart12_triggers();
+
+INSERT INTO fk_r_1 VALUES (2, 1, 2); -- fails as EXPECTED
+DELETE FROM fk_p; -- should fail but was buggy
+
+ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1);
+
+\d fk_r_1
+SELECT * FROM fkpart12_constraints();
+SELECT * FROM fkpart12_triggers();
+
+DELETE FROM fk_p; -- should fail but was buggy
+
+SET client_min_messages TO warning;
+DROP SCHEMA fkpart12 CASCADE;
+RESET client_min_messages;
+RESET search_path;
--
2.46.0
v3-15-0002-Rework-foreign-key-mangling-during-ATTACH-DETA.patchtext/x-patchDownload
From 1fc4bdfaaa265773d2625f3d343b3b774ead7c76 Mon Sep 17 00:00:00 2001
From: Jehan-Guillaume de Rorthais <jgdr@dalibo.com>
Date: Wed, 25 Sep 2024 15:38:42 +0200
Subject: [PATCH v3-15 2/4] Rework foreign key mangling during ATTACH/DETACH
... when the reference table is partitioned.
It turns out that the catalog representation we chose for foreign keys
connecting partitioned tables is suboptimal, particularly in the sense
that a standalone table has a different way to represent it when
referencing a partitioned table, than when the same table becomes a
partition (and vice versa). This difference means we need to spawn
additional catalog rows on detach, and remove them on attach.
As a very obvious symptom, we were missing action triggers after
detach, which means that you could update/delete rows from the
referenced partitioned table that still had referencing rows at the
other side previously detached, and fail to throw the required errors.
This means existing FKs might have rows that break relational
integrity.
To fix this bug, this patch rework addFkRecurseReferenced() so it
can be called from DetachPartitionFinalize(), so we can keep this
logic in a single function. The addFkRecurseReferenced() rework
leads to create addFkConstraint(), but it seems cleaner as
addFkRecurseReferenced() and addFkRecurseReferencing() have the same
logic now. Some other code factoring using addFkConstraint() are
added in next commits.
Another possible problem is that trying to reattach a table
that had been detached would fail indicating that internal triggers
cannot be found, which from the user's point of view is nonsensical.
We might want to rethink the representation in the future to avoid this
messiness, but the code now seems to do what's required to make the
constraints operate correctly.
co-author: Alvaro Herrera <alvherre@alvh.no-ip.org>
co-author: Jehan-Guillaume de Rorthais <jgdr@dalibo.com>
co-author: Tender Wang <tndrwang@gmail.com>
Reported-by: Guillaume Lelarge <guillaume@lelarge.info>
Reported-by: Jehan-Guillaume de Rorthais <jgdr@dalibo.com>
Reported-by: Thomas Baehler (SBB CFF FFS) <thomas.baehler2@sbb.ch>
Discussion: https://postgr.es/m/20230420144344.40744130@karst
Discussion: https://postgr.es/m/20230705233028.2f554f73@karst
Discussion: https://postgr.es/m/GVAP278MB02787E7134FD691861635A8BC9032@GVAP278MB0278.CHEP278.PROD.OUTLOOK.COM
Discussion: https://postgr.es/m/18541-628a61bc267cd2d3@postgresql.org
---
src/backend/commands/tablecmds.c | 369 ++++++++++++++++++++++++-------
1 file changed, 288 insertions(+), 81 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index f3050d8a1d..d6a3090ff4 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -489,7 +489,13 @@ static ObjectAddress ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *
Relation rel, Constraint *fkconstraint,
bool recurse, bool recursing,
LOCKMODE lockmode);
-static ObjectAddress addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint,
+static ObjectAddress addFkConstraint(Constraint *fkconstraint, Relation rel,
+ Relation pkrel, Oid indexOid, Oid parentConstr,
+ int numfks, int16 *pkattnum, int16 *fkattnum,
+ Oid *pfeqoperators, Oid *ppeqoperators,
+ Oid *ffeqoperators, int numfkdelsetcols,
+ int16 *fkdelsetcols);
+static void addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint,
Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks, int16 *pkattnum, int16 *fkattnum,
Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
@@ -625,9 +631,11 @@ static void DropClonedTriggersFromPartition(Oid partitionId);
static ObjectAddress ATExecDetachPartition(List **wqueue, AlteredTableInfo *tab,
Relation rel, RangeVar *name,
bool concurrent);
-static void DetachPartitionFinalize(Relation rel, Relation partRel,
- bool concurrent, Oid defaultPartOid);
-static ObjectAddress ATExecDetachPartitionFinalize(Relation rel, RangeVar *name);
+static void DetachPartitionFinalize(List **wqueue, Relation rel,
+ Relation partRel, bool concurrent,
+ Oid defaultPartOid);
+static ObjectAddress ATExecDetachPartitionFinalize(List **wqueue, Relation rel,
+ RangeVar *name);
static ObjectAddress ATExecAttachPartitionIdx(List **wqueue, Relation rel,
RangeVar *name);
static void validatePartitionedIndex(Relation partedIdx, Relation partedTbl);
@@ -5283,7 +5291,7 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
((PartitionCmd *) cmd->def)->concurrent);
break;
case AT_DetachPartitionFinalize:
- ATExecDetachPartitionFinalize(rel, ((PartitionCmd *) cmd->def)->name);
+ ATExecDetachPartitionFinalize(wqueue, rel, ((PartitionCmd *) cmd->def)->name);
break;
default: /* oops */
elog(ERROR, "unrecognized alter table type: %d",
@@ -9528,9 +9536,21 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
* Create all the constraint and trigger objects, recursing to partitions
* as necessary. First handle the referenced side.
*/
- address = addFkRecurseReferenced(wqueue, fkconstraint, rel, pkrel,
+ address = addFkConstraint(fkconstraint, rel, pkrel,
+ indexOid,
+ InvalidOid, /* no parent constraint */
+ numfks,
+ pkattnum,
+ fkattnum,
+ pfeqoperators,
+ ppeqoperators,
+ ffeqoperators,
+ numfkdelsetcols,
+ fkdelsetcols);
+
+ addFkRecurseReferenced(wqueue, fkconstraint, rel, pkrel,
indexOid,
- InvalidOid, /* no parent constraint */
+ address.objectId,
numfks,
pkattnum,
fkattnum,
@@ -9602,46 +9622,31 @@ validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
}
/*
- * addFkRecurseReferenced
- * subroutine for ATAddForeignKeyConstraint; recurses on the referenced
- * side of the constraint
+ * addFkConstraint
+ * Add a new Foreign Key constraint.
*
- * Create pg_constraint rows for the referenced side of the constraint,
- * referencing the parent of the referencing side; also create action triggers
- * on leaf partitions. If the table is partitioned, recurse to handle each
- * partition.
+ * Creates a pg_constraint rows referencing a parent if given.
*
- * wqueue is the ALTER TABLE work queue; can be NULL when not running as part
- * of an ALTER TABLE sequence.
- * fkconstraint is the constraint being added.
- * rel is the root referencing relation.
- * pkrel is the referenced relation; might be a partition, if recursing.
- * indexOid is the OID of the index (on pkrel) implementing this constraint.
- * parentConstr is the OID of a parent constraint; InvalidOid if this is a
- * top-level constraint.
- * numfks is the number of columns in the foreign key
- * pkattnum is the attnum array of referenced attributes.
- * fkattnum is the attnum array of referencing attributes.
- * numfkdelsetcols is the number of columns in the ON DELETE SET NULL/DEFAULT
- * (...) clause
- * fkdelsetcols is the attnum array of the columns in the ON DELETE SET
- * NULL/DEFAULT clause
- * pf/pp/ffeqoperators are OID array of operators between columns.
- * old_check_ok signals that this constraint replaces an existing one that
- * was already validated (thus this one doesn't need validation).
- * parentDelTrigger and parentUpdTrigger, when being recursively called on
- * a partition, are the OIDs of the parent action triggers for DELETE and
- * UPDATE respectively.
+ * * fkconstraint is the constraint being added.
+ * * rel is the root referencing relation.
+ * * pkrel is the referenced relation; might be a partition, if recursing.
+ * * indexOid is the OID of the index (on pkrel) implementing this constraint.
+ * * parentConstr is the OID of a parent constraint; InvalidOid if this is a
+ * top-level constraint.
+ * * numfks is the number of columns in the foreign key.
+ * * pkattnum is the attnum array of referenced attributes.
+ * * fkattnum is the attnum array of referencing attributes.
+ * * pf/pp/ffeqoperators are OID array of operators between columns.
+ * * numfkdelsetcols is the number of columns in the ON DELETE SET NULL/DEFAULT
+ * (...) clause.
+ * * fkdelsetcols is the attnum array of the columns in the ON DELETE SET
+ * NULL/DEFAULT clause.
*/
static ObjectAddress
-addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
- Relation pkrel, Oid indexOid, Oid parentConstr,
- int numfks,
- int16 *pkattnum, int16 *fkattnum, Oid *pfeqoperators,
- Oid *ppeqoperators, Oid *ffeqoperators,
- int numfkdelsetcols, int16 *fkdelsetcols,
- bool old_check_ok,
- Oid parentDelTrigger, Oid parentUpdTrigger)
+addFkConstraint(Constraint *fkconstraint, Relation rel, Relation pkrel,
+ Oid indexOid, Oid parentConstr, int numfks, int16 *pkattnum,
+ int16 *fkattnum, Oid *pfeqoperators, Oid *ppeqoperators,
+ Oid *ffeqoperators, int numfkdelsetcols, int16 *fkdelsetcols)
{
ObjectAddress address;
Oid constrOid;
@@ -9649,8 +9654,6 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
bool conislocal;
int coninhcount;
bool connoinherit;
- Oid deleteTriggerOid,
- updateTriggerOid;
/*
* Verify relkind for each referenced partition. At the top level, this
@@ -9748,12 +9751,61 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
/* make new constraint visible, in case we add more */
CommandCounterIncrement();
+ return address;
+}
+
+/*
+ * addFkRecurseReferenced
+ * subroutine for ATAddForeignKeyConstraint; recurses on the referenced
+ * side of the constraint
+ *
+ * If the referenced relation is a plain relation, create the necessary check
+ * triggers that implement the constraint. If the referenced relation is a
+ * partitioned table, then we create a pg_constraint row referencing the parent
+ * of the referencing side for it and recurse on this routine for each
+ * partition.
+ *
+ * wqueue is the ALTER TABLE work queue; can be NULL when not running as part
+ * of an ALTER TABLE sequence.
+ * fkconstraint is the constraint being added.
+ * rel is the root referencing relation.
+ * pkrel is the referenced relation; might be a partition, if recursing.
+ * indexOid is the OID of the index (on pkrel) implementing this constraint.
+ * parentConstr is the OID of a parent constraint; InvalidOid if this is a
+ * top-level constraint.
+ * numfks is the number of columns in the foreign key
+ * pkattnum is the attnum array of referenced attributes.
+ * fkattnum is the attnum array of referencing attributes.
+ * numfkdelsetcols is the number of columns in the ON DELETE SET NULL/DEFAULT
+ * (...) clause
+ * fkdelsetcols is the attnum array of the columns in the ON DELETE SET
+ * NULL/DEFAULT clause
+ * pf/pp/ffeqoperators are OID array of operators between columns.
+ * old_check_ok signals that this constraint replaces an existing one that
+ * was already validated (thus this one doesn't need validation).
+ * parentDelTrigger and parentUpdTrigger, when being recursively called on
+ * a partition, are the OIDs of the parent action triggers for DELETE and
+ * UPDATE respectively.
+ */
+static void
+addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
+ Relation pkrel, Oid indexOid, Oid parentConstr,
+ int numfks,
+ int16 *pkattnum, int16 *fkattnum, Oid *pfeqoperators,
+ Oid *ppeqoperators, Oid *ffeqoperators,
+ int numfkdelsetcols, int16 *fkdelsetcols,
+ bool old_check_ok,
+ Oid parentDelTrigger, Oid parentUpdTrigger)
+{
+ Oid deleteTriggerOid,
+ updateTriggerOid;
+
/*
* Create the action triggers that enforce the constraint.
*/
createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
fkconstraint,
- constrOid, indexOid,
+ parentConstr, indexOid,
parentDelTrigger, parentUpdTrigger,
&deleteTriggerOid, &updateTriggerOid);
@@ -9772,6 +9824,7 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
AttrMap *map;
AttrNumber *mapped_pkattnum;
Oid partIndexId;
+ ObjectAddress address;
partRel = table_open(pd->oids[i], ShareRowExclusiveLock);
@@ -9795,8 +9848,14 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
if (!OidIsValid(partIndexId))
elog(ERROR, "index for %u not found in partition %s",
indexOid, RelationGetRelationName(partRel));
+
+ address = addFkConstraint(fkconstraint, rel, partRel,
+ partIndexId, parentConstr, numfks,
+ mapped_pkattnum, fkattnum,
+ pfeqoperators, ppeqoperators, ffeqoperators,
+ numfkdelsetcols, fkdelsetcols);
addFkRecurseReferenced(wqueue, fkconstraint, rel, partRel,
- partIndexId, constrOid, numfks,
+ partIndexId, address.objectId, numfks,
mapped_pkattnum, fkattnum,
pfeqoperators, ppeqoperators, ffeqoperators,
numfkdelsetcols, fkdelsetcols,
@@ -9812,8 +9871,6 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
}
}
}
-
- return address;
}
/*
@@ -10173,6 +10230,7 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
int numfkdelsetcols;
AttrNumber confdelsetcols[INDEX_MAX_KEYS];
Constraint *fkconstraint;
+ ObjectAddress address;
Oid deleteTriggerOid,
updateTriggerOid;
@@ -10206,7 +10264,7 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
* Because we're only expanding the key space at the referenced side,
* we don't need to prevent any operation in the referencing table, so
* AccessShareLock suffices (assumes that dropping the constraint
- * acquires AEL).
+ * acquires AccessExclusiveLock).
*/
fkRel = table_open(constrForm->conrelid, AccessShareLock);
@@ -10272,12 +10330,16 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
constrForm->confrelid, constrForm->conrelid,
&deleteTriggerOid, &updateTriggerOid);
+ address = addFkConstraint(fkconstraint, fkRel, partitionRel, partIndexId,
+ constrOid, numfks, mapped_confkey, conkey,
+ conpfeqop, conppeqop, conffeqop,
+ numfkdelsetcols, confdelsetcols);
addFkRecurseReferenced(NULL,
fkconstraint,
fkRel,
partitionRel,
partIndexId,
- constrOid,
+ address.objectId,
numfks,
mapped_confkey,
conkey,
@@ -10728,6 +10790,83 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
partRelid);
+ /*
+ * If the referenced table is partitioned, then the partition we're
+ * attaching now has extra pg_constraint rows and action triggers that are
+ * no longer needed. Remove those.
+ */
+ if (get_rel_relkind(fk->confrelid) == RELKIND_PARTITIONED_TABLE)
+ {
+ Relation pg_constraint = table_open(ConstraintRelationId, RowShareLock);
+ ObjectAddresses *objs;
+ HeapTuple consttup;
+
+ ScanKeyInit(&key,
+ Anum_pg_constraint_conrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(fk->conrelid));
+
+ scan = systable_beginscan(pg_constraint,
+ ConstraintRelidTypidNameIndexId,
+ true, NULL, 1, &key);
+ objs = new_object_addresses();
+ while ((consttup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_constraint conform = (Form_pg_constraint) GETSTRUCT(consttup);
+
+ if (conform->conparentid != fk->conoid)
+ continue;
+ else
+ {
+ ObjectAddress addr;
+ int n;
+ SysScanDesc scan2;
+ ScanKeyData key2;
+
+ ObjectAddressSet(addr, ConstraintRelationId, conform->oid);
+ add_exact_object_address(&addr, objs);
+
+ /*
+ * First we must delete the dependency records that bind
+ * the constraint records together.
+ */
+ n = deleteDependencyRecordsForSpecific(ConstraintRelationId,
+ conform->oid,
+ DEPENDENCY_INTERNAL,
+ ConstraintRelationId,
+ fk->conoid);
+ if (n != 1)
+ elog(WARNING, "oops: found %d instead of 1 deps from %u to %u",
+ n, conform->oid, fk->conoid);
+
+ /*
+ * Now search for the triggers for this constraint and set
+ * them up for deletion too
+ */
+ ScanKeyInit(&key2,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conform->oid));
+ scan2 = systable_beginscan(trigrel, TriggerConstraintIndexId,
+ true, NULL, 1, &key2);
+ while ((trigtup = systable_getnext(scan2)) != NULL)
+ {
+ ObjectAddressSet(addr, TriggerRelationId,
+ ((Form_pg_trigger) GETSTRUCT(trigtup))->oid);
+ add_exact_object_address(&addr, objs);
+ }
+ systable_endscan(scan2);
+ }
+ }
+ /* make the dependency deletions visible */
+ CommandCounterIncrement();
+ performMultipleDeletions(objs, DROP_RESTRICT,
+ PERFORM_DELETION_INTERNAL);
+ systable_endscan(scan);
+
+ table_close(pg_constraint, RowShareLock);
+ }
+
CommandCounterIncrement();
return true;
}
@@ -18674,7 +18813,7 @@ ATExecDetachPartition(List **wqueue, AlteredTableInfo *tab, Relation rel,
}
/* Do the final part of detaching */
- DetachPartitionFinalize(rel, partRel, concurrent, defaultPartOid);
+ DetachPartitionFinalize(wqueue, rel, partRel, concurrent, defaultPartOid);
ObjectAddressSet(address, RelationRelationId, RelationGetRelid(partRel));
@@ -18691,8 +18830,8 @@ ATExecDetachPartition(List **wqueue, AlteredTableInfo *tab, Relation rel,
* transaction of the concurrent algorithm fails (crash or abort).
*/
static void
-DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
- Oid defaultPartOid)
+DetachPartitionFinalize(List **wqueue, Relation rel, Relation partRel,
+ bool concurrent, Oid defaultPartOid)
{
Relation classRel;
List *fks;
@@ -18728,8 +18867,11 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
{
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
+ HeapTuple parentConTup;
Form_pg_constraint conform;
+ Form_pg_constraint parentConForm;
Constraint *fkconstraint;
+ Oid parentConstrOid;
Oid insertTriggerOid,
updateTriggerOid;
@@ -18746,7 +18888,20 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
continue;
}
- /* unset conparentid and adjust conislocal, coninhcount, etc. */
+ parentConstrOid = conform->conparentid;
+
+ Assert(OidIsValid(conform->conparentid));
+ parentConTup = SearchSysCache1(CONSTROID,
+ ObjectIdGetDatum(parentConstrOid));
+ if (!HeapTupleIsValid(parentConTup))
+ elog(ERROR, "cache lookup failed for constraint %u",
+ conform->conparentid);
+ parentConForm = (Form_pg_constraint) GETSTRUCT(parentConTup);
+
+ /*
+ * The constraint on this table must be marked no longer a child of
+ * the parent's constraint, as do its check triggers.
+ */
ConstraintSetParentConstraint(fk->conoid, InvalidOid, InvalidOid);
/*
@@ -18764,35 +18919,87 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
RelationGetRelid(partRel));
/*
- * Make the action triggers on the referenced relation. When this was
- * a partition the action triggers pointed to the parent rel (they
- * still do), but now we need separate ones of our own.
+ * If the referenced side is partitioned (which we know because our
+ * parent's constraint points to a different relation than ours) then
+ * we must, in addition to the above, create pg_constraint rows that
+ * point to each partition, each with its own action triggers.
*/
- fkconstraint = makeNode(Constraint);
- fkconstraint->contype = CONSTRAINT_FOREIGN;
- fkconstraint->conname = pstrdup(NameStr(conform->conname));
- fkconstraint->deferrable = conform->condeferrable;
- fkconstraint->initdeferred = conform->condeferred;
- fkconstraint->location = -1;
- fkconstraint->pktable = NULL;
- fkconstraint->fk_attrs = NIL;
- fkconstraint->pk_attrs = NIL;
- fkconstraint->fk_matchtype = conform->confmatchtype;
- fkconstraint->fk_upd_action = conform->confupdtype;
- fkconstraint->fk_del_action = conform->confdeltype;
- fkconstraint->fk_del_set_cols = NIL;
- fkconstraint->old_conpfeqop = NIL;
- fkconstraint->old_pktable_oid = InvalidOid;
- fkconstraint->skip_validation = false;
- fkconstraint->initially_valid = true;
+ if (parentConForm->conrelid != conform->conrelid)
+ {
+ int numfks;
+ AttrNumber conkey[INDEX_MAX_KEYS];
+ AttrMap *attmap;
+ AttrNumber confkey[INDEX_MAX_KEYS];
+ Oid conpfeqop[INDEX_MAX_KEYS];
+ Oid conppeqop[INDEX_MAX_KEYS];
+ Oid conffeqop[INDEX_MAX_KEYS];
+ int numfkdelsetcols;
+ AttrNumber confdelsetcols[INDEX_MAX_KEYS];
+ Relation refdRel;
+
+ DeconstructFkConstraintRow(contup,
+ &numfks,
+ conkey,
+ confkey,
+ conpfeqop,
+ conppeqop,
+ conffeqop,
+ &numfkdelsetcols,
+ confdelsetcols);
+
+ /* Create a synthetic node we'll use throughout */
+ fkconstraint = makeNode(Constraint);
+ fkconstraint->contype = CONSTRAINT_FOREIGN;
+ fkconstraint->conname = pstrdup(NameStr(conform->conname));
+ fkconstraint->deferrable = conform->condeferrable;
+ fkconstraint->initdeferred = conform->condeferred;
+ fkconstraint->skip_validation = true;
+ fkconstraint->initially_valid = true;
+ /* a few irrelevant fields omitted here */
+ fkconstraint->pktable = NULL;
+ fkconstraint->fk_attrs = NIL;
+ fkconstraint->pk_attrs = NIL;
+ fkconstraint->fk_matchtype = conform->confmatchtype;
+ fkconstraint->fk_upd_action = conform->confupdtype;
+ fkconstraint->fk_del_action = conform->confdeltype;
+ fkconstraint->fk_del_set_cols = NIL;
+ fkconstraint->old_conpfeqop = NIL;
+ fkconstraint->old_pktable_oid = InvalidOid;
+ fkconstraint->location = -1;
+
+ attmap = build_attrmap_by_name(RelationGetDescr(partRel),
+ RelationGetDescr(rel));
+ for (int i = 0; i < numfks; i++)
+ {
+ Form_pg_attribute att;
- createForeignKeyActionTriggers(partRel, conform->confrelid,
- fkconstraint, fk->conoid,
+ att = TupleDescAttr(RelationGetDescr(partRel),
+ attmap->attnums[conkey[i] - 1] - 1);
+ fkconstraint->fk_attrs = lappend(fkconstraint->fk_attrs,
+ makeString(NameStr(att->attname)));
+ }
+
+ refdRel = table_open(fk->confrelid, AccessShareLock);
+
+ addFkRecurseReferenced(wqueue, fkconstraint, partRel,
+ refdRel,
conform->conindid,
- InvalidOid, InvalidOid,
- NULL, NULL);
+ fk->conoid,
+ numfks,
+ confkey,
+ conkey,
+ conpfeqop,
+ conppeqop,
+ conffeqop,
+ numfkdelsetcols,
+ confdelsetcols,
+ true,
+ InvalidOid, InvalidOid);
+ table_close(refdRel, AccessShareLock);
+ }
ReleaseSysCache(contup);
+ ReleaseSysCache(parentConTup);
}
list_free_deep(fks);
if (trigrel)
@@ -18927,7 +19134,7 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
* completion; this completes the detaching process.
*/
static ObjectAddress
-ATExecDetachPartitionFinalize(Relation rel, RangeVar *name)
+ATExecDetachPartitionFinalize(List **wqueue, Relation rel, RangeVar *name)
{
Relation partRel;
ObjectAddress address;
@@ -18945,7 +19152,7 @@ ATExecDetachPartitionFinalize(Relation rel, RangeVar *name)
*/
WaitForOlderSnapshots(snap->xmin, false);
- DetachPartitionFinalize(rel, partRel, true, InvalidOid);
+ DetachPartitionFinalize(wqueue, rel, partRel, true, InvalidOid);
ObjectAddressSet(address, RelationRelationId, RelationGetRelid(partRel));
--
2.46.0
v3-15-0003-Use-addFkConstraint-in-addFkRecurseReferencing.patchtext/x-patchDownload
From 4ccec0ce346135e129243139e04c6927f137b5ef Mon Sep 17 00:00:00 2001
From: Jehan-Guillaume de Rorthais <jgdr@dalibo.com>
Date: Tue, 24 Sep 2024 18:10:33 +0200
Subject: [PATCH v3-15 3/4] Use addFkConstraint() in addFkRecurseReferencing()
---
src/backend/commands/tablecmds.c | 121 ++++++++++++-------------------
1 file changed, 48 insertions(+), 73 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d6a3090ff4..53b5b59f69 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -335,6 +335,14 @@ typedef struct ForeignTruncateInfo
List *rels;
} ForeignTruncateInfo;
+typedef enum FKConstraintSide
+{
+ FK_BOTH_SIDE,
+ FK_REFERENCED_SIDE,
+ FK_REFERENCING_SIDE
+}
+FKConstraintSide;
+
/*
* Partition tables are expected to be dropped when the parent partitioned
* table gets dropped. Hence for partitioning we use AUTO dependency.
@@ -490,7 +498,8 @@ static ObjectAddress ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *
bool recurse, bool recursing,
LOCKMODE lockmode);
static ObjectAddress addFkConstraint(Constraint *fkconstraint, Relation rel,
- Relation pkrel, Oid indexOid, Oid parentConstr,
+ Relation pkrel, Oid indexOid,
+ Oid parentConstr, FKConstraintSide fkside,
int numfks, int16 *pkattnum, int16 *fkattnum,
Oid *pfeqoperators, Oid *ppeqoperators,
Oid *ffeqoperators, int numfkdelsetcols,
@@ -9539,6 +9548,7 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
address = addFkConstraint(fkconstraint, rel, pkrel,
indexOid,
InvalidOid, /* no parent constraint */
+ FK_BOTH_SIDE,
numfks,
pkattnum,
fkattnum,
@@ -9633,6 +9643,7 @@ validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
* * indexOid is the OID of the index (on pkrel) implementing this constraint.
* * parentConstr is the OID of a parent constraint; InvalidOid if this is a
* top-level constraint.
+ * * fkside is the side of FK related to this inherited constraint.
* * numfks is the number of columns in the foreign key.
* * pkattnum is the attnum array of referenced attributes.
* * fkattnum is the attnum array of referencing attributes.
@@ -9644,8 +9655,9 @@ validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
*/
static ObjectAddress
addFkConstraint(Constraint *fkconstraint, Relation rel, Relation pkrel,
- Oid indexOid, Oid parentConstr, int numfks, int16 *pkattnum,
- int16 *fkattnum, Oid *pfeqoperators, Oid *ppeqoperators,
+ Oid indexOid, Oid parentConstr, FKConstraintSide fkside,
+ int numfks, int16 *pkattnum, int16 *fkattnum,
+ Oid *pfeqoperators, Oid *ppeqoperators,
Oid *ffeqoperators, int numfkdelsetcols, int16 *fkdelsetcols)
{
ObjectAddress address;
@@ -9734,18 +9746,31 @@ addFkConstraint(Constraint *fkconstraint, Relation rel, Relation pkrel,
ObjectAddressSet(address, ConstraintRelationId, constrOid);
- /*
- * Mark the child constraint as part of the parent constraint; it must not
- * be dropped on its own. (This constraint is deleted when the partition
- * is detached, but a special check needs to occur that the partition
- * contains no referenced values.)
- */
if (OidIsValid(parentConstr))
{
ObjectAddress referenced;
ObjectAddressSet(referenced, ConstraintRelationId, parentConstr);
- recordDependencyOn(&address, &referenced, DEPENDENCY_INTERNAL);
+ if (fkside == FK_REFERENCED_SIDE)
+ {
+ /*
+ * Mark the child constraint as part of the parent constraint; it must not
+ * be dropped on its own. (This constraint is deleted when the partition
+ * is detached, but a special check needs to occur that the partition
+ * contains no referenced values.)
+ */
+ recordDependencyOn(&address, &referenced, DEPENDENCY_INTERNAL);
+ }
+ else
+ {
+ /*
+ * Give this constraint partition-type dependencies on the parent
+ * constraint as well as the table.
+ */
+ recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_PRI);
+ ObjectAddressSet(referenced, RelationRelationId, RelationGetRelid(rel));
+ recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_SEC);
+ }
}
/* make new constraint visible, in case we add more */
@@ -9850,8 +9875,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
indexOid, RelationGetRelationName(partRel));
address = addFkConstraint(fkconstraint, rel, partRel,
- partIndexId, parentConstr, numfks,
- mapped_pkattnum, fkattnum,
+ partIndexId, parentConstr, FK_REFERENCED_SIDE,
+ numfks, mapped_pkattnum, fkattnum,
pfeqoperators, ppeqoperators, ffeqoperators,
numfkdelsetcols, fkdelsetcols);
addFkRecurseReferenced(wqueue, fkconstraint, rel, partRel,
@@ -9995,10 +10020,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
AttrMap *attmap;
AttrNumber mapped_fkattnum[INDEX_MAX_KEYS];
bool attached;
- char *conname;
- Oid constrOid;
- ObjectAddress address,
- referenced;
+ ObjectAddress address;
ListCell *cell;
CheckAlterTableIsSafe(partition);
@@ -10040,65 +10062,18 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
/*
* No luck finding a good constraint to reuse; create our own.
*/
- if (ConstraintNameIsUsed(CONSTRAINT_RELATION,
- RelationGetRelid(partition),
- fkconstraint->conname))
- conname = ChooseConstraintName(RelationGetRelationName(partition),
- ChooseForeignKeyConstraintNameAddition(fkconstraint->fk_attrs),
- "fkey",
- RelationGetNamespace(partition), NIL);
- else
- conname = fkconstraint->conname;
- constrOid =
- CreateConstraintEntry(conname,
- RelationGetNamespace(partition),
- CONSTRAINT_FOREIGN,
- fkconstraint->deferrable,
- fkconstraint->initdeferred,
- fkconstraint->initially_valid,
- parentConstr,
- partitionId,
- mapped_fkattnum,
- numfks,
- numfks,
- InvalidOid,
- indexOid,
- RelationGetRelid(pkrel),
- pkattnum,
- pfeqoperators,
- ppeqoperators,
- ffeqoperators,
- numfks,
- fkconstraint->fk_upd_action,
- fkconstraint->fk_del_action,
- fkdelsetcols,
- numfkdelsetcols,
- fkconstraint->fk_matchtype,
- NULL,
- NULL,
- NULL,
- false,
- 1,
- false,
- false);
-
- /*
- * Give this constraint partition-type dependencies on the parent
- * constraint as well as the table.
- */
- ObjectAddressSet(address, ConstraintRelationId, constrOid);
- ObjectAddressSet(referenced, ConstraintRelationId, parentConstr);
- recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_PRI);
- ObjectAddressSet(referenced, RelationRelationId, partitionId);
- recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_SEC);
- /* Make all this visible before recursing */
- CommandCounterIncrement();
+ address = addFkConstraint(fkconstraint,
+ partition, pkrel, indexOid, parentConstr,
+ FK_REFERENCING_SIDE, numfks, pkattnum,
+ mapped_fkattnum, pfeqoperators,
+ ppeqoperators, ffeqoperators,
+ numfkdelsetcols, fkdelsetcols);
/* call ourselves to finalize the creation and we're done */
addFkRecurseReferencing(wqueue, fkconstraint, partition, pkrel,
indexOid,
- constrOid,
+ address.objectId,
numfks,
pkattnum,
mapped_fkattnum,
@@ -10331,9 +10306,9 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
&deleteTriggerOid, &updateTriggerOid);
address = addFkConstraint(fkconstraint, fkRel, partitionRel, partIndexId,
- constrOid, numfks, mapped_confkey, conkey,
- conpfeqop, conppeqop, conffeqop,
- numfkdelsetcols, confdelsetcols);
+ constrOid, FK_REFERENCED_SIDE, numfks,
+ mapped_confkey, conkey, conpfeqop, conppeqop,
+ conffeqop, numfkdelsetcols, confdelsetcols);
addFkRecurseReferenced(NULL,
fkconstraint,
fkRel,
--
2.46.0
v3-15-0004-Use-addFkConstraint-in-CloneFkReferencing.patchtext/x-patchDownload
From e543fd92bb3e15c7ba09dbabcfaaab90b6c77a91 Mon Sep 17 00:00:00 2001
From: Jehan-Guillaume de Rorthais <jgdr@dalibo.com>
Date: Wed, 25 Sep 2024 13:31:07 +0200
Subject: [PATCH v3-15 4/4] Use addFkConstraint() in CloneFkReferencing()
---
src/backend/commands/tablecmds.c | 123 +++++++++++--------------------
1 file changed, 43 insertions(+), 80 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 53b5b59f69..f03698bde5 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -497,13 +497,14 @@ static ObjectAddress ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *
Relation rel, Constraint *fkconstraint,
bool recurse, bool recursing,
LOCKMODE lockmode);
-static ObjectAddress addFkConstraint(Constraint *fkconstraint, Relation rel,
+static ObjectAddress addFkConstraint(char *constraintname,
+ Constraint *fkconstraint, Relation rel,
Relation pkrel, Oid indexOid,
Oid parentConstr, FKConstraintSide fkside,
int numfks, int16 *pkattnum, int16 *fkattnum,
Oid *pfeqoperators, Oid *ppeqoperators,
Oid *ffeqoperators, int numfkdelsetcols,
- int16 *fkdelsetcols);
+ int16 *fkdelsetcols, bool is_internal);
static void addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint,
Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks, int16 *pkattnum, int16 *fkattnum,
@@ -9545,7 +9546,7 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
* Create all the constraint and trigger objects, recursing to partitions
* as necessary. First handle the referenced side.
*/
- address = addFkConstraint(fkconstraint, rel, pkrel,
+ address = addFkConstraint(fkconstraint->conname, fkconstraint, rel, pkrel,
indexOid,
InvalidOid, /* no parent constraint */
FK_BOTH_SIDE,
@@ -9556,7 +9557,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
ppeqoperators,
ffeqoperators,
numfkdelsetcols,
- fkdelsetcols);
+ fkdelsetcols,
+ false);
addFkRecurseReferenced(wqueue, fkconstraint, rel, pkrel,
indexOid,
@@ -9637,6 +9639,8 @@ validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
*
* Creates a pg_constraint rows referencing a parent if given.
*
+ * * constraintname the base name for the constraint being added. This is
+ * copied to fkconstraint->conname if the later is not set.
* * fkconstraint is the constraint being added.
* * rel is the root referencing relation.
* * pkrel is the referenced relation; might be a partition, if recursing.
@@ -9654,11 +9658,12 @@ validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
* NULL/DEFAULT clause.
*/
static ObjectAddress
-addFkConstraint(Constraint *fkconstraint, Relation rel, Relation pkrel,
- Oid indexOid, Oid parentConstr, FKConstraintSide fkside,
- int numfks, int16 *pkattnum, int16 *fkattnum,
- Oid *pfeqoperators, Oid *ppeqoperators,
- Oid *ffeqoperators, int numfkdelsetcols, int16 *fkdelsetcols)
+addFkConstraint(char *constraintname, Constraint *fkconstraint, Relation rel,
+ Relation pkrel, Oid indexOid, Oid parentConstr,
+ FKConstraintSide fkside, int numfks, int16 *pkattnum,
+ int16 *fkattnum, Oid *pfeqoperators, Oid *ppeqoperators,
+ Oid *ffeqoperators, int numfkdelsetcols, int16 *fkdelsetcols,
+ bool is_internal)
{
ObjectAddress address;
Oid constrOid;
@@ -9684,13 +9689,16 @@ addFkConstraint(Constraint *fkconstraint, Relation rel, Relation pkrel,
*/
if (ConstraintNameIsUsed(CONSTRAINT_RELATION,
RelationGetRelid(rel),
- fkconstraint->conname))
+ constraintname))
conname = ChooseConstraintName(RelationGetRelationName(rel),
ChooseForeignKeyConstraintNameAddition(fkconstraint->fk_attrs),
"fkey",
RelationGetNamespace(rel), NIL);
else
- conname = fkconstraint->conname;
+ conname = constraintname;
+
+ if (fkconstraint->conname == NULL)
+ fkconstraint->conname = pstrdup(conname);
if (OidIsValid(parentConstr))
{
@@ -9742,7 +9750,7 @@ addFkConstraint(Constraint *fkconstraint, Relation rel, Relation pkrel,
conislocal, /* islocal */
coninhcount, /* inhcount */
connoinherit, /* conNoInherit */
- false); /* is_internal */
+ is_internal); /* is_internal */
ObjectAddressSet(address, ConstraintRelationId, constrOid);
@@ -9874,11 +9882,12 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
elog(ERROR, "index for %u not found in partition %s",
indexOid, RelationGetRelationName(partRel));
- address = addFkConstraint(fkconstraint, rel, partRel,
- partIndexId, parentConstr, FK_REFERENCED_SIDE,
- numfks, mapped_pkattnum, fkattnum,
- pfeqoperators, ppeqoperators, ffeqoperators,
- numfkdelsetcols, fkdelsetcols);
+ address = addFkConstraint(fkconstraint->conname, fkconstraint, rel,
+ partRel, partIndexId, parentConstr,
+ FK_REFERENCED_SIDE, numfks, mapped_pkattnum,
+ fkattnum, pfeqoperators, ppeqoperators,
+ ffeqoperators, numfkdelsetcols,
+ fkdelsetcols, false);
addFkRecurseReferenced(wqueue, fkconstraint, rel, partRel,
partIndexId, address.objectId, numfks,
mapped_pkattnum, fkattnum,
@@ -10063,12 +10072,12 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
* No luck finding a good constraint to reuse; create our own.
*/
- address = addFkConstraint(fkconstraint,
+ address = addFkConstraint(fkconstraint->conname, fkconstraint,
partition, pkrel, indexOid, parentConstr,
FK_REFERENCING_SIDE, numfks, pkattnum,
mapped_fkattnum, pfeqoperators,
ppeqoperators, ffeqoperators,
- numfkdelsetcols, fkdelsetcols);
+ numfkdelsetcols, fkdelsetcols, false);
/* call ourselves to finalize the creation and we're done */
addFkRecurseReferencing(wqueue, fkconstraint, partition, pkrel,
@@ -10305,10 +10314,11 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
constrForm->confrelid, constrForm->conrelid,
&deleteTriggerOid, &updateTriggerOid);
- address = addFkConstraint(fkconstraint, fkRel, partitionRel, partIndexId,
- constrOid, FK_REFERENCED_SIDE, numfks,
- mapped_confkey, conkey, conpfeqop, conppeqop,
- conffeqop, numfkdelsetcols, confdelsetcols);
+ address = addFkConstraint(fkconstraint->conname, fkconstraint, fkRel,
+ partitionRel, partIndexId, constrOid,
+ FK_REFERENCED_SIDE, numfks, mapped_confkey,
+ conkey, conpfeqop, conppeqop, conffeqop,
+ numfkdelsetcols, confdelsetcols, false);
addFkRecurseReferenced(NULL,
fkconstraint,
fkRel,
@@ -10428,9 +10438,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
Constraint *fkconstraint;
bool attached;
Oid indexOid;
- Oid constrOid;
- ObjectAddress address,
- referenced;
+ ObjectAddress address;
ListCell *cell;
Oid insertTriggerOid,
updateTriggerOid;
@@ -10527,7 +10535,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
fkconstraint->old_conpfeqop = NIL;
fkconstraint->old_pktable_oid = InvalidOid;
fkconstraint->skip_validation = false;
- fkconstraint->initially_valid = true;
+ fkconstraint->initially_valid = constrForm->convalidated;
for (int i = 0; i < numfks; i++)
{
Form_pg_attribute att;
@@ -10537,71 +10545,26 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
fkconstraint->fk_attrs = lappend(fkconstraint->fk_attrs,
makeString(NameStr(att->attname)));
}
- if (ConstraintNameIsUsed(CONSTRAINT_RELATION,
- RelationGetRelid(partRel),
- NameStr(constrForm->conname)))
- fkconstraint->conname =
- ChooseConstraintName(RelationGetRelationName(partRel),
- ChooseForeignKeyConstraintNameAddition(fkconstraint->fk_attrs),
- "fkey",
- RelationGetNamespace(partRel), NIL);
- else
- fkconstraint->conname = pstrdup(NameStr(constrForm->conname));
indexOid = constrForm->conindid;
- constrOid =
- CreateConstraintEntry(fkconstraint->conname,
- constrForm->connamespace,
- CONSTRAINT_FOREIGN,
- fkconstraint->deferrable,
- fkconstraint->initdeferred,
- constrForm->convalidated,
- parentConstrOid,
- RelationGetRelid(partRel),
- mapped_conkey,
- numfks,
- numfks,
- InvalidOid, /* not a domain constraint */
- indexOid,
- constrForm->confrelid, /* same foreign rel */
- confkey,
- conpfeqop,
- conppeqop,
- conffeqop,
- numfks,
- fkconstraint->fk_upd_action,
- fkconstraint->fk_del_action,
- confdelsetcols,
- numfkdelsetcols,
- fkconstraint->fk_matchtype,
- NULL,
- NULL,
- NULL,
- false, /* islocal */
- 1, /* inhcount */
- false, /* conNoInherit */
- true);
- /* Set up partition dependencies for the new constraint */
- ObjectAddressSet(address, ConstraintRelationId, constrOid);
- ObjectAddressSet(referenced, ConstraintRelationId, parentConstrOid);
- recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_PRI);
- ObjectAddressSet(referenced, RelationRelationId,
- RelationGetRelid(partRel));
- recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_SEC);
+ address = addFkConstraint(NameStr(constrForm->conname), fkconstraint,
+ partRel, pkrel, indexOid, parentConstrOid,
+ FK_REFERENCING_SIDE, numfks, confkey,
+ mapped_conkey, conpfeqop,
+ conppeqop, conffeqop,
+ numfkdelsetcols, confdelsetcols,
+ true);
/* Done with the cloned constraint's tuple */
ReleaseSysCache(tuple);
- /* Make all this visible before recursing */
- CommandCounterIncrement();
-
addFkRecurseReferencing(wqueue,
fkconstraint,
partRel,
pkrel,
indexOid,
- constrOid,
+ address.objectId,
numfks,
confkey,
mapped_conkey,
--
2.46.0
On 2024-Sep-26, Jehan-Guillaume de Rorthais wrote:
REL_14_STABLE backport doesn't seem trivial, so I'll wait for some
feedback, review & decision before going further down in backpatching.
Hi, thanks for these patches. I have made some edits of my own. In the
end I decided that I quite liked the new structure of the
addFkConstraint() function and friends. I added a bunch of comments and
changed names somewhat. Also, I think the benefit of the refactoring is
more obvious when all patches are squashed together, so I did that.
For branch 14, I opted to make it delete the constraints on detach.
This isn't ideal but unless somebody wants to spend a lot more time on
this, it seems the best we can do. Leaving broken constraints around
seems worse. The patch for 14 doesn't apply to 13 sadly. I didn't have
time to verify why, but it seems there's some rather larger conflict in
one spot.
I hope to be able to get these pushed over the weekend. That would give
us a couple of weeks before the November releases (but my availability
in those two weeks might be spotty.)
I spent some time going through your test additions and ended up with
your functions in this form:
-- displays constraints in schema fkpart12
CREATE OR REPLACE FUNCTION
fkpart12_constraints(OUT conrel regclass, OUT conname name,
OUT confrelid regclass, OUT conparent text)
RETURNS SETOF record AS $$
WITH RECURSIVE arrh AS (
SELECT oid, conrelid, conname, confrelid, NULL::name AS conparent
FROM pg_constraint
WHERE connamespace = 'fkpart12'::regnamespace AND
contype = 'f' AND conparentid = 0
UNION ALL
SELECT c.oid, c.conrelid, c.conname, c.confrelid,
(pg_identify_object('pg_constraint'::regclass, arrh.oid, 0)).identity
FROM pg_constraint c
JOIN arrh ON c.conparentid = arrh.oid
) SELECT conrelid::regclass, conname, confrelid::regclass, conparent
FROM arrh
ORDER BY conrelid::regclass::text, conname;
$$
LANGUAGE SQL;
-- displays triggers in tables in schema fkpart12
CREATE OR REPLACE FUNCTION
fkpart12_triggers(OUT tablename regclass, OUT constr text,
OUT samefunc boolean, OUT parent text)
RETURNS SETOF record AS $$
WITH RECURSIVE arrh AS (
SELECT t.oid, t.tgrelid::regclass as tablename, tgname,
t.tgfoid::regproc as trigfn,
(pg_identify_object('pg_constraint'::regclass, c.oid, 0)).identity as constr,
NULL::bool as samefunc,
NULL::name AS parent
FROM pg_trigger t
LEFT JOIN pg_constraint c ON c.oid = t.tgconstraint
WHERE (SELECT relnamespace FROM pg_class WHERE oid = t.tgrelid) = 'fkpart12'::regnamespace
AND c.contype = 'f' AND t.tgparentid = 0
UNION ALL
SELECT t2.oid, t2.tgrelid::regclass as tablename, t2.tgname,
t2.tgfoid::regproc as trigfn,
(pg_identify_object('pg_constraint'::regclass, c2.oid, 0)).identity,
arrh.trigfn = t2.tgfoid as samefunc,
replace((pg_identify_object('pg_trigger'::regclass, t2.tgparentid, 0)).identity,
t2.tgparentid::text, 'TGOID')
FROM pg_trigger t2
LEFT JOIN pg_constraint c2 ON c2.oid = t2.tgconstraint
JOIN arrh ON t2.tgparentid = arrh.oid
) SELECT tablename, constr, samefunc, parent
FROM arrh
ORDER BY tablename::text, constr;
$$
LANGUAGE SQL;
However, in the end I think this is a very good technique to verify that
the fix works correctly, but it's excessive to include these results in
the tests forevermore. So I've left them out for now. Maybe we should
reconsider on the older branches?
--
Álvaro Herrera PostgreSQL Developer — https://www.EnterpriseDB.com/
"If you have nothing to say, maybe you need just the right tool to help you
not say it." (New York Times, about Microsoft PowerPoint)
Attachments:
v4_14-0001-Redo-foreign-key-maintenance-during-partition-.patchtext/x-diff; charset=utf-8Download
From 87cfd9e5e6942e4287f4a578a8e6d86a66b099a3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=81lvaro=20Herrera?= <alvherre@alvh.no-ip.org>
Date: Fri, 18 Oct 2024 16:40:34 +0200
Subject: [PATCH v4_14] Redo foreign key maintenance during partition
ATTACH/DETACH
... when the reference table is partitioned.
It turns out that the catalog representation we chose for foreign keys
connecting partitioned tables is suboptimal, particularly in the sense
that a standalone table has a different way to represent it when
referencing a partitioned table, than when the same table becomes a
partition (and vice versa). This difference means we need to create
additional catalog rows on detach (pg_constraint and pg_trigger), and
remove them on attach.
As a very obvious symptom, we were missing action triggers after
detach, which means that you could update/delete rows from the
referenced partitioned table that still had referencing rows at the
other side previously detached, and fail to throw the required errors.
!!!!!!!!!
Note that this means existing FKs that refer to partitioned tables might
have rows that break relational integrity, on tables that were once
partitions.
Another possible problem is that trying to reattach a table
that had been detached would fail indicating that internal triggers
cannot be found, which from the user's point of view is nonsensical.
In branches 15 and above, we fix this by creating a new helper function
addFkConstraint() which is in charge of creating a standalone
pg_constraint row, and repurposing addFkRecurseReferencing() and
addFkRecurseReferenced() so that they're only the recursive routine for
each side of the FK, and they call addFkConstraint() to create
pg_constraint at each partitioning level and add the necessary triggers.
These new routines can be used during partition creation, partition
attach and detach, and foreign key creation. This reduces redundant
code and simplifies the flow.
In branches 14 and 13, we have a much simpler fix that consists on
simply removing the constraint on detach. The reason is that those
branches are missing commit f4566345cf40, which reworked the way this
works in a way that we didn't consider back-patchable at the time. I
doubt we want to change that now, so this is probably the best we can do
there.
We opted to leave branch 12 alone, because it's going to end maintenance
very soon, and any rushed patch might be worse.
In the future we might want to rethink the catalog representation to
avoid this problem, but for now the code seems to do what's required to
make the constraints operate correctly.
Co-authored-by: Jehan-Guillaume de Rorthais <jgdr@dalibo.com>
Co-authored-by: Tender Wang <tndrwang@gmail.com>
Co-authored-by: Alvaro Herrera <alvherre@alvh.no-ip.org>
Reported-by: Guillaume Lelarge <guillaume@lelarge.info>
Reported-by: Jehan-Guillaume de Rorthais <jgdr@dalibo.com>
Reported-by: Thomas Baehler (SBB CFF FFS) <thomas.baehler2@sbb.ch>
Discussion: https://postgr.es/m/20230420144344.40744130@karst
Discussion: https://postgr.es/m/20230705233028.2f554f73@karst
Discussion: https://postgr.es/m/GVAP278MB02787E7134FD691861635A8BC9032@GVAP278MB0278.CHEP278.PROD.OUTLOOK.COM
Discussion: https://postgr.es/m/18541-628a61bc267cd2d3@postgresql.org
---
src/backend/commands/tablecmds.c | 160 ++++++++++++++++++----
src/test/regress/expected/foreign_key.out | 91 ++++++++++++
src/test/regress/sql/foreign_key.sql | 56 ++++++++
3 files changed, 281 insertions(+), 26 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 38cc7f6b854..6dd4e88d48e 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -10389,6 +10389,82 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
table_close(trigrel, RowExclusiveLock);
ConstraintSetParentConstraint(fk->conoid, parentConstrOid, partRelid);
+
+ /*
+ * If the referenced table is partitioned, then the partition we're
+ * attaching now has extra pg_constraint rows and action triggers that are
+ * no longer needed. Remove those.
+ */
+ if (get_rel_relkind(fk->confrelid) == RELKIND_PARTITIONED_TABLE)
+ {
+ Relation pg_constraint = table_open(ConstraintRelationId, RowShareLock);
+ ObjectAddresses *objs;
+ HeapTuple consttup;
+
+ ScanKeyInit(&key,
+ Anum_pg_constraint_conrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(fk->conrelid));
+
+ scan = systable_beginscan(pg_constraint,
+ ConstraintRelidTypidNameIndexId,
+ true, NULL, 1, &key);
+ objs = new_object_addresses();
+ while ((consttup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_constraint conform = (Form_pg_constraint) GETSTRUCT(consttup);
+
+ if (conform->conparentid != fk->conoid)
+ continue;
+ else
+ {
+ ObjectAddress addr;
+ SysScanDesc scan2;
+ ScanKeyData key2;
+ int n PG_USED_FOR_ASSERTS_ONLY;
+
+ ObjectAddressSet(addr, ConstraintRelationId, conform->oid);
+ add_exact_object_address(&addr, objs);
+
+ /*
+ * First we must delete the dependency records that bind
+ * the constraint records together.
+ */
+ n = deleteDependencyRecordsForSpecific(ConstraintRelationId,
+ conform->oid,
+ DEPENDENCY_INTERNAL,
+ ConstraintRelationId,
+ fk->conoid);
+ Assert(n == 1); /* actually only one is expected */
+
+ /*
+ * Now search for the triggers for this constraint and set
+ * them up for deletion too
+ */
+ ScanKeyInit(&key2,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conform->oid));
+ scan2 = systable_beginscan(trigrel, TriggerConstraintIndexId,
+ true, NULL, 1, &key2);
+ while ((trigtup = systable_getnext(scan2)) != NULL)
+ {
+ ObjectAddressSet(addr, TriggerRelationId,
+ ((Form_pg_trigger) GETSTRUCT(trigtup))->oid);
+ add_exact_object_address(&addr, objs);
+ }
+ systable_endscan(scan2);
+ }
+ }
+ /* make the dependency deletions visible */
+ CommandCounterIncrement();
+ performMultipleDeletions(objs, DROP_RESTRICT,
+ PERFORM_DELETION_INTERNAL);
+ systable_endscan(scan);
+
+ table_close(pg_constraint, RowShareLock);
+ }
+
CommandCounterIncrement();
return true;
}
@@ -18154,6 +18230,7 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
new_repl[Natts_pg_class];
HeapTuple tuple,
newtuple;
+ ObjectAddresses *dropobjs = NULL;
if (concurrent)
{
@@ -18168,8 +18245,8 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
DropClonedTriggersFromPartition(RelationGetRelid(partRel));
/*
- * Detach any foreign keys that are inherited. This includes creating
- * additional action triggers.
+ * Detach any foreign keys that are inherited -- or, if they reference
+ * partitioned tables, drop them.
*/
fks = copyObject(RelationGetFKeyList(partRel));
foreach(cell, fks)
@@ -18177,7 +18254,6 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
Form_pg_constraint conform;
- Constraint *fkconstraint;
contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
if (!HeapTupleIsValid(contup))
@@ -18192,39 +18268,71 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
continue;
}
- /* unset conparentid and adjust conislocal, coninhcount, etc. */
+ /* Mark the constraint as independent */
ConstraintSetParentConstraint(fk->conoid, InvalidOid, 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.
+ * If the constraint references a partitioned table, just drop the
+ * constraint, because it's more work to preserve the constraint
+ * correctly.
+ *
+ * If it references a plain table, then we can create the action
+ * triggers and it'll be okay.
*/
- fkconstraint = makeNode(Constraint);
- fkconstraint->contype = CONSTRAINT_FOREIGN;
- fkconstraint->conname = pstrdup(NameStr(conform->conname));
- fkconstraint->deferrable = conform->condeferrable;
- fkconstraint->initdeferred = conform->condeferred;
- fkconstraint->location = -1;
- fkconstraint->pktable = NULL;
- fkconstraint->fk_attrs = NIL;
- fkconstraint->pk_attrs = NIL;
- fkconstraint->fk_matchtype = conform->confmatchtype;
- fkconstraint->fk_upd_action = conform->confupdtype;
- fkconstraint->fk_del_action = conform->confdeltype;
- fkconstraint->old_conpfeqop = NIL;
- fkconstraint->old_pktable_oid = InvalidOid;
- fkconstraint->skip_validation = false;
- fkconstraint->initially_valid = true;
+ if (get_rel_relkind(fk->confrelid) == RELKIND_PARTITIONED_TABLE)
+ {
+ ObjectAddress constraddr;
- createForeignKeyActionTriggers(partRel, conform->confrelid,
- fkconstraint, fk->conoid,
- conform->conindid);
+ /* make the dependency deletions above visible */
+ CommandCounterIncrement();
+
+ /*
+ * Remember the constraint and its triggers for later deletion.
+ */
+ if (dropobjs == NULL)
+ dropobjs = new_object_addresses();
+ ObjectAddressSet(constraddr, ConstraintRelationId, fk->conoid);
+ add_exact_object_address(&constraddr, dropobjs);
+ }
+ else
+ {
+ Constraint *fkconstraint;
+
+ /*
+ * Make the action triggers on the referenced relation. When this
+ * was a partition the action triggers pointed to the parent rel
+ * (they still do), but now we need separate ones of our own.
+ */
+ fkconstraint = makeNode(Constraint);
+ fkconstraint->contype = CONSTRAINT_FOREIGN;
+ fkconstraint->conname = pstrdup(NameStr(conform->conname));
+ fkconstraint->deferrable = conform->condeferrable;
+ fkconstraint->initdeferred = conform->condeferred;
+ fkconstraint->location = -1;
+ fkconstraint->pktable = NULL;
+ fkconstraint->fk_attrs = NIL;
+ fkconstraint->pk_attrs = NIL;
+ fkconstraint->fk_matchtype = conform->confmatchtype;
+ fkconstraint->fk_upd_action = conform->confupdtype;
+ fkconstraint->fk_del_action = conform->confdeltype;
+ fkconstraint->old_conpfeqop = NIL;
+ fkconstraint->old_pktable_oid = InvalidOid;
+ fkconstraint->skip_validation = false;
+ fkconstraint->initially_valid = true;
+
+ createForeignKeyActionTriggers(partRel, conform->confrelid,
+ fkconstraint, fk->conoid,
+ conform->conindid);
+ }
ReleaseSysCache(contup);
}
list_free_deep(fks);
+ /* If we collected any constraints for deletion, do so now. */
+ if (dropobjs != NULL)
+ performMultipleDeletions(dropobjs, DROP_CASCADE, 0);
+
/*
* Any sub-constraints that are in the referenced-side of a larger
* constraint have to be removed. This partition is no longer part of the
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index a95fe17d1a7..e17b95021cf 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -2665,3 +2665,94 @@ DROP SCHEMA fkpart10 CASCADE;
NOTICE: drop cascades to 2 other objects
DETAIL: drop cascades to table fkpart10.tbl1
drop cascades to table fkpart10.tbl2
+-- When a table is attached as partition to a partitioned table that has
+-- a foreign key to another partitioned table, it acquires a clone of the
+-- FK. Upon detach, Postgres 14 and earlier remove the foreign key (newer
+-- versions make it a standalone constraint.)
+CREATE SCHEMA fkpart12
+ CREATE TABLE fk_p ( id int, jd int, PRIMARY KEY(id, jd)) PARTITION BY list (id)
+ CREATE TABLE fk_p_1 PARTITION OF fk_p FOR VALUES IN (1) PARTITION BY list (jd)
+ CREATE TABLE fk_p_1_1 PARTITION OF fk_p_1 FOR VALUES IN (1)
+ CREATE TABLE fk_p_1_2 PARTITION OF fk_p_1 FOR VALUES IN (2)
+ CREATE TABLE fk_p_2 PARTITION OF fk_p FOR VALUES IN (2) PARTITION BY list (jd)
+ CREATE TABLE fk_p_2_1 PARTITION OF fk_p_2 FOR VALUES IN (1)
+ CREATE TABLE fk_p_2_2 PARTITION OF fk_p_2 FOR VALUES IN (2)
+ CREATE TABLE fk_r_1 ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL)
+ CREATE TABLE fk_r_2 ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL) PARTITION BY list (id)
+ CREATE TABLE fk_r_2_1 PARTITION OF fk_r_2 FOR VALUES IN (2, 1)
+ CREATE TABLE fk_r ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL,
+ FOREIGN KEY (p_id, p_jd) REFERENCES fk_p (id, jd)
+ ) PARTITION BY list (id);
+SET search_path TO fkpart12;
+INSERT INTO fk_p VALUES (1, 1);
+ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1);
+ALTER TABLE fk_r ATTACH PARTITION fk_r_2 FOR VALUES IN (2);
+\d fk_r_2
+ Partitioned table "fkpart12.fk_r_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ id | integer | | not null |
+ p_id | integer | | not null |
+ p_jd | integer | | not null |
+Partition of: fk_r FOR VALUES IN (2)
+Partition key: LIST (id)
+Indexes:
+ "fk_r_2_pkey" PRIMARY KEY, btree (id)
+Foreign-key constraints:
+ TABLE "fk_r" CONSTRAINT "fk_r_p_id_p_jd_fkey" FOREIGN KEY (p_id, p_jd) REFERENCES fk_p(id, jd)
+Number of partitions: 1 (Use \d+ to list them.)
+
+INSERT INTO fk_r VALUES (1, 1, 1);
+INSERT INTO fk_r VALUES (2, 2, 1); -- fails
+ERROR: insert or update on table "fk_r_2_1" violates foreign key constraint "fk_r_p_id_p_jd_fkey"
+DETAIL: Key (p_id, p_jd)=(2, 1) is not present in table "fk_p".
+ALTER TABLE fk_r DETACH PARTITION fk_r_1;
+ALTER TABLE fk_r DETACH PARTITION fk_r_2;
+\d fk_r_2
+ Partitioned table "fkpart12.fk_r_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ id | integer | | not null |
+ p_id | integer | | not null |
+ p_jd | integer | | not null |
+Partition key: LIST (id)
+Indexes:
+ "fk_r_2_pkey" PRIMARY KEY, btree (id)
+Number of partitions: 1 (Use \d+ to list them.)
+
+INSERT INTO fk_r_1 VALUES (2, 1, 2); -- works: there's no FK anymore
+DELETE FROM fk_p; -- works
+ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1); -- fails
+ERROR: partition constraint of relation "fk_r_1" is violated by some row
+INSERT INTO fk_r_2 VALUES (2, 2, 2);
+INSERT INTO fk_p VALUES (2, 2);
+ALTER TABLE fk_r ATTACH PARTITION fk_r_2 FOR VALUES IN (2);
+\d fk_r_2
+ Partitioned table "fkpart12.fk_r_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ id | integer | | not null |
+ p_id | integer | | not null |
+ p_jd | integer | | not null |
+Partition of: fk_r FOR VALUES IN (2)
+Partition key: LIST (id)
+Indexes:
+ "fk_r_2_pkey" PRIMARY KEY, btree (id)
+Foreign-key constraints:
+ TABLE "fk_r" CONSTRAINT "fk_r_p_id_p_jd_fkey" FOREIGN KEY (p_id, p_jd) REFERENCES fk_p(id, jd)
+Number of partitions: 1 (Use \d+ to list them.)
+
+DELETE FROM fk_p; -- fails
+ERROR: update or delete on table "fk_p_2_2" violates foreign key constraint "fk_r_p_id_p_jd_fkey6" on table "fk_r"
+DETAIL: Key (id, jd)=(2, 2) is still referenced from table "fk_r".
+-- these should all fail
+ALTER TABLE fk_r_1 DROP CONSTRAINT fk_r_p_id_p_jd_fkey;
+ERROR: constraint "fk_r_p_id_p_jd_fkey" of relation "fk_r_1" does not exist
+ALTER TABLE fk_r DROP CONSTRAINT fk_r_p_id_p_jd_fkey1;
+ERROR: cannot drop inherited constraint "fk_r_p_id_p_jd_fkey1" of relation "fk_r"
+ALTER TABLE fk_r_2 DROP CONSTRAINT fk_r_p_id_p_jd_fkey;
+ERROR: cannot drop inherited constraint "fk_r_p_id_p_jd_fkey" of relation "fk_r_2"
+SET client_min_messages TO warning;
+DROP SCHEMA fkpart12 CASCADE;
+RESET client_min_messages;
+RESET search_path;
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index 2ae74ebb148..7af7d562c7b 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -1902,3 +1902,59 @@ UPDATE fkpart10.tbl1 SET f1 = 2 WHERE f1 = 1;
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
COMMIT;
DROP SCHEMA fkpart10 CASCADE;
+
+-- When a table is attached as partition to a partitioned table that has
+-- a foreign key to another partitioned table, it acquires a clone of the
+-- FK. Upon detach, Postgres 14 and earlier remove the foreign key (newer
+-- versions make it a standalone constraint.)
+CREATE SCHEMA fkpart12
+ CREATE TABLE fk_p ( id int, jd int, PRIMARY KEY(id, jd)) PARTITION BY list (id)
+ CREATE TABLE fk_p_1 PARTITION OF fk_p FOR VALUES IN (1) PARTITION BY list (jd)
+ CREATE TABLE fk_p_1_1 PARTITION OF fk_p_1 FOR VALUES IN (1)
+ CREATE TABLE fk_p_1_2 PARTITION OF fk_p_1 FOR VALUES IN (2)
+ CREATE TABLE fk_p_2 PARTITION OF fk_p FOR VALUES IN (2) PARTITION BY list (jd)
+ CREATE TABLE fk_p_2_1 PARTITION OF fk_p_2 FOR VALUES IN (1)
+ CREATE TABLE fk_p_2_2 PARTITION OF fk_p_2 FOR VALUES IN (2)
+ CREATE TABLE fk_r_1 ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL)
+ CREATE TABLE fk_r_2 ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL) PARTITION BY list (id)
+ CREATE TABLE fk_r_2_1 PARTITION OF fk_r_2 FOR VALUES IN (2, 1)
+ CREATE TABLE fk_r ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL,
+ FOREIGN KEY (p_id, p_jd) REFERENCES fk_p (id, jd)
+ ) PARTITION BY list (id);
+SET search_path TO fkpart12;
+
+INSERT INTO fk_p VALUES (1, 1);
+
+ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1);
+ALTER TABLE fk_r ATTACH PARTITION fk_r_2 FOR VALUES IN (2);
+
+\d fk_r_2
+
+INSERT INTO fk_r VALUES (1, 1, 1);
+INSERT INTO fk_r VALUES (2, 2, 1); -- fails
+
+ALTER TABLE fk_r DETACH PARTITION fk_r_1;
+ALTER TABLE fk_r DETACH PARTITION fk_r_2;
+
+\d fk_r_2
+
+INSERT INTO fk_r_1 VALUES (2, 1, 2); -- works: there's no FK anymore
+DELETE FROM fk_p; -- works
+
+ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1); -- fails
+
+INSERT INTO fk_r_2 VALUES (2, 2, 2);
+INSERT INTO fk_p VALUES (2, 2);
+ALTER TABLE fk_r ATTACH PARTITION fk_r_2 FOR VALUES IN (2);
+\d fk_r_2
+DELETE FROM fk_p; -- fails
+
+-- these should all fail
+ALTER TABLE fk_r_1 DROP CONSTRAINT fk_r_p_id_p_jd_fkey;
+ALTER TABLE fk_r DROP CONSTRAINT fk_r_p_id_p_jd_fkey1;
+ALTER TABLE fk_r_2 DROP CONSTRAINT fk_r_p_id_p_jd_fkey;
+
+SET client_min_messages TO warning;
+DROP SCHEMA fkpart12 CASCADE;
+RESET client_min_messages;
+RESET search_path;
--
2.39.5
v4_15-0001-Redo-foreign-key-maintenance-during-partition-.patchtext/x-diff; charset=utf-8Download
From d8dd2463a06370d435e63f7448cdf40bab587234 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=81lvaro=20Herrera?= <alvherre@alvh.no-ip.org>
Date: Fri, 18 Oct 2024 16:40:35 +0200
Subject: [PATCH v4_15] Redo foreign key maintenance during partition
ATTACH/DETACH
... when the reference table is partitioned.
It turns out that the catalog representation we chose for foreign keys
connecting partitioned tables is suboptimal, particularly in the sense
that a standalone table has a different way to represent it when
referencing a partitioned table, than when the same table becomes a
partition (and vice versa). This difference means we need to create
additional catalog rows on detach (pg_constraint and pg_trigger), and
remove them on attach.
As a very obvious symptom, we were missing action triggers after
detach, which means that you could update/delete rows from the
referenced partitioned table that still had referencing rows at the
other side previously detached, and fail to throw the required errors.
!!!!!!!!!
Note that this means existing FKs that refer to partitioned tables might
have rows that break relational integrity, on tables that were once
partitions.
Another possible problem is that trying to reattach a table
that had been detached would fail indicating that internal triggers
cannot be found, which from the user's point of view is nonsensical.
In branches 15 and above, we fix this by creating a new helper function
addFkConstraint() which is in charge of creating a standalone
pg_constraint row, and repurposing addFkRecurseReferencing() and
addFkRecurseReferenced() so that they're only the recursive routine for
each side of the FK, and they call addFkConstraint() to create
pg_constraint at each partitioning level and add the necessary triggers.
These new routines can be used during partition creation, partition
attach and detach, and foreign key creation. This reduces redundant
code and simplifies the flow.
In branches 14 and 13, we have a much simpler fix that consists on
simply removing the constraint on detach. The reason is that those
branches are missing commit f4566345cf40, which reworked the way this
works in a way that we didn't consider back-patchable at the time. I
doubt we want to change that now, so this is probably the best we can do
there.
We opted to leave branch 12 alone, because it's going to end maintenance
very soon, and any rushed patch might be worse.
In the future we might want to rethink the catalog representation to
avoid this problem, but for now the code seems to do what's required to
make the constraints operate correctly.
Co-authored-by: Jehan-Guillaume de Rorthais <jgdr@dalibo.com>
Co-authored-by: Tender Wang <tndrwang@gmail.com>
Co-authored-by: Alvaro Herrera <alvherre@alvh.no-ip.org>
Reported-by: Guillaume Lelarge <guillaume@lelarge.info>
Reported-by: Jehan-Guillaume de Rorthais <jgdr@dalibo.com>
Reported-by: Thomas Baehler (SBB CFF FFS) <thomas.baehler2@sbb.ch>
Discussion: https://postgr.es/m/20230420144344.40744130@karst
Discussion: https://postgr.es/m/20230705233028.2f554f73@karst
Discussion: https://postgr.es/m/GVAP278MB02787E7134FD691861635A8BC9032@GVAP278MB0278.CHEP278.PROD.OUTLOOK.COM
Discussion: https://postgr.es/m/18541-628a61bc267cd2d3@postgresql.org
---
src/backend/commands/tablecmds.c | 631 ++++++++++++++--------
src/test/regress/expected/foreign_key.out | 96 ++++
src/test/regress/sql/foreign_key.sql | 57 ++
src/tools/pgindent/typedefs.list | 1 +
4 files changed, 549 insertions(+), 236 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index e6c911fd9a4..18690dc4af0 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -335,6 +335,14 @@ typedef struct ForeignTruncateInfo
List *rels;
} ForeignTruncateInfo;
+/* Partial or complete FK creation in addFkConstraint() */
+typedef enum addFkConstraintSides
+{
+ addFkReferencedSide,
+ addFkReferencingSide,
+ addFkBothSides,
+} addFkConstraintSides;
+
/*
* Partition tables are expected to be dropped when the parent partitioned
* table gets dropped. Hence for partitioning we use AUTO dependency.
@@ -489,16 +497,25 @@ static ObjectAddress ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *
Relation rel, Constraint *fkconstraint,
bool recurse, bool recursing,
LOCKMODE lockmode);
-static ObjectAddress addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint,
- Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
- int numfks, int16 *pkattnum, int16 *fkattnum,
- Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
- int numfkdelsetcols, int16 *fkdelsetcols,
- bool old_check_ok,
- Oid parentDelTrigger, Oid parentUpdTrigger);
static void validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
int numfksetcols, const int16 *fksetcolsattnums,
List *fksetcols);
+static ObjectAddress addFkConstraint(addFkConstraintSides fkside,
+ char *constraintname,
+ Constraint *fkconstraint, Relation rel,
+ Relation pkrel, Oid indexOid,
+ Oid parentConstr,
+ int numfks, int16 *pkattnum, int16 *fkattnum,
+ Oid *pfeqoperators, Oid *ppeqoperators,
+ Oid *ffeqoperators, int numfkdelsetcols,
+ int16 *fkdelsetcols, bool is_internal);
+static void addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint,
+ Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
+ int numfks, int16 *pkattnum, int16 *fkattnum,
+ Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
+ int numfkdelsetcols, int16 *fkdelsetcols,
+ bool old_check_ok,
+ Oid parentDelTrigger, Oid parentUpdTrigger);
static void addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint,
Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks, int16 *pkattnum, int16 *fkattnum,
@@ -625,10 +642,12 @@ static void DropClonedTriggersFromPartition(Oid partitionId);
static ObjectAddress ATExecDetachPartition(List **wqueue, AlteredTableInfo *tab,
Relation rel, RangeVar *name,
bool concurrent);
-static void DetachPartitionFinalize(Relation rel, Relation partRel,
- bool concurrent, Oid defaultPartOid);
-static ObjectAddress ATExecDetachPartitionFinalize(Relation rel, RangeVar *name);
-static ObjectAddress ATExecAttachPartitionIdx(List **wqueue, Relation rel,
+static void DetachPartitionFinalize(List **wqueue, Relation rel,
+ Relation partRel, bool concurrent,
+ Oid defaultPartOid);
+static ObjectAddress ATExecDetachPartitionFinalize(List **wqueue, Relation rel,
+ RangeVar *name);
+static ObjectAddress ATExecAttachPartitionIdx(List **wqueue, Relation parentIdx,
RangeVar *name);
static void validatePartitionedIndex(Relation partedIdx, Relation partedTbl);
static void refuseDupeIndexAttach(Relation parentIdx, Relation partIdx,
@@ -5283,7 +5302,7 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
((PartitionCmd *) cmd->def)->concurrent);
break;
case AT_DetachPartitionFinalize:
- ATExecDetachPartitionFinalize(rel, ((PartitionCmd *) cmd->def)->name);
+ address = ATExecDetachPartitionFinalize(wqueue, rel, ((PartitionCmd *) cmd->def)->name);
break;
default: /* oops */
elog(ERROR, "unrecognized alter table type: %d",
@@ -9524,25 +9543,37 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
ffeqoperators[i] = ffeqop;
}
- /*
- * Create all the constraint and trigger objects, recursing to partitions
- * as necessary. First handle the referenced side.
- */
- address = addFkRecurseReferenced(wqueue, fkconstraint, rel, pkrel,
- indexOid,
- InvalidOid, /* no parent constraint */
- numfks,
- pkattnum,
- fkattnum,
- pfeqoperators,
- ppeqoperators,
- ffeqoperators,
- numfkdelsetcols,
- fkdelsetcols,
- old_check_ok,
- InvalidOid, InvalidOid);
+ /* First, create the constraint catalog entry itself. */
+ address = addFkConstraint(addFkBothSides,
+ fkconstraint->conname, fkconstraint, rel, pkrel,
+ indexOid,
+ InvalidOid, /* no parent constraint */
+ numfks,
+ pkattnum,
+ fkattnum,
+ pfeqoperators,
+ ppeqoperators,
+ ffeqoperators,
+ numfkdelsetcols,
+ fkdelsetcols,
+ false);
- /* Now handle the referencing side. */
+ /* Next process the action triggers at the referenced side and recurse */
+ addFkRecurseReferenced(wqueue, fkconstraint, rel, pkrel,
+ indexOid,
+ address.objectId,
+ numfks,
+ pkattnum,
+ fkattnum,
+ pfeqoperators,
+ ppeqoperators,
+ ffeqoperators,
+ numfkdelsetcols,
+ fkdelsetcols,
+ old_check_ok,
+ InvalidOid, InvalidOid);
+
+ /* Lastly create the check triggers at the referencing side and recurse */
addFkRecurseReferencing(wqueue, fkconstraint, rel, pkrel,
indexOid,
address.objectId,
@@ -9602,46 +9633,43 @@ validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
}
/*
- * addFkRecurseReferenced
- * subroutine for ATAddForeignKeyConstraint; recurses on the referenced
- * side of the constraint
+ * addFkConstraint
+ * Install pg_constraint entries to implement a foreign key constraint.
+ * Caller must separately invoke addFkRecurseReferenced and
+ * addFkRecurseReferencing, as appropriate, to install pg_trigger entries
+ * and (for partitioned tables) recurse to partitions.
*
- * Create pg_constraint rows for the referenced side of the constraint,
- * referencing the parent of the referencing side; also create action triggers
- * on leaf partitions. If the table is partitioned, recurse to handle each
- * partition.
- *
- * wqueue is the ALTER TABLE work queue; can be NULL when not running as part
- * of an ALTER TABLE sequence.
- * fkconstraint is the constraint being added.
- * rel is the root referencing relation.
- * pkrel is the referenced relation; might be a partition, if recursing.
- * indexOid is the OID of the index (on pkrel) implementing this constraint.
- * parentConstr is the OID of a parent constraint; InvalidOid if this is a
- * top-level constraint.
- * numfks is the number of columns in the foreign key
- * pkattnum is the attnum array of referenced attributes.
- * fkattnum is the attnum array of referencing attributes.
- * numfkdelsetcols is the number of columns in the ON DELETE SET NULL/DEFAULT
- * (...) clause
- * fkdelsetcols is the attnum array of the columns in the ON DELETE SET
- * NULL/DEFAULT clause
- * pf/pp/ffeqoperators are OID array of operators between columns.
- * old_check_ok signals that this constraint replaces an existing one that
- * was already validated (thus this one doesn't need validation).
- * parentDelTrigger and parentUpdTrigger, when being recursively called on
- * a partition, are the OIDs of the parent action triggers for DELETE and
- * UPDATE respectively.
+ * * fkside indicates which side of the FK (or both) to create. Caller must
+ * call addFkRecurseReferenced if it's addFkReferencedSide,
+ * addFkRecurseReferencing if it's addFkReferencingSide, or both if it's
+ * addFkBothSides.
+ * * constraintname the base name for the constraint being added. This is
+ * copied to fkconstraint->conname if the latter is not set.
+ * * fkconstraint is the constraint being added.
+ * * rel is the root referencing relation.
+ * * pkrel is the referenced relation; might be a partition, if recursing.
+ * * indexOid is the OID of the index (on pkrel) implementing this constraint.
+ * * parentConstr is the OID of a parent constraint; InvalidOid if this is a
+ * top-level constraint.
+ * * numfks is the number of columns in the foreign key.
+ * * pkattnum is the attnum array of referenced attributes.
+ * * fkattnum is the attnum array of referencing attributes.
+ * * pf/pp/ffeqoperators are OID array of operators between columns.
+ * * numfkdelsetcols is the number of columns in the ON DELETE SET NULL/DEFAULT
+ * (...) clause.
+ * * fkdelsetcols is the attnum array of the columns in the ON DELETE SET
+ * NULL/DEFAULT clause.
+ * * is_internal: whether to mark the constraint as internal (used during
+ * recursion)
*/
static ObjectAddress
-addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
- Relation pkrel, Oid indexOid, Oid parentConstr,
- int numfks,
- int16 *pkattnum, int16 *fkattnum, Oid *pfeqoperators,
- Oid *ppeqoperators, Oid *ffeqoperators,
- int numfkdelsetcols, int16 *fkdelsetcols,
- bool old_check_ok,
- Oid parentDelTrigger, Oid parentUpdTrigger)
+addFkConstraint(addFkConstraintSides fkside,
+ char *constraintname, Constraint *fkconstraint,
+ Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
+ int numfks, int16 *pkattnum,
+ int16 *fkattnum, Oid *pfeqoperators, Oid *ppeqoperators,
+ Oid *ffeqoperators, int numfkdelsetcols, int16 *fkdelsetcols,
+ bool is_internal)
{
ObjectAddress address;
Oid constrOid;
@@ -9649,8 +9677,6 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
bool conislocal;
int coninhcount;
bool connoinherit;
- Oid deleteTriggerOid,
- updateTriggerOid;
/*
* Verify relkind for each referenced partition. At the top level, this
@@ -9669,13 +9695,16 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
*/
if (ConstraintNameIsUsed(CONSTRAINT_RELATION,
RelationGetRelid(rel),
- fkconstraint->conname))
+ constraintname))
conname = ChooseConstraintName(RelationGetRelationName(rel),
ChooseForeignKeyConstraintNameAddition(fkconstraint->fk_attrs),
"fkey",
RelationGetNamespace(rel), NIL);
else
- conname = fkconstraint->conname;
+ conname = constraintname;
+
+ if (fkconstraint->conname == NULL)
+ fkconstraint->conname = pstrdup(conname);
if (OidIsValid(parentConstr))
{
@@ -9727,33 +9756,95 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
conislocal, /* islocal */
coninhcount, /* inhcount */
connoinherit, /* conNoInherit */
- false); /* is_internal */
+ is_internal); /* is_internal */
ObjectAddressSet(address, ConstraintRelationId, constrOid);
- /*
- * Mark the child constraint as part of the parent constraint; it must not
- * be dropped on its own. (This constraint is deleted when the partition
- * is detached, but a special check needs to occur that the partition
- * contains no referenced values.)
- */
if (OidIsValid(parentConstr))
{
ObjectAddress referenced;
ObjectAddressSet(referenced, ConstraintRelationId, parentConstr);
- recordDependencyOn(&address, &referenced, DEPENDENCY_INTERNAL);
+ if (fkside == addFkReferencedSide)
+ {
+ /*
+ * Mark the child constraint as part of the parent constraint; it
+ * must not be dropped on its own. (This constraint is deleted
+ * when the partition is detached, but a special check needs to
+ * occur that the partition contains no referenced values.)
+ */
+ recordDependencyOn(&address, &referenced, DEPENDENCY_INTERNAL);
+ }
+ else
+ {
+ /*
+ * Give this constraint partition-type dependencies on the parent
+ * constraint as well as the table.
+ */
+ recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_PRI);
+ ObjectAddressSet(referenced, RelationRelationId, RelationGetRelid(rel));
+ recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_SEC);
+ }
}
/* make new constraint visible, in case we add more */
CommandCounterIncrement();
+ return address;
+}
+
+/*
+ * addFkRecurseReferenced
+ * Recursive helper for the referenced side of foreign key creation,
+ * which creates the action triggers and recurses
+ *
+ * If the referenced relation is a plain relation, create the necessary action
+ * triggers that implement the constraint. If the referenced relation is a
+ * partitioned table, then we create a pg_constraint row referencing the parent
+ * of the referencing side for it and recurse on this routine for each
+ * partition.
+ *
+ * wqueue is the ALTER TABLE work queue; can be NULL when not running as part
+ * of an ALTER TABLE sequence.
+ * fkconstraint is the constraint being added.
+ * rel is the root referencing relation.
+ * pkrel is the referenced relation; might be a partition, if recursing.
+ * indexOid is the OID of the index (on pkrel) implementing this constraint.
+ * parentConstr is the OID of a parent constraint; InvalidOid if this is a
+ * top-level constraint.
+ * numfks is the number of columns in the foreign key
+ * pkattnum is the attnum array of referenced attributes.
+ * fkattnum is the attnum array of referencing attributes.
+ * numfkdelsetcols is the number of columns in the ON DELETE SET NULL/DEFAULT
+ * (...) clause
+ * fkdelsetcols is the attnum array of the columns in the ON DELETE SET
+ * NULL/DEFAULT clause
+ * pf/pp/ffeqoperators are OID array of operators between columns.
+ * old_check_ok signals that this constraint replaces an existing one that
+ * was already validated (thus this one doesn't need validation).
+ * parentDelTrigger and parentUpdTrigger, when being recursively called on
+ * a partition, are the OIDs of the parent action triggers for DELETE and
+ * UPDATE respectively.
+ */
+static void
+addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
+ Relation pkrel, Oid indexOid, Oid parentConstr,
+ int numfks,
+ int16 *pkattnum, int16 *fkattnum, Oid *pfeqoperators,
+ Oid *ppeqoperators, Oid *ffeqoperators,
+ int numfkdelsetcols, int16 *fkdelsetcols,
+ bool old_check_ok,
+ Oid parentDelTrigger, Oid parentUpdTrigger)
+{
+ Oid deleteTriggerOid,
+ updateTriggerOid;
+
/*
* Create the action triggers that enforce the constraint.
*/
createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
fkconstraint,
- constrOid, indexOid,
+ parentConstr, indexOid,
parentDelTrigger, parentUpdTrigger,
&deleteTriggerOid, &updateTriggerOid);
@@ -9772,6 +9863,7 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
AttrMap *map;
AttrNumber *mapped_pkattnum;
Oid partIndexId;
+ ObjectAddress address;
partRel = table_open(pd->oids[i], ShareRowExclusiveLock);
@@ -9790,13 +9882,23 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
else
mapped_pkattnum = pkattnum;
- /* do the deed */
+ /* Determine the index to use at this level */
partIndexId = index_get_partition(partRel, indexOid);
if (!OidIsValid(partIndexId))
elog(ERROR, "index for %u not found in partition %s",
indexOid, RelationGetRelationName(partRel));
+
+ /* Create entry at this level ... */
+ address = addFkConstraint(addFkReferencedSide,
+ fkconstraint->conname, fkconstraint, rel,
+ partRel, partIndexId, parentConstr,
+ numfks, mapped_pkattnum,
+ fkattnum, pfeqoperators, ppeqoperators,
+ ffeqoperators, numfkdelsetcols,
+ fkdelsetcols, true);
+ /* ... and recurse to our children */
addFkRecurseReferenced(wqueue, fkconstraint, rel, partRel,
- partIndexId, constrOid, numfks,
+ partIndexId, address.objectId, numfks,
mapped_pkattnum, fkattnum,
pfeqoperators, ppeqoperators, ffeqoperators,
numfkdelsetcols, fkdelsetcols,
@@ -9812,13 +9914,12 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
}
}
}
-
- return address;
}
/*
* addFkRecurseReferencing
- * subroutine for ATAddForeignKeyConstraint and CloneFkReferencing
+ * Recursive helper for the referencing side of foreign key creation,
+ * which creates the check triggers and recurses
*
* If the referencing relation is a plain relation, create the necessary check
* triggers that implement the constraint, and set up for Phase 3 constraint
@@ -9938,10 +10039,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
AttrMap *attmap;
AttrNumber mapped_fkattnum[INDEX_MAX_KEYS];
bool attached;
- char *conname;
- Oid constrOid;
- ObjectAddress address,
- referenced;
+ ObjectAddress address;
ListCell *cell;
CheckAlterTableIsSafe(partition);
@@ -9983,65 +10081,18 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
/*
* No luck finding a good constraint to reuse; create our own.
*/
- if (ConstraintNameIsUsed(CONSTRAINT_RELATION,
- RelationGetRelid(partition),
- fkconstraint->conname))
- conname = ChooseConstraintName(RelationGetRelationName(partition),
- ChooseForeignKeyConstraintNameAddition(fkconstraint->fk_attrs),
- "fkey",
- RelationGetNamespace(partition), NIL);
- else
- conname = fkconstraint->conname;
- constrOid =
- CreateConstraintEntry(conname,
- RelationGetNamespace(partition),
- CONSTRAINT_FOREIGN,
- fkconstraint->deferrable,
- fkconstraint->initdeferred,
- fkconstraint->initially_valid,
- parentConstr,
- partitionId,
- mapped_fkattnum,
- numfks,
- numfks,
- InvalidOid,
- indexOid,
- RelationGetRelid(pkrel),
- pkattnum,
- pfeqoperators,
- ppeqoperators,
- ffeqoperators,
- numfks,
- fkconstraint->fk_upd_action,
- fkconstraint->fk_del_action,
- fkdelsetcols,
- numfkdelsetcols,
- fkconstraint->fk_matchtype,
- NULL,
- NULL,
- NULL,
- false,
- 1,
- false,
- false);
-
- /*
- * Give this constraint partition-type dependencies on the parent
- * constraint as well as the table.
- */
- ObjectAddressSet(address, ConstraintRelationId, constrOid);
- ObjectAddressSet(referenced, ConstraintRelationId, parentConstr);
- recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_PRI);
- ObjectAddressSet(referenced, RelationRelationId, partitionId);
- recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_SEC);
-
- /* Make all this visible before recursing */
- CommandCounterIncrement();
+ address = addFkConstraint(addFkReferencingSide,
+ fkconstraint->conname, fkconstraint,
+ partition, pkrel, indexOid, parentConstr,
+ numfks, pkattnum,
+ mapped_fkattnum, pfeqoperators,
+ ppeqoperators, ffeqoperators,
+ numfkdelsetcols, fkdelsetcols, true);
/* call ourselves to finalize the creation and we're done */
addFkRecurseReferencing(wqueue, fkconstraint, partition, pkrel,
indexOid,
- constrOid,
+ address.objectId,
numfks,
pkattnum,
mapped_fkattnum,
@@ -10173,6 +10224,7 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
int numfkdelsetcols;
AttrNumber confdelsetcols[INDEX_MAX_KEYS];
Constraint *fkconstraint;
+ ObjectAddress address;
Oid deleteTriggerOid,
updateTriggerOid;
@@ -10206,7 +10258,7 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
* Because we're only expanding the key space at the referenced side,
* we don't need to prevent any operation in the referencing table, so
* AccessShareLock suffices (assumes that dropping the constraint
- * acquires AEL).
+ * acquires AccessExclusiveLock).
*/
fkRel = table_open(constrForm->conrelid, AccessShareLock);
@@ -10272,12 +10324,20 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
constrForm->confrelid, constrForm->conrelid,
&deleteTriggerOid, &updateTriggerOid);
+ /* Add this constraint ... */
+ address = addFkConstraint(addFkReferencedSide,
+ fkconstraint->conname, fkconstraint, fkRel,
+ partitionRel, partIndexId, constrOid,
+ numfks, mapped_confkey,
+ conkey, conpfeqop, conppeqop, conffeqop,
+ numfkdelsetcols, confdelsetcols, false);
+ /* ... and recurse */
addFkRecurseReferenced(NULL,
fkconstraint,
fkRel,
partitionRel,
partIndexId,
- constrOid,
+ address.objectId,
numfks,
mapped_confkey,
conkey,
@@ -10307,8 +10367,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
* child.
*
* If wqueue is given, it is used to set up phase-3 verification for each
- * cloned constraint; if omitted, we assume that such verification is not
- * needed (example: the partition is being created anew).
+ * cloned constraint; omit it if such verification is not needed
+ * (example: the partition is being created anew).
*/
static void
CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
@@ -10391,9 +10451,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
Constraint *fkconstraint;
bool attached;
Oid indexOid;
- Oid constrOid;
- ObjectAddress address,
- referenced;
+ ObjectAddress address;
ListCell *cell;
Oid insertTriggerOid,
updateTriggerOid;
@@ -10490,7 +10548,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
fkconstraint->old_conpfeqop = NIL;
fkconstraint->old_pktable_oid = InvalidOid;
fkconstraint->skip_validation = false;
- fkconstraint->initially_valid = true;
+ fkconstraint->initially_valid = constrForm->convalidated;
for (int i = 0; i < numfks; i++)
{
Form_pg_attribute att;
@@ -10500,71 +10558,29 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
fkconstraint->fk_attrs = lappend(fkconstraint->fk_attrs,
makeString(NameStr(att->attname)));
}
- if (ConstraintNameIsUsed(CONSTRAINT_RELATION,
- RelationGetRelid(partRel),
- NameStr(constrForm->conname)))
- fkconstraint->conname =
- ChooseConstraintName(RelationGetRelationName(partRel),
- ChooseForeignKeyConstraintNameAddition(fkconstraint->fk_attrs),
- "fkey",
- RelationGetNamespace(partRel), NIL);
- else
- fkconstraint->conname = pstrdup(NameStr(constrForm->conname));
indexOid = constrForm->conindid;
- constrOid =
- CreateConstraintEntry(fkconstraint->conname,
- constrForm->connamespace,
- CONSTRAINT_FOREIGN,
- fkconstraint->deferrable,
- fkconstraint->initdeferred,
- constrForm->convalidated,
- parentConstrOid,
- RelationGetRelid(partRel),
- mapped_conkey,
- numfks,
- numfks,
- InvalidOid, /* not a domain constraint */
- indexOid,
- constrForm->confrelid, /* same foreign rel */
- confkey,
- conpfeqop,
- conppeqop,
- conffeqop,
- numfks,
- fkconstraint->fk_upd_action,
- fkconstraint->fk_del_action,
- confdelsetcols,
- numfkdelsetcols,
- fkconstraint->fk_matchtype,
- NULL,
- NULL,
- NULL,
- false, /* islocal */
- 1, /* inhcount */
- false, /* conNoInherit */
- true);
- /* Set up partition dependencies for the new constraint */
- ObjectAddressSet(address, ConstraintRelationId, constrOid);
- ObjectAddressSet(referenced, ConstraintRelationId, parentConstrOid);
- recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_PRI);
- ObjectAddressSet(referenced, RelationRelationId,
- RelationGetRelid(partRel));
- recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_SEC);
+ /* Create the pg_constraint entry at this level */
+ address = addFkConstraint(addFkReferencingSide,
+ NameStr(constrForm->conname), fkconstraint,
+ partRel, pkrel, indexOid, parentConstrOid,
+ numfks, confkey,
+ mapped_conkey, conpfeqop,
+ conppeqop, conffeqop,
+ numfkdelsetcols, confdelsetcols,
+ false);
/* Done with the cloned constraint's tuple */
ReleaseSysCache(tuple);
- /* Make all this visible before recursing */
- CommandCounterIncrement();
-
+ /* Create the check triggers, and recurse to partitions, if any */
addFkRecurseReferencing(wqueue,
fkconstraint,
partRel,
pkrel,
indexOid,
- constrOid,
+ address.objectId,
numfks,
confkey,
mapped_conkey,
@@ -10728,6 +10744,81 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
partRelid);
+ /*
+ * If the referenced table is partitioned, then the partition we're
+ * attaching now has extra pg_constraint rows and action triggers that are
+ * no longer needed. Remove those.
+ */
+ if (get_rel_relkind(fk->confrelid) == RELKIND_PARTITIONED_TABLE)
+ {
+ Relation pg_constraint = table_open(ConstraintRelationId, RowShareLock);
+ ObjectAddresses *objs;
+ HeapTuple consttup;
+
+ ScanKeyInit(&key,
+ Anum_pg_constraint_conrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(fk->conrelid));
+
+ scan = systable_beginscan(pg_constraint,
+ ConstraintRelidTypidNameIndexId,
+ true, NULL, 1, &key);
+ objs = new_object_addresses();
+ while ((consttup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_constraint conform = (Form_pg_constraint) GETSTRUCT(consttup);
+
+ if (conform->conparentid != fk->conoid)
+ continue;
+ else
+ {
+ ObjectAddress addr;
+ SysScanDesc scan2;
+ ScanKeyData key2;
+ int n PG_USED_FOR_ASSERTS_ONLY;
+
+ ObjectAddressSet(addr, ConstraintRelationId, conform->oid);
+ add_exact_object_address(&addr, objs);
+
+ /*
+ * First we must delete the dependency record that binds the
+ * constraint records together.
+ */
+ n = deleteDependencyRecordsForSpecific(ConstraintRelationId,
+ conform->oid,
+ DEPENDENCY_INTERNAL,
+ ConstraintRelationId,
+ fk->conoid);
+ Assert(n == 1); /* actually only one is expected */
+
+ /*
+ * Now search for the triggers for this constraint and set
+ * them up for deletion too
+ */
+ ScanKeyInit(&key2,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conform->oid));
+ scan2 = systable_beginscan(trigrel, TriggerConstraintIndexId,
+ true, NULL, 1, &key2);
+ while ((trigtup = systable_getnext(scan2)) != NULL)
+ {
+ ObjectAddressSet(addr, TriggerRelationId,
+ ((Form_pg_trigger) GETSTRUCT(trigtup))->oid);
+ add_exact_object_address(&addr, objs);
+ }
+ systable_endscan(scan2);
+ }
+ }
+ /* make the dependency deletions visible */
+ CommandCounterIncrement();
+ performMultipleDeletions(objs, DROP_RESTRICT,
+ PERFORM_DELETION_INTERNAL);
+ systable_endscan(scan);
+
+ table_close(pg_constraint, RowShareLock);
+ }
+
CommandCounterIncrement();
return true;
}
@@ -18674,7 +18765,7 @@ ATExecDetachPartition(List **wqueue, AlteredTableInfo *tab, Relation rel,
}
/* Do the final part of detaching */
- DetachPartitionFinalize(rel, partRel, concurrent, defaultPartOid);
+ DetachPartitionFinalize(wqueue, rel, partRel, concurrent, defaultPartOid);
ObjectAddressSet(address, RelationRelationId, RelationGetRelid(partRel));
@@ -18691,8 +18782,8 @@ ATExecDetachPartition(List **wqueue, AlteredTableInfo *tab, Relation rel,
* transaction of the concurrent algorithm fails (crash or abort).
*/
static void
-DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
- Oid defaultPartOid)
+DetachPartitionFinalize(List **wqueue, Relation rel, Relation partRel,
+ bool concurrent, Oid defaultPartOid)
{
Relation classRel;
List *fks;
@@ -18728,8 +18819,11 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
{
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
+ HeapTuple parentConTup;
Form_pg_constraint conform;
+ Form_pg_constraint parentConForm;
Constraint *fkconstraint;
+ Oid parentConstrOid;
Oid insertTriggerOid,
updateTriggerOid;
@@ -18746,7 +18840,20 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
continue;
}
- /* unset conparentid and adjust conislocal, coninhcount, etc. */
+ parentConstrOid = conform->conparentid;
+
+ Assert(OidIsValid(conform->conparentid));
+ parentConTup = SearchSysCache1(CONSTROID,
+ ObjectIdGetDatum(parentConstrOid));
+ if (!HeapTupleIsValid(parentConTup))
+ elog(ERROR, "cache lookup failed for constraint %u",
+ conform->conparentid);
+ parentConForm = (Form_pg_constraint) GETSTRUCT(parentConTup);
+
+ /*
+ * The constraint on this table must be marked no longer a child of
+ * the parent's constraint, as do its check triggers.
+ */
ConstraintSetParentConstraint(fk->conoid, InvalidOid, InvalidOid);
/*
@@ -18764,35 +18871,87 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
RelationGetRelid(partRel));
/*
- * Make the action triggers on the referenced relation. When this was
- * a partition the action triggers pointed to the parent rel (they
- * still do), but now we need separate ones of our own.
+ * If the referenced side is partitioned (which we know because our
+ * parent's constraint points to a different relation than ours) then
+ * we must, in addition to the above, create pg_constraint rows that
+ * point to each partition, each with its own action triggers.
*/
- fkconstraint = makeNode(Constraint);
- fkconstraint->contype = CONSTRAINT_FOREIGN;
- fkconstraint->conname = pstrdup(NameStr(conform->conname));
- fkconstraint->deferrable = conform->condeferrable;
- fkconstraint->initdeferred = conform->condeferred;
- fkconstraint->location = -1;
- fkconstraint->pktable = NULL;
- fkconstraint->fk_attrs = NIL;
- fkconstraint->pk_attrs = NIL;
- fkconstraint->fk_matchtype = conform->confmatchtype;
- fkconstraint->fk_upd_action = conform->confupdtype;
- fkconstraint->fk_del_action = conform->confdeltype;
- fkconstraint->fk_del_set_cols = NIL;
- fkconstraint->old_conpfeqop = NIL;
- fkconstraint->old_pktable_oid = InvalidOid;
- fkconstraint->skip_validation = false;
- fkconstraint->initially_valid = true;
+ if (parentConForm->conrelid != conform->conrelid)
+ {
+ int numfks;
+ AttrNumber conkey[INDEX_MAX_KEYS];
+ AttrMap *attmap;
+ AttrNumber confkey[INDEX_MAX_KEYS];
+ Oid conpfeqop[INDEX_MAX_KEYS];
+ Oid conppeqop[INDEX_MAX_KEYS];
+ Oid conffeqop[INDEX_MAX_KEYS];
+ int numfkdelsetcols;
+ AttrNumber confdelsetcols[INDEX_MAX_KEYS];
+ Relation refdRel;
- createForeignKeyActionTriggers(partRel, conform->confrelid,
- fkconstraint, fk->conoid,
- conform->conindid,
- InvalidOid, InvalidOid,
- NULL, NULL);
+ DeconstructFkConstraintRow(contup,
+ &numfks,
+ conkey,
+ confkey,
+ conpfeqop,
+ conppeqop,
+ conffeqop,
+ &numfkdelsetcols,
+ confdelsetcols);
+
+ /* Create a synthetic node we'll use throughout */
+ fkconstraint = makeNode(Constraint);
+ fkconstraint->contype = CONSTRAINT_FOREIGN;
+ fkconstraint->conname = pstrdup(NameStr(conform->conname));
+ fkconstraint->deferrable = conform->condeferrable;
+ fkconstraint->initdeferred = conform->condeferred;
+ fkconstraint->skip_validation = true;
+ fkconstraint->initially_valid = true;
+ /* a few irrelevant fields omitted here */
+ fkconstraint->pktable = NULL;
+ fkconstraint->fk_attrs = NIL;
+ fkconstraint->pk_attrs = NIL;
+ fkconstraint->fk_matchtype = conform->confmatchtype;
+ fkconstraint->fk_upd_action = conform->confupdtype;
+ fkconstraint->fk_del_action = conform->confdeltype;
+ fkconstraint->fk_del_set_cols = NIL;
+ fkconstraint->old_conpfeqop = NIL;
+ fkconstraint->old_pktable_oid = InvalidOid;
+ fkconstraint->location = -1;
+
+ attmap = build_attrmap_by_name(RelationGetDescr(partRel),
+ RelationGetDescr(rel));
+ for (int i = 0; i < numfks; i++)
+ {
+ Form_pg_attribute att;
+
+ att = TupleDescAttr(RelationGetDescr(partRel),
+ attmap->attnums[conkey[i] - 1] - 1);
+ fkconstraint->fk_attrs = lappend(fkconstraint->fk_attrs,
+ makeString(NameStr(att->attname)));
+ }
+
+ refdRel = table_open(fk->confrelid, AccessShareLock);
+
+ addFkRecurseReferenced(wqueue, fkconstraint, partRel,
+ refdRel,
+ conform->conindid,
+ fk->conoid,
+ numfks,
+ confkey,
+ conkey,
+ conpfeqop,
+ conppeqop,
+ conffeqop,
+ numfkdelsetcols,
+ confdelsetcols,
+ true,
+ InvalidOid, InvalidOid);
+ table_close(refdRel, AccessShareLock);
+ }
ReleaseSysCache(contup);
+ ReleaseSysCache(parentConTup);
}
list_free_deep(fks);
if (trigrel)
@@ -18927,7 +19086,7 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
* completion; this completes the detaching process.
*/
static ObjectAddress
-ATExecDetachPartitionFinalize(Relation rel, RangeVar *name)
+ATExecDetachPartitionFinalize(List **wqueue, Relation rel, RangeVar *name)
{
Relation partRel;
ObjectAddress address;
@@ -18945,7 +19104,7 @@ ATExecDetachPartitionFinalize(Relation rel, RangeVar *name)
*/
WaitForOlderSnapshots(snap->xmin, false);
- DetachPartitionFinalize(rel, partRel, true, InvalidOid);
+ DetachPartitionFinalize(wqueue, rel, partRel, true, InvalidOid);
ObjectAddressSet(address, RelationRelationId, RelationGetRelid(partRel));
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 5f4d6f84edf..02cf78ee26a 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -2934,3 +2934,99 @@ DETAIL: drop cascades to table fkpart11.pk
drop cascades to table fkpart11.fk_parted
drop cascades to table fkpart11.fk_another
drop cascades to function fkpart11.print_row()
+-- When a table is attached as partition to a partitioned table that has
+-- a foreign key to another partitioned table, it acquires a clone of the
+-- FK. Upon detach, this clone is not removed, but instead becomes an
+-- independent FK. If it then attaches to the partitioned table again,
+-- the FK from the parent "takes over" ownership of the independent FK rather
+-- than creating a separate one.
+CREATE SCHEMA fkpart12
+ CREATE TABLE fk_p ( id int, jd int, PRIMARY KEY(id, jd)) PARTITION BY list (id)
+ CREATE TABLE fk_p_1 PARTITION OF fk_p FOR VALUES IN (1) PARTITION BY list (jd)
+ CREATE TABLE fk_p_1_1 PARTITION OF fk_p_1 FOR VALUES IN (1)
+ CREATE TABLE fk_p_1_2 PARTITION OF fk_p_1 FOR VALUES IN (2)
+ CREATE TABLE fk_p_2 PARTITION OF fk_p FOR VALUES IN (2) PARTITION BY list (jd)
+ CREATE TABLE fk_p_2_1 PARTITION OF fk_p_2 FOR VALUES IN (1)
+ CREATE TABLE fk_p_2_2 PARTITION OF fk_p_2 FOR VALUES IN (2)
+ CREATE TABLE fk_r_1 ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL)
+ CREATE TABLE fk_r_2 ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL) PARTITION BY list (id)
+ CREATE TABLE fk_r_2_1 PARTITION OF fk_r_2 FOR VALUES IN (2, 1)
+ CREATE TABLE fk_r ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL,
+ FOREIGN KEY (p_id, p_jd) REFERENCES fk_p (id, jd)
+ ) PARTITION BY list (id);
+SET search_path TO fkpart12;
+INSERT INTO fk_p VALUES (1, 1);
+ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1);
+ALTER TABLE fk_r ATTACH PARTITION fk_r_2 FOR VALUES IN (2);
+\d fk_r_2
+ Partitioned table "fkpart12.fk_r_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ id | integer | | not null |
+ p_id | integer | | not null |
+ p_jd | integer | | not null |
+Partition of: fk_r FOR VALUES IN (2)
+Partition key: LIST (id)
+Indexes:
+ "fk_r_2_pkey" PRIMARY KEY, btree (id)
+Foreign-key constraints:
+ TABLE "fk_r" CONSTRAINT "fk_r_p_id_p_jd_fkey" FOREIGN KEY (p_id, p_jd) REFERENCES fk_p(id, jd)
+Number of partitions: 1 (Use \d+ to list them.)
+
+INSERT INTO fk_r VALUES (1, 1, 1);
+INSERT INTO fk_r VALUES (2, 2, 1);
+ERROR: insert or update on table "fk_r_2_1" violates foreign key constraint "fk_r_p_id_p_jd_fkey"
+DETAIL: Key (p_id, p_jd)=(2, 1) is not present in table "fk_p".
+ALTER TABLE fk_r DETACH PARTITION fk_r_1;
+ALTER TABLE fk_r DETACH PARTITION fk_r_2;
+\d fk_r_2
+ Partitioned table "fkpart12.fk_r_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ id | integer | | not null |
+ p_id | integer | | not null |
+ p_jd | integer | | not null |
+Partition key: LIST (id)
+Indexes:
+ "fk_r_2_pkey" PRIMARY KEY, btree (id)
+Foreign-key constraints:
+ "fk_r_p_id_p_jd_fkey" FOREIGN KEY (p_id, p_jd) REFERENCES fk_p(id, jd)
+Number of partitions: 1 (Use \d+ to list them.)
+
+INSERT INTO fk_r_1 VALUES (2, 1, 2); -- should fail
+ERROR: insert or update on table "fk_r_1" violates foreign key constraint "fk_r_p_id_p_jd_fkey"
+DETAIL: Key (p_id, p_jd)=(1, 2) is not present in table "fk_p".
+DELETE FROM fk_p; -- should fail
+ERROR: update or delete on table "fk_p_1_1" violates foreign key constraint "fk_r_1_p_id_p_jd_fkey1" on table "fk_r_1"
+DETAIL: Key (id, jd)=(1, 1) is still referenced from table "fk_r_1".
+ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1);
+ALTER TABLE fk_r ATTACH PARTITION fk_r_2 FOR VALUES IN (2);
+\d fk_r_2
+ Partitioned table "fkpart12.fk_r_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ id | integer | | not null |
+ p_id | integer | | not null |
+ p_jd | integer | | not null |
+Partition of: fk_r FOR VALUES IN (2)
+Partition key: LIST (id)
+Indexes:
+ "fk_r_2_pkey" PRIMARY KEY, btree (id)
+Foreign-key constraints:
+ TABLE "fk_r" CONSTRAINT "fk_r_p_id_p_jd_fkey" FOREIGN KEY (p_id, p_jd) REFERENCES fk_p(id, jd)
+Number of partitions: 1 (Use \d+ to list them.)
+
+DELETE FROM fk_p; -- should fail
+ERROR: update or delete on table "fk_p_1_1" violates foreign key constraint "fk_r_p_id_p_jd_fkey2" on table "fk_r"
+DETAIL: Key (id, jd)=(1, 1) is still referenced from table "fk_r".
+-- these should all fail
+ALTER TABLE fk_r_1 DROP CONSTRAINT fk_r_p_id_p_jd_fkey;
+ERROR: cannot drop inherited constraint "fk_r_p_id_p_jd_fkey" of relation "fk_r_1"
+ALTER TABLE fk_r DROP CONSTRAINT fk_r_p_id_p_jd_fkey1;
+ERROR: cannot drop inherited constraint "fk_r_p_id_p_jd_fkey1" of relation "fk_r"
+ALTER TABLE fk_r_2 DROP CONSTRAINT fk_r_p_id_p_jd_fkey;
+ERROR: cannot drop inherited constraint "fk_r_p_id_p_jd_fkey" of relation "fk_r_2"
+SET client_min_messages TO warning;
+DROP SCHEMA fkpart12 CASCADE;
+RESET client_min_messages;
+RESET search_path;
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index bb39c9719a9..9f424f2e143 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -2086,3 +2086,60 @@ UPDATE fkpart11.pk SET a = 3 WHERE a = 4;
UPDATE fkpart11.pk SET a = 1 WHERE a = 2;
DROP SCHEMA fkpart11 CASCADE;
+
+-- When a table is attached as partition to a partitioned table that has
+-- a foreign key to another partitioned table, it acquires a clone of the
+-- FK. Upon detach, this clone is not removed, but instead becomes an
+-- independent FK. If it then attaches to the partitioned table again,
+-- the FK from the parent "takes over" ownership of the independent FK rather
+-- than creating a separate one.
+CREATE SCHEMA fkpart12
+ CREATE TABLE fk_p ( id int, jd int, PRIMARY KEY(id, jd)) PARTITION BY list (id)
+ CREATE TABLE fk_p_1 PARTITION OF fk_p FOR VALUES IN (1) PARTITION BY list (jd)
+ CREATE TABLE fk_p_1_1 PARTITION OF fk_p_1 FOR VALUES IN (1)
+ CREATE TABLE fk_p_1_2 PARTITION OF fk_p_1 FOR VALUES IN (2)
+ CREATE TABLE fk_p_2 PARTITION OF fk_p FOR VALUES IN (2) PARTITION BY list (jd)
+ CREATE TABLE fk_p_2_1 PARTITION OF fk_p_2 FOR VALUES IN (1)
+ CREATE TABLE fk_p_2_2 PARTITION OF fk_p_2 FOR VALUES IN (2)
+ CREATE TABLE fk_r_1 ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL)
+ CREATE TABLE fk_r_2 ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL) PARTITION BY list (id)
+ CREATE TABLE fk_r_2_1 PARTITION OF fk_r_2 FOR VALUES IN (2, 1)
+ CREATE TABLE fk_r ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL,
+ FOREIGN KEY (p_id, p_jd) REFERENCES fk_p (id, jd)
+ ) PARTITION BY list (id);
+SET search_path TO fkpart12;
+
+INSERT INTO fk_p VALUES (1, 1);
+
+ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1);
+ALTER TABLE fk_r ATTACH PARTITION fk_r_2 FOR VALUES IN (2);
+
+\d fk_r_2
+
+INSERT INTO fk_r VALUES (1, 1, 1);
+INSERT INTO fk_r VALUES (2, 2, 1);
+
+ALTER TABLE fk_r DETACH PARTITION fk_r_1;
+ALTER TABLE fk_r DETACH PARTITION fk_r_2;
+
+\d fk_r_2
+
+INSERT INTO fk_r_1 VALUES (2, 1, 2); -- should fail
+DELETE FROM fk_p; -- should fail
+
+ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1);
+ALTER TABLE fk_r ATTACH PARTITION fk_r_2 FOR VALUES IN (2);
+
+\d fk_r_2
+
+DELETE FROM fk_p; -- should fail
+
+-- these should all fail
+ALTER TABLE fk_r_1 DROP CONSTRAINT fk_r_p_id_p_jd_fkey;
+ALTER TABLE fk_r DROP CONSTRAINT fk_r_p_id_p_jd_fkey1;
+ALTER TABLE fk_r_2 DROP CONSTRAINT fk_r_p_id_p_jd_fkey;
+
+SET client_min_messages TO warning;
+DROP SCHEMA fkpart12 CASCADE;
+RESET client_min_messages;
+RESET search_path;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index e4dbc435967..387f2234243 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -3075,6 +3075,7 @@ _locale_t
_resultmap
_stringlist
acquireLocksOnSubLinks_context
+addFkConstraintSides
adjust_appendrel_attrs_context
aff_regex_struct
allocfunc
--
2.39.5
v4_16-0001-Redo-foreign-key-maintenance-during-partition-.patchtext/x-diff; charset=utf-8Download
From 752bcd855724471abd0dda0a4c9e81ba9ba85009 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=81lvaro=20Herrera?= <alvherre@alvh.no-ip.org>
Date: Fri, 18 Oct 2024 16:40:35 +0200
Subject: [PATCH v4_16] Redo foreign key maintenance during partition
ATTACH/DETACH
... when the reference table is partitioned.
It turns out that the catalog representation we chose for foreign keys
connecting partitioned tables is suboptimal, particularly in the sense
that a standalone table has a different way to represent it when
referencing a partitioned table, than when the same table becomes a
partition (and vice versa). This difference means we need to create
additional catalog rows on detach (pg_constraint and pg_trigger), and
remove them on attach.
As a very obvious symptom, we were missing action triggers after
detach, which means that you could update/delete rows from the
referenced partitioned table that still had referencing rows at the
other side previously detached, and fail to throw the required errors.
!!!!!!!!!
Note that this means existing FKs that refer to partitioned tables might
have rows that break relational integrity, on tables that were once
partitions.
Another possible problem is that trying to reattach a table
that had been detached would fail indicating that internal triggers
cannot be found, which from the user's point of view is nonsensical.
In branches 15 and above, we fix this by creating a new helper function
addFkConstraint() which is in charge of creating a standalone
pg_constraint row, and repurposing addFkRecurseReferencing() and
addFkRecurseReferenced() so that they're only the recursive routine for
each side of the FK, and they call addFkConstraint() to create
pg_constraint at each partitioning level and add the necessary triggers.
These new routines can be used during partition creation, partition
attach and detach, and foreign key creation. This reduces redundant
code and simplifies the flow.
In branches 14 and 13, we have a much simpler fix that consists on
simply removing the constraint on detach. The reason is that those
branches are missing commit f4566345cf40, which reworked the way this
works in a way that we didn't consider back-patchable at the time. I
doubt we want to change that now, so this is probably the best we can do
there.
We opted to leave branch 12 alone, because it's going to end maintenance
very soon, and any rushed patch might be worse.
In the future we might want to rethink the catalog representation to
avoid this problem, but for now the code seems to do what's required to
make the constraints operate correctly.
Co-authored-by: Jehan-Guillaume de Rorthais <jgdr@dalibo.com>
Co-authored-by: Tender Wang <tndrwang@gmail.com>
Co-authored-by: Alvaro Herrera <alvherre@alvh.no-ip.org>
Reported-by: Guillaume Lelarge <guillaume@lelarge.info>
Reported-by: Jehan-Guillaume de Rorthais <jgdr@dalibo.com>
Reported-by: Thomas Baehler (SBB CFF FFS) <thomas.baehler2@sbb.ch>
Discussion: https://postgr.es/m/20230420144344.40744130@karst
Discussion: https://postgr.es/m/20230705233028.2f554f73@karst
Discussion: https://postgr.es/m/GVAP278MB02787E7134FD691861635A8BC9032@GVAP278MB0278.CHEP278.PROD.OUTLOOK.COM
Discussion: https://postgr.es/m/18541-628a61bc267cd2d3@postgresql.org
---
src/backend/commands/tablecmds.c | 630 ++++++++++++++--------
src/test/regress/expected/foreign_key.out | 96 ++++
src/test/regress/sql/foreign_key.sql | 57 ++
src/tools/pgindent/typedefs.list | 1 +
4 files changed, 549 insertions(+), 235 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 568ad96834e..4a5bb87a12c 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -336,6 +336,14 @@ typedef struct ForeignTruncateInfo
List *rels;
} ForeignTruncateInfo;
+/* Partial or complete FK creation in addFkConstraint() */
+typedef enum addFkConstraintSides
+{
+ addFkReferencedSide,
+ addFkReferencingSide,
+ addFkBothSides,
+} addFkConstraintSides;
+
/*
* Partition tables are expected to be dropped when the parent partitioned
* table gets dropped. Hence for partitioning we use AUTO dependency.
@@ -490,16 +498,25 @@ static ObjectAddress ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *
Relation rel, Constraint *fkconstraint,
bool recurse, bool recursing,
LOCKMODE lockmode);
-static ObjectAddress addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint,
- Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
- int numfks, int16 *pkattnum, int16 *fkattnum,
- Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
- int numfkdelsetcols, int16 *fkdelsetcols,
- bool old_check_ok,
- Oid parentDelTrigger, Oid parentUpdTrigger);
static void validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
int numfksetcols, const int16 *fksetcolsattnums,
List *fksetcols);
+static ObjectAddress addFkConstraint(addFkConstraintSides fkside,
+ char *constraintname,
+ Constraint *fkconstraint, Relation rel,
+ Relation pkrel, Oid indexOid,
+ Oid parentConstr,
+ int numfks, int16 *pkattnum, int16 *fkattnum,
+ Oid *pfeqoperators, Oid *ppeqoperators,
+ Oid *ffeqoperators, int numfkdelsetcols,
+ int16 *fkdelsetcols, bool is_internal);
+static void addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint,
+ Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
+ int numfks, int16 *pkattnum, int16 *fkattnum,
+ Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
+ int numfkdelsetcols, int16 *fkdelsetcols,
+ bool old_check_ok,
+ Oid parentDelTrigger, Oid parentUpdTrigger);
static void addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint,
Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks, int16 *pkattnum, int16 *fkattnum,
@@ -627,9 +644,11 @@ static void DropClonedTriggersFromPartition(Oid partitionId);
static ObjectAddress ATExecDetachPartition(List **wqueue, AlteredTableInfo *tab,
Relation rel, RangeVar *name,
bool concurrent);
-static void DetachPartitionFinalize(Relation rel, Relation partRel,
- bool concurrent, Oid defaultPartOid);
-static ObjectAddress ATExecDetachPartitionFinalize(Relation rel, RangeVar *name);
+static void DetachPartitionFinalize(List **wqueue, Relation rel,
+ Relation partRel, bool concurrent,
+ Oid defaultPartOid);
+static ObjectAddress ATExecDetachPartitionFinalize(List **wqueue, Relation rel,
+ RangeVar *name);
static ObjectAddress ATExecAttachPartitionIdx(List **wqueue, Relation parentIdx,
RangeVar *name);
static void validatePartitionedIndex(Relation partedIdx, Relation partedTbl);
@@ -5344,7 +5363,7 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
((PartitionCmd *) cmd->def)->concurrent);
break;
case AT_DetachPartitionFinalize:
- address = ATExecDetachPartitionFinalize(rel, ((PartitionCmd *) cmd->def)->name);
+ address = ATExecDetachPartitionFinalize(wqueue, rel, ((PartitionCmd *) cmd->def)->name);
break;
default: /* oops */
elog(ERROR, "unrecognized alter table type: %d",
@@ -9555,25 +9574,37 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
ffeqoperators[i] = ffeqop;
}
- /*
- * Create all the constraint and trigger objects, recursing to partitions
- * as necessary. First handle the referenced side.
- */
- address = addFkRecurseReferenced(wqueue, fkconstraint, rel, pkrel,
- indexOid,
- InvalidOid, /* no parent constraint */
- numfks,
- pkattnum,
- fkattnum,
- pfeqoperators,
- ppeqoperators,
- ffeqoperators,
- numfkdelsetcols,
- fkdelsetcols,
- old_check_ok,
- InvalidOid, InvalidOid);
+ /* First, create the constraint catalog entry itself. */
+ address = addFkConstraint(addFkBothSides,
+ fkconstraint->conname, fkconstraint, rel, pkrel,
+ indexOid,
+ InvalidOid, /* no parent constraint */
+ numfks,
+ pkattnum,
+ fkattnum,
+ pfeqoperators,
+ ppeqoperators,
+ ffeqoperators,
+ numfkdelsetcols,
+ fkdelsetcols,
+ false);
- /* Now handle the referencing side. */
+ /* Next process the action triggers at the referenced side and recurse */
+ addFkRecurseReferenced(wqueue, fkconstraint, rel, pkrel,
+ indexOid,
+ address.objectId,
+ numfks,
+ pkattnum,
+ fkattnum,
+ pfeqoperators,
+ ppeqoperators,
+ ffeqoperators,
+ numfkdelsetcols,
+ fkdelsetcols,
+ old_check_ok,
+ InvalidOid, InvalidOid);
+
+ /* Lastly create the check triggers at the referencing side and recurse */
addFkRecurseReferencing(wqueue, fkconstraint, rel, pkrel,
indexOid,
address.objectId,
@@ -9633,46 +9664,43 @@ validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
}
/*
- * addFkRecurseReferenced
- * subroutine for ATAddForeignKeyConstraint; recurses on the referenced
- * side of the constraint
+ * addFkConstraint
+ * Install pg_constraint entries to implement a foreign key constraint.
+ * Caller must separately invoke addFkRecurseReferenced and
+ * addFkRecurseReferencing, as appropriate, to install pg_trigger entries
+ * and (for partitioned tables) recurse to partitions.
*
- * Create pg_constraint rows for the referenced side of the constraint,
- * referencing the parent of the referencing side; also create action triggers
- * on leaf partitions. If the table is partitioned, recurse to handle each
- * partition.
- *
- * wqueue is the ALTER TABLE work queue; can be NULL when not running as part
- * of an ALTER TABLE sequence.
- * fkconstraint is the constraint being added.
- * rel is the root referencing relation.
- * pkrel is the referenced relation; might be a partition, if recursing.
- * indexOid is the OID of the index (on pkrel) implementing this constraint.
- * parentConstr is the OID of a parent constraint; InvalidOid if this is a
- * top-level constraint.
- * numfks is the number of columns in the foreign key
- * pkattnum is the attnum array of referenced attributes.
- * fkattnum is the attnum array of referencing attributes.
- * numfkdelsetcols is the number of columns in the ON DELETE SET NULL/DEFAULT
- * (...) clause
- * fkdelsetcols is the attnum array of the columns in the ON DELETE SET
- * NULL/DEFAULT clause
- * pf/pp/ffeqoperators are OID array of operators between columns.
- * old_check_ok signals that this constraint replaces an existing one that
- * was already validated (thus this one doesn't need validation).
- * parentDelTrigger and parentUpdTrigger, when being recursively called on
- * a partition, are the OIDs of the parent action triggers for DELETE and
- * UPDATE respectively.
+ * * fkside indicates which side of the FK (or both) to create. Caller must
+ * call addFkRecurseReferenced if it's addFkReferencedSide,
+ * addFkRecurseReferencing if it's addFkReferencingSide, or both if it's
+ * addFkBothSides.
+ * * constraintname the base name for the constraint being added. This is
+ * copied to fkconstraint->conname if the latter is not set.
+ * * fkconstraint is the constraint being added.
+ * * rel is the root referencing relation.
+ * * pkrel is the referenced relation; might be a partition, if recursing.
+ * * indexOid is the OID of the index (on pkrel) implementing this constraint.
+ * * parentConstr is the OID of a parent constraint; InvalidOid if this is a
+ * top-level constraint.
+ * * numfks is the number of columns in the foreign key.
+ * * pkattnum is the attnum array of referenced attributes.
+ * * fkattnum is the attnum array of referencing attributes.
+ * * pf/pp/ffeqoperators are OID array of operators between columns.
+ * * numfkdelsetcols is the number of columns in the ON DELETE SET NULL/DEFAULT
+ * (...) clause.
+ * * fkdelsetcols is the attnum array of the columns in the ON DELETE SET
+ * NULL/DEFAULT clause.
+ * * is_internal: whether to mark the constraint as internal (used during
+ * recursion)
*/
static ObjectAddress
-addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
- Relation pkrel, Oid indexOid, Oid parentConstr,
- int numfks,
- int16 *pkattnum, int16 *fkattnum, Oid *pfeqoperators,
- Oid *ppeqoperators, Oid *ffeqoperators,
- int numfkdelsetcols, int16 *fkdelsetcols,
- bool old_check_ok,
- Oid parentDelTrigger, Oid parentUpdTrigger)
+addFkConstraint(addFkConstraintSides fkside,
+ char *constraintname, Constraint *fkconstraint,
+ Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
+ int numfks, int16 *pkattnum,
+ int16 *fkattnum, Oid *pfeqoperators, Oid *ppeqoperators,
+ Oid *ffeqoperators, int numfkdelsetcols, int16 *fkdelsetcols,
+ bool is_internal)
{
ObjectAddress address;
Oid constrOid;
@@ -9680,8 +9708,6 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
bool conislocal;
int coninhcount;
bool connoinherit;
- Oid deleteTriggerOid,
- updateTriggerOid;
/*
* Verify relkind for each referenced partition. At the top level, this
@@ -9700,13 +9726,16 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
*/
if (ConstraintNameIsUsed(CONSTRAINT_RELATION,
RelationGetRelid(rel),
- fkconstraint->conname))
+ constraintname))
conname = ChooseConstraintName(RelationGetRelationName(rel),
ChooseForeignKeyConstraintNameAddition(fkconstraint->fk_attrs),
"fkey",
RelationGetNamespace(rel), NIL);
else
- conname = fkconstraint->conname;
+ conname = constraintname;
+
+ if (fkconstraint->conname == NULL)
+ fkconstraint->conname = pstrdup(conname);
if (OidIsValid(parentConstr))
{
@@ -9758,33 +9787,95 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
conislocal, /* islocal */
coninhcount, /* inhcount */
connoinherit, /* conNoInherit */
- false); /* is_internal */
+ is_internal); /* is_internal */
ObjectAddressSet(address, ConstraintRelationId, constrOid);
- /*
- * Mark the child constraint as part of the parent constraint; it must not
- * be dropped on its own. (This constraint is deleted when the partition
- * is detached, but a special check needs to occur that the partition
- * contains no referenced values.)
- */
if (OidIsValid(parentConstr))
{
ObjectAddress referenced;
ObjectAddressSet(referenced, ConstraintRelationId, parentConstr);
- recordDependencyOn(&address, &referenced, DEPENDENCY_INTERNAL);
+ if (fkside == addFkReferencedSide)
+ {
+ /*
+ * Mark the child constraint as part of the parent constraint; it
+ * must not be dropped on its own. (This constraint is deleted
+ * when the partition is detached, but a special check needs to
+ * occur that the partition contains no referenced values.)
+ */
+ recordDependencyOn(&address, &referenced, DEPENDENCY_INTERNAL);
+ }
+ else
+ {
+ /*
+ * Give this constraint partition-type dependencies on the parent
+ * constraint as well as the table.
+ */
+ recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_PRI);
+ ObjectAddressSet(referenced, RelationRelationId, RelationGetRelid(rel));
+ recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_SEC);
+ }
}
/* make new constraint visible, in case we add more */
CommandCounterIncrement();
+ return address;
+}
+
+/*
+ * addFkRecurseReferenced
+ * Recursive helper for the referenced side of foreign key creation,
+ * which creates the action triggers and recurses
+ *
+ * If the referenced relation is a plain relation, create the necessary action
+ * triggers that implement the constraint. If the referenced relation is a
+ * partitioned table, then we create a pg_constraint row referencing the parent
+ * of the referencing side for it and recurse on this routine for each
+ * partition.
+ *
+ * wqueue is the ALTER TABLE work queue; can be NULL when not running as part
+ * of an ALTER TABLE sequence.
+ * fkconstraint is the constraint being added.
+ * rel is the root referencing relation.
+ * pkrel is the referenced relation; might be a partition, if recursing.
+ * indexOid is the OID of the index (on pkrel) implementing this constraint.
+ * parentConstr is the OID of a parent constraint; InvalidOid if this is a
+ * top-level constraint.
+ * numfks is the number of columns in the foreign key
+ * pkattnum is the attnum array of referenced attributes.
+ * fkattnum is the attnum array of referencing attributes.
+ * numfkdelsetcols is the number of columns in the ON DELETE SET NULL/DEFAULT
+ * (...) clause
+ * fkdelsetcols is the attnum array of the columns in the ON DELETE SET
+ * NULL/DEFAULT clause
+ * pf/pp/ffeqoperators are OID array of operators between columns.
+ * old_check_ok signals that this constraint replaces an existing one that
+ * was already validated (thus this one doesn't need validation).
+ * parentDelTrigger and parentUpdTrigger, when being recursively called on
+ * a partition, are the OIDs of the parent action triggers for DELETE and
+ * UPDATE respectively.
+ */
+static void
+addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
+ Relation pkrel, Oid indexOid, Oid parentConstr,
+ int numfks,
+ int16 *pkattnum, int16 *fkattnum, Oid *pfeqoperators,
+ Oid *ppeqoperators, Oid *ffeqoperators,
+ int numfkdelsetcols, int16 *fkdelsetcols,
+ bool old_check_ok,
+ Oid parentDelTrigger, Oid parentUpdTrigger)
+{
+ Oid deleteTriggerOid,
+ updateTriggerOid;
+
/*
* Create the action triggers that enforce the constraint.
*/
createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
fkconstraint,
- constrOid, indexOid,
+ parentConstr, indexOid,
parentDelTrigger, parentUpdTrigger,
&deleteTriggerOid, &updateTriggerOid);
@@ -9803,6 +9894,7 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
AttrMap *map;
AttrNumber *mapped_pkattnum;
Oid partIndexId;
+ ObjectAddress address;
partRel = table_open(pd->oids[i], ShareRowExclusiveLock);
@@ -9822,13 +9914,23 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
else
mapped_pkattnum = pkattnum;
- /* do the deed */
+ /* Determine the index to use at this level */
partIndexId = index_get_partition(partRel, indexOid);
if (!OidIsValid(partIndexId))
elog(ERROR, "index for %u not found in partition %s",
indexOid, RelationGetRelationName(partRel));
+
+ /* Create entry at this level ... */
+ address = addFkConstraint(addFkReferencedSide,
+ fkconstraint->conname, fkconstraint, rel,
+ partRel, partIndexId, parentConstr,
+ numfks, mapped_pkattnum,
+ fkattnum, pfeqoperators, ppeqoperators,
+ ffeqoperators, numfkdelsetcols,
+ fkdelsetcols, true);
+ /* ... and recurse to our children */
addFkRecurseReferenced(wqueue, fkconstraint, rel, partRel,
- partIndexId, constrOid, numfks,
+ partIndexId, address.objectId, numfks,
mapped_pkattnum, fkattnum,
pfeqoperators, ppeqoperators, ffeqoperators,
numfkdelsetcols, fkdelsetcols,
@@ -9844,13 +9946,12 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
}
}
}
-
- return address;
}
/*
* addFkRecurseReferencing
- * subroutine for ATAddForeignKeyConstraint and CloneFkReferencing
+ * Recursive helper for the referencing side of foreign key creation,
+ * which creates the check triggers and recurses
*
* If the referencing relation is a plain relation, create the necessary check
* triggers that implement the constraint, and set up for Phase 3 constraint
@@ -9970,10 +10071,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
AttrMap *attmap;
AttrNumber mapped_fkattnum[INDEX_MAX_KEYS];
bool attached;
- char *conname;
- Oid constrOid;
- ObjectAddress address,
- referenced;
+ ObjectAddress address;
ListCell *cell;
CheckAlterTableIsSafe(partition);
@@ -10016,65 +10114,18 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
/*
* No luck finding a good constraint to reuse; create our own.
*/
- if (ConstraintNameIsUsed(CONSTRAINT_RELATION,
- RelationGetRelid(partition),
- fkconstraint->conname))
- conname = ChooseConstraintName(RelationGetRelationName(partition),
- ChooseForeignKeyConstraintNameAddition(fkconstraint->fk_attrs),
- "fkey",
- RelationGetNamespace(partition), NIL);
- else
- conname = fkconstraint->conname;
- constrOid =
- CreateConstraintEntry(conname,
- RelationGetNamespace(partition),
- CONSTRAINT_FOREIGN,
- fkconstraint->deferrable,
- fkconstraint->initdeferred,
- fkconstraint->initially_valid,
- parentConstr,
- partitionId,
- mapped_fkattnum,
- numfks,
- numfks,
- InvalidOid,
- indexOid,
- RelationGetRelid(pkrel),
- pkattnum,
- pfeqoperators,
- ppeqoperators,
- ffeqoperators,
- numfks,
- fkconstraint->fk_upd_action,
- fkconstraint->fk_del_action,
- fkdelsetcols,
- numfkdelsetcols,
- fkconstraint->fk_matchtype,
- NULL,
- NULL,
- NULL,
- false,
- 1,
- false,
- false);
-
- /*
- * Give this constraint partition-type dependencies on the parent
- * constraint as well as the table.
- */
- ObjectAddressSet(address, ConstraintRelationId, constrOid);
- ObjectAddressSet(referenced, ConstraintRelationId, parentConstr);
- recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_PRI);
- ObjectAddressSet(referenced, RelationRelationId, partitionId);
- recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_SEC);
-
- /* Make all this visible before recursing */
- CommandCounterIncrement();
+ address = addFkConstraint(addFkReferencingSide,
+ fkconstraint->conname, fkconstraint,
+ partition, pkrel, indexOid, parentConstr,
+ numfks, pkattnum,
+ mapped_fkattnum, pfeqoperators,
+ ppeqoperators, ffeqoperators,
+ numfkdelsetcols, fkdelsetcols, true);
/* call ourselves to finalize the creation and we're done */
addFkRecurseReferencing(wqueue, fkconstraint, partition, pkrel,
indexOid,
- constrOid,
+ address.objectId,
numfks,
pkattnum,
mapped_fkattnum,
@@ -10207,6 +10258,7 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
int numfkdelsetcols;
AttrNumber confdelsetcols[INDEX_MAX_KEYS];
Constraint *fkconstraint;
+ ObjectAddress address;
Oid deleteTriggerOid,
updateTriggerOid;
@@ -10240,7 +10292,7 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
* Because we're only expanding the key space at the referenced side,
* we don't need to prevent any operation in the referencing table, so
* AccessShareLock suffices (assumes that dropping the constraint
- * acquires AEL).
+ * acquires AccessExclusiveLock).
*/
fkRel = table_open(constrForm->conrelid, AccessShareLock);
@@ -10306,12 +10358,20 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
constrForm->confrelid, constrForm->conrelid,
&deleteTriggerOid, &updateTriggerOid);
+ /* Add this constraint ... */
+ address = addFkConstraint(addFkReferencedSide,
+ fkconstraint->conname, fkconstraint, fkRel,
+ partitionRel, partIndexId, constrOid,
+ numfks, mapped_confkey,
+ conkey, conpfeqop, conppeqop, conffeqop,
+ numfkdelsetcols, confdelsetcols, false);
+ /* ... and recurse */
addFkRecurseReferenced(NULL,
fkconstraint,
fkRel,
partitionRel,
partIndexId,
- constrOid,
+ address.objectId,
numfks,
mapped_confkey,
conkey,
@@ -10341,8 +10401,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
* child.
*
* If wqueue is given, it is used to set up phase-3 verification for each
- * cloned constraint; if omitted, we assume that such verification is not
- * needed (example: the partition is being created anew).
+ * cloned constraint; omit it if such verification is not needed
+ * (example: the partition is being created anew).
*/
static void
CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
@@ -10426,9 +10486,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
Constraint *fkconstraint;
bool attached;
Oid indexOid;
- Oid constrOid;
- ObjectAddress address,
- referenced;
+ ObjectAddress address;
ListCell *lc;
Oid insertTriggerOid,
updateTriggerOid;
@@ -10525,7 +10583,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
fkconstraint->old_conpfeqop = NIL;
fkconstraint->old_pktable_oid = InvalidOid;
fkconstraint->skip_validation = false;
- fkconstraint->initially_valid = true;
+ fkconstraint->initially_valid = constrForm->convalidated;
for (int i = 0; i < numfks; i++)
{
Form_pg_attribute att;
@@ -10535,71 +10593,29 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
fkconstraint->fk_attrs = lappend(fkconstraint->fk_attrs,
makeString(NameStr(att->attname)));
}
- if (ConstraintNameIsUsed(CONSTRAINT_RELATION,
- RelationGetRelid(partRel),
- NameStr(constrForm->conname)))
- fkconstraint->conname =
- ChooseConstraintName(RelationGetRelationName(partRel),
- ChooseForeignKeyConstraintNameAddition(fkconstraint->fk_attrs),
- "fkey",
- RelationGetNamespace(partRel), NIL);
- else
- fkconstraint->conname = pstrdup(NameStr(constrForm->conname));
indexOid = constrForm->conindid;
- constrOid =
- CreateConstraintEntry(fkconstraint->conname,
- constrForm->connamespace,
- CONSTRAINT_FOREIGN,
- fkconstraint->deferrable,
- fkconstraint->initdeferred,
- constrForm->convalidated,
- parentConstrOid,
- RelationGetRelid(partRel),
- mapped_conkey,
- numfks,
- numfks,
- InvalidOid, /* not a domain constraint */
- indexOid,
- constrForm->confrelid, /* same foreign rel */
- confkey,
- conpfeqop,
- conppeqop,
- conffeqop,
- numfks,
- fkconstraint->fk_upd_action,
- fkconstraint->fk_del_action,
- confdelsetcols,
- numfkdelsetcols,
- fkconstraint->fk_matchtype,
- NULL,
- NULL,
- NULL,
- false, /* islocal */
- 1, /* inhcount */
- false, /* conNoInherit */
- true);
- /* Set up partition dependencies for the new constraint */
- ObjectAddressSet(address, ConstraintRelationId, constrOid);
- ObjectAddressSet(referenced, ConstraintRelationId, parentConstrOid);
- recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_PRI);
- ObjectAddressSet(referenced, RelationRelationId,
- RelationGetRelid(partRel));
- recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_SEC);
+ /* Create the pg_constraint entry at this level */
+ address = addFkConstraint(addFkReferencingSide,
+ NameStr(constrForm->conname), fkconstraint,
+ partRel, pkrel, indexOid, parentConstrOid,
+ numfks, confkey,
+ mapped_conkey, conpfeqop,
+ conppeqop, conffeqop,
+ numfkdelsetcols, confdelsetcols,
+ false);
/* Done with the cloned constraint's tuple */
ReleaseSysCache(tuple);
- /* Make all this visible before recursing */
- CommandCounterIncrement();
-
+ /* Create the check triggers, and recurse to partitions, if any */
addFkRecurseReferencing(wqueue,
fkconstraint,
partRel,
pkrel,
indexOid,
- constrOid,
+ address.objectId,
numfks,
confkey,
mapped_conkey,
@@ -10763,6 +10779,81 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
partRelid);
+ /*
+ * If the referenced table is partitioned, then the partition we're
+ * attaching now has extra pg_constraint rows and action triggers that are
+ * no longer needed. Remove those.
+ */
+ if (get_rel_relkind(fk->confrelid) == RELKIND_PARTITIONED_TABLE)
+ {
+ Relation pg_constraint = table_open(ConstraintRelationId, RowShareLock);
+ ObjectAddresses *objs;
+ HeapTuple consttup;
+
+ ScanKeyInit(&key,
+ Anum_pg_constraint_conrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(fk->conrelid));
+
+ scan = systable_beginscan(pg_constraint,
+ ConstraintRelidTypidNameIndexId,
+ true, NULL, 1, &key);
+ objs = new_object_addresses();
+ while ((consttup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_constraint conform = (Form_pg_constraint) GETSTRUCT(consttup);
+
+ if (conform->conparentid != fk->conoid)
+ continue;
+ else
+ {
+ ObjectAddress addr;
+ SysScanDesc scan2;
+ ScanKeyData key2;
+ int n PG_USED_FOR_ASSERTS_ONLY;
+
+ ObjectAddressSet(addr, ConstraintRelationId, conform->oid);
+ add_exact_object_address(&addr, objs);
+
+ /*
+ * First we must delete the dependency record that binds the
+ * constraint records together.
+ */
+ n = deleteDependencyRecordsForSpecific(ConstraintRelationId,
+ conform->oid,
+ DEPENDENCY_INTERNAL,
+ ConstraintRelationId,
+ fk->conoid);
+ Assert(n == 1); /* actually only one is expected */
+
+ /*
+ * Now search for the triggers for this constraint and set
+ * them up for deletion too
+ */
+ ScanKeyInit(&key2,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conform->oid));
+ scan2 = systable_beginscan(trigrel, TriggerConstraintIndexId,
+ true, NULL, 1, &key2);
+ while ((trigtup = systable_getnext(scan2)) != NULL)
+ {
+ ObjectAddressSet(addr, TriggerRelationId,
+ ((Form_pg_trigger) GETSTRUCT(trigtup))->oid);
+ add_exact_object_address(&addr, objs);
+ }
+ systable_endscan(scan2);
+ }
+ }
+ /* make the dependency deletions visible */
+ CommandCounterIncrement();
+ performMultipleDeletions(objs, DROP_RESTRICT,
+ PERFORM_DELETION_INTERNAL);
+ systable_endscan(scan);
+
+ table_close(pg_constraint, RowShareLock);
+ }
+
CommandCounterIncrement();
return true;
}
@@ -18653,7 +18744,7 @@ ATExecDetachPartition(List **wqueue, AlteredTableInfo *tab, Relation rel,
}
/* Do the final part of detaching */
- DetachPartitionFinalize(rel, partRel, concurrent, defaultPartOid);
+ DetachPartitionFinalize(wqueue, rel, partRel, concurrent, defaultPartOid);
ObjectAddressSet(address, RelationRelationId, RelationGetRelid(partRel));
@@ -18670,8 +18761,8 @@ ATExecDetachPartition(List **wqueue, AlteredTableInfo *tab, Relation rel,
* transaction of the concurrent algorithm fails (crash or abort).
*/
static void
-DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
- Oid defaultPartOid)
+DetachPartitionFinalize(List **wqueue, Relation rel, Relation partRel,
+ bool concurrent, Oid defaultPartOid)
{
Relation classRel;
List *fks;
@@ -18707,8 +18798,11 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
{
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
+ HeapTuple parentConTup;
Form_pg_constraint conform;
+ Form_pg_constraint parentConForm;
Constraint *fkconstraint;
+ Oid parentConstrOid;
Oid insertTriggerOid,
updateTriggerOid;
@@ -18725,7 +18819,20 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
continue;
}
- /* unset conparentid and adjust conislocal, coninhcount, etc. */
+ parentConstrOid = conform->conparentid;
+
+ Assert(OidIsValid(conform->conparentid));
+ parentConTup = SearchSysCache1(CONSTROID,
+ ObjectIdGetDatum(parentConstrOid));
+ if (!HeapTupleIsValid(parentConTup))
+ elog(ERROR, "cache lookup failed for constraint %u",
+ conform->conparentid);
+ parentConForm = (Form_pg_constraint) GETSTRUCT(parentConTup);
+
+ /*
+ * The constraint on this table must be marked no longer a child of
+ * the parent's constraint, as do its check triggers.
+ */
ConstraintSetParentConstraint(fk->conoid, InvalidOid, InvalidOid);
/*
@@ -18743,35 +18850,88 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
RelationGetRelid(partRel));
/*
- * Make the action triggers on the referenced relation. When this was
- * a partition the action triggers pointed to the parent rel (they
- * still do), but now we need separate ones of our own.
+ * If the referenced side is partitioned (which we know because our
+ * parent's constraint points to a different relation than ours) then
+ * we must, in addition to the above, create pg_constraint rows that
+ * point to each partition, each with its own action triggers.
*/
- fkconstraint = makeNode(Constraint);
- fkconstraint->contype = CONSTRAINT_FOREIGN;
- fkconstraint->conname = pstrdup(NameStr(conform->conname));
- fkconstraint->deferrable = conform->condeferrable;
- fkconstraint->initdeferred = conform->condeferred;
- fkconstraint->location = -1;
- fkconstraint->pktable = NULL;
- fkconstraint->fk_attrs = NIL;
- fkconstraint->pk_attrs = NIL;
- fkconstraint->fk_matchtype = conform->confmatchtype;
- fkconstraint->fk_upd_action = conform->confupdtype;
- fkconstraint->fk_del_action = conform->confdeltype;
- fkconstraint->fk_del_set_cols = NIL;
- fkconstraint->old_conpfeqop = NIL;
- fkconstraint->old_pktable_oid = InvalidOid;
- fkconstraint->skip_validation = false;
- fkconstraint->initially_valid = true;
+ if (parentConForm->conrelid != conform->conrelid)
+ {
+ int numfks;
+ AttrNumber conkey[INDEX_MAX_KEYS];
+ AttrMap *attmap;
+ AttrNumber confkey[INDEX_MAX_KEYS];
+ Oid conpfeqop[INDEX_MAX_KEYS];
+ Oid conppeqop[INDEX_MAX_KEYS];
+ Oid conffeqop[INDEX_MAX_KEYS];
+ int numfkdelsetcols;
+ AttrNumber confdelsetcols[INDEX_MAX_KEYS];
+ Relation refdRel;
- createForeignKeyActionTriggers(partRel, conform->confrelid,
- fkconstraint, fk->conoid,
- conform->conindid,
- InvalidOid, InvalidOid,
- NULL, NULL);
+ DeconstructFkConstraintRow(contup,
+ &numfks,
+ conkey,
+ confkey,
+ conpfeqop,
+ conppeqop,
+ conffeqop,
+ &numfkdelsetcols,
+ confdelsetcols);
+
+ /* Create a synthetic node we'll use throughout */
+ fkconstraint = makeNode(Constraint);
+ fkconstraint->contype = CONSTRAINT_FOREIGN;
+ fkconstraint->conname = pstrdup(NameStr(conform->conname));
+ fkconstraint->deferrable = conform->condeferrable;
+ fkconstraint->initdeferred = conform->condeferred;
+ fkconstraint->skip_validation = true;
+ fkconstraint->initially_valid = true;
+ /* a few irrelevant fields omitted here */
+ fkconstraint->pktable = NULL;
+ fkconstraint->fk_attrs = NIL;
+ fkconstraint->pk_attrs = NIL;
+ fkconstraint->fk_matchtype = conform->confmatchtype;
+ fkconstraint->fk_upd_action = conform->confupdtype;
+ fkconstraint->fk_del_action = conform->confdeltype;
+ fkconstraint->fk_del_set_cols = NIL;
+ fkconstraint->old_conpfeqop = NIL;
+ fkconstraint->old_pktable_oid = InvalidOid;
+ fkconstraint->location = -1;
+
+ attmap = build_attrmap_by_name(RelationGetDescr(partRel),
+ RelationGetDescr(rel),
+ false);
+ for (int i = 0; i < numfks; i++)
+ {
+ Form_pg_attribute att;
+
+ att = TupleDescAttr(RelationGetDescr(partRel),
+ attmap->attnums[conkey[i] - 1] - 1);
+ fkconstraint->fk_attrs = lappend(fkconstraint->fk_attrs,
+ makeString(NameStr(att->attname)));
+ }
+
+ refdRel = table_open(fk->confrelid, AccessShareLock);
+
+ addFkRecurseReferenced(wqueue, fkconstraint, partRel,
+ refdRel,
+ conform->conindid,
+ fk->conoid,
+ numfks,
+ confkey,
+ conkey,
+ conpfeqop,
+ conppeqop,
+ conffeqop,
+ numfkdelsetcols,
+ confdelsetcols,
+ true,
+ InvalidOid, InvalidOid);
+ table_close(refdRel, AccessShareLock);
+ }
ReleaseSysCache(contup);
+ ReleaseSysCache(parentConTup);
}
list_free_deep(fks);
if (trigrel)
@@ -18906,7 +19066,7 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
* completion; this completes the detaching process.
*/
static ObjectAddress
-ATExecDetachPartitionFinalize(Relation rel, RangeVar *name)
+ATExecDetachPartitionFinalize(List **wqueue, Relation rel, RangeVar *name)
{
Relation partRel;
ObjectAddress address;
@@ -18924,7 +19084,7 @@ ATExecDetachPartitionFinalize(Relation rel, RangeVar *name)
*/
WaitForOlderSnapshots(snap->xmin, false);
- DetachPartitionFinalize(rel, partRel, true, InvalidOid);
+ DetachPartitionFinalize(wqueue, rel, partRel, true, InvalidOid);
ObjectAddressSet(address, RelationRelationId, RelationGetRelid(partRel));
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 037b73204ab..b6e8d602f9d 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -2934,3 +2934,99 @@ DETAIL: drop cascades to table fkpart11.pk
drop cascades to table fkpart11.fk_parted
drop cascades to table fkpart11.fk_another
drop cascades to function fkpart11.print_row()
+-- When a table is attached as partition to a partitioned table that has
+-- a foreign key to another partitioned table, it acquires a clone of the
+-- FK. Upon detach, this clone is not removed, but instead becomes an
+-- independent FK. If it then attaches to the partitioned table again,
+-- the FK from the parent "takes over" ownership of the independent FK rather
+-- than creating a separate one.
+CREATE SCHEMA fkpart12
+ CREATE TABLE fk_p ( id int, jd int, PRIMARY KEY(id, jd)) PARTITION BY list (id)
+ CREATE TABLE fk_p_1 PARTITION OF fk_p FOR VALUES IN (1) PARTITION BY list (jd)
+ CREATE TABLE fk_p_1_1 PARTITION OF fk_p_1 FOR VALUES IN (1)
+ CREATE TABLE fk_p_1_2 PARTITION OF fk_p_1 FOR VALUES IN (2)
+ CREATE TABLE fk_p_2 PARTITION OF fk_p FOR VALUES IN (2) PARTITION BY list (jd)
+ CREATE TABLE fk_p_2_1 PARTITION OF fk_p_2 FOR VALUES IN (1)
+ CREATE TABLE fk_p_2_2 PARTITION OF fk_p_2 FOR VALUES IN (2)
+ CREATE TABLE fk_r_1 ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL)
+ CREATE TABLE fk_r_2 ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL) PARTITION BY list (id)
+ CREATE TABLE fk_r_2_1 PARTITION OF fk_r_2 FOR VALUES IN (2, 1)
+ CREATE TABLE fk_r ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL,
+ FOREIGN KEY (p_id, p_jd) REFERENCES fk_p (id, jd)
+ ) PARTITION BY list (id);
+SET search_path TO fkpart12;
+INSERT INTO fk_p VALUES (1, 1);
+ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1);
+ALTER TABLE fk_r ATTACH PARTITION fk_r_2 FOR VALUES IN (2);
+\d fk_r_2
+ Partitioned table "fkpart12.fk_r_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ id | integer | | not null |
+ p_id | integer | | not null |
+ p_jd | integer | | not null |
+Partition of: fk_r FOR VALUES IN (2)
+Partition key: LIST (id)
+Indexes:
+ "fk_r_2_pkey" PRIMARY KEY, btree (id)
+Foreign-key constraints:
+ TABLE "fk_r" CONSTRAINT "fk_r_p_id_p_jd_fkey" FOREIGN KEY (p_id, p_jd) REFERENCES fk_p(id, jd)
+Number of partitions: 1 (Use \d+ to list them.)
+
+INSERT INTO fk_r VALUES (1, 1, 1);
+INSERT INTO fk_r VALUES (2, 2, 1);
+ERROR: insert or update on table "fk_r_2_1" violates foreign key constraint "fk_r_p_id_p_jd_fkey"
+DETAIL: Key (p_id, p_jd)=(2, 1) is not present in table "fk_p".
+ALTER TABLE fk_r DETACH PARTITION fk_r_1;
+ALTER TABLE fk_r DETACH PARTITION fk_r_2;
+\d fk_r_2
+ Partitioned table "fkpart12.fk_r_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ id | integer | | not null |
+ p_id | integer | | not null |
+ p_jd | integer | | not null |
+Partition key: LIST (id)
+Indexes:
+ "fk_r_2_pkey" PRIMARY KEY, btree (id)
+Foreign-key constraints:
+ "fk_r_p_id_p_jd_fkey" FOREIGN KEY (p_id, p_jd) REFERENCES fk_p(id, jd)
+Number of partitions: 1 (Use \d+ to list them.)
+
+INSERT INTO fk_r_1 VALUES (2, 1, 2); -- should fail
+ERROR: insert or update on table "fk_r_1" violates foreign key constraint "fk_r_p_id_p_jd_fkey"
+DETAIL: Key (p_id, p_jd)=(1, 2) is not present in table "fk_p".
+DELETE FROM fk_p; -- should fail
+ERROR: update or delete on table "fk_p_1_1" violates foreign key constraint "fk_r_1_p_id_p_jd_fkey1" on table "fk_r_1"
+DETAIL: Key (id, jd)=(1, 1) is still referenced from table "fk_r_1".
+ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1);
+ALTER TABLE fk_r ATTACH PARTITION fk_r_2 FOR VALUES IN (2);
+\d fk_r_2
+ Partitioned table "fkpart12.fk_r_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ id | integer | | not null |
+ p_id | integer | | not null |
+ p_jd | integer | | not null |
+Partition of: fk_r FOR VALUES IN (2)
+Partition key: LIST (id)
+Indexes:
+ "fk_r_2_pkey" PRIMARY KEY, btree (id)
+Foreign-key constraints:
+ TABLE "fk_r" CONSTRAINT "fk_r_p_id_p_jd_fkey" FOREIGN KEY (p_id, p_jd) REFERENCES fk_p(id, jd)
+Number of partitions: 1 (Use \d+ to list them.)
+
+DELETE FROM fk_p; -- should fail
+ERROR: update or delete on table "fk_p_1_1" violates foreign key constraint "fk_r_p_id_p_jd_fkey2" on table "fk_r"
+DETAIL: Key (id, jd)=(1, 1) is still referenced from table "fk_r".
+-- these should all fail
+ALTER TABLE fk_r_1 DROP CONSTRAINT fk_r_p_id_p_jd_fkey;
+ERROR: cannot drop inherited constraint "fk_r_p_id_p_jd_fkey" of relation "fk_r_1"
+ALTER TABLE fk_r DROP CONSTRAINT fk_r_p_id_p_jd_fkey1;
+ERROR: cannot drop inherited constraint "fk_r_p_id_p_jd_fkey1" of relation "fk_r"
+ALTER TABLE fk_r_2 DROP CONSTRAINT fk_r_p_id_p_jd_fkey;
+ERROR: cannot drop inherited constraint "fk_r_p_id_p_jd_fkey" of relation "fk_r_2"
+SET client_min_messages TO warning;
+DROP SCHEMA fkpart12 CASCADE;
+RESET client_min_messages;
+RESET search_path;
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index 9a547fb1be0..09b089b60e1 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -2086,3 +2086,60 @@ UPDATE fkpart11.pk SET a = 3 WHERE a = 4;
UPDATE fkpart11.pk SET a = 1 WHERE a = 2;
DROP SCHEMA fkpart11 CASCADE;
+
+-- When a table is attached as partition to a partitioned table that has
+-- a foreign key to another partitioned table, it acquires a clone of the
+-- FK. Upon detach, this clone is not removed, but instead becomes an
+-- independent FK. If it then attaches to the partitioned table again,
+-- the FK from the parent "takes over" ownership of the independent FK rather
+-- than creating a separate one.
+CREATE SCHEMA fkpart12
+ CREATE TABLE fk_p ( id int, jd int, PRIMARY KEY(id, jd)) PARTITION BY list (id)
+ CREATE TABLE fk_p_1 PARTITION OF fk_p FOR VALUES IN (1) PARTITION BY list (jd)
+ CREATE TABLE fk_p_1_1 PARTITION OF fk_p_1 FOR VALUES IN (1)
+ CREATE TABLE fk_p_1_2 PARTITION OF fk_p_1 FOR VALUES IN (2)
+ CREATE TABLE fk_p_2 PARTITION OF fk_p FOR VALUES IN (2) PARTITION BY list (jd)
+ CREATE TABLE fk_p_2_1 PARTITION OF fk_p_2 FOR VALUES IN (1)
+ CREATE TABLE fk_p_2_2 PARTITION OF fk_p_2 FOR VALUES IN (2)
+ CREATE TABLE fk_r_1 ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL)
+ CREATE TABLE fk_r_2 ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL) PARTITION BY list (id)
+ CREATE TABLE fk_r_2_1 PARTITION OF fk_r_2 FOR VALUES IN (2, 1)
+ CREATE TABLE fk_r ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL,
+ FOREIGN KEY (p_id, p_jd) REFERENCES fk_p (id, jd)
+ ) PARTITION BY list (id);
+SET search_path TO fkpart12;
+
+INSERT INTO fk_p VALUES (1, 1);
+
+ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1);
+ALTER TABLE fk_r ATTACH PARTITION fk_r_2 FOR VALUES IN (2);
+
+\d fk_r_2
+
+INSERT INTO fk_r VALUES (1, 1, 1);
+INSERT INTO fk_r VALUES (2, 2, 1);
+
+ALTER TABLE fk_r DETACH PARTITION fk_r_1;
+ALTER TABLE fk_r DETACH PARTITION fk_r_2;
+
+\d fk_r_2
+
+INSERT INTO fk_r_1 VALUES (2, 1, 2); -- should fail
+DELETE FROM fk_p; -- should fail
+
+ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1);
+ALTER TABLE fk_r ATTACH PARTITION fk_r_2 FOR VALUES IN (2);
+
+\d fk_r_2
+
+DELETE FROM fk_p; -- should fail
+
+-- these should all fail
+ALTER TABLE fk_r_1 DROP CONSTRAINT fk_r_p_id_p_jd_fkey;
+ALTER TABLE fk_r DROP CONSTRAINT fk_r_p_id_p_jd_fkey1;
+ALTER TABLE fk_r_2 DROP CONSTRAINT fk_r_p_id_p_jd_fkey;
+
+SET client_min_messages TO warning;
+DROP SCHEMA fkpart12 CASCADE;
+RESET client_min_messages;
+RESET search_path;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 4791528e140..43570438e99 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -3130,6 +3130,7 @@ _resultmap
_stringlist
acquireLocksOnSubLinks_context
add_nulling_relids_context
+addFkConstraintSides
adjust_appendrel_attrs_context
allocfunc
amadjustmembers_function
--
2.39.5
v4_17-0001-Redo-foreign-key-maintenance-during-partition-.patchtext/x-diff; charset=utf-8Download
From 54842333a6c0fab92dea6422529fb928ebae3877 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=81lvaro=20Herrera?= <alvherre@alvh.no-ip.org>
Date: Fri, 18 Oct 2024 16:40:35 +0200
Subject: [PATCH v4_17] Redo foreign key maintenance during partition
ATTACH/DETACH
... when the reference table is partitioned.
It turns out that the catalog representation we chose for foreign keys
connecting partitioned tables is suboptimal, particularly in the sense
that a standalone table has a different way to represent it when
referencing a partitioned table, than when the same table becomes a
partition (and vice versa). This difference means we need to create
additional catalog rows on detach (pg_constraint and pg_trigger), and
remove them on attach.
As a very obvious symptom, we were missing action triggers after
detach, which means that you could update/delete rows from the
referenced partitioned table that still had referencing rows at the
other side previously detached, and fail to throw the required errors.
!!!!!!!!!
Note that this means existing FKs that refer to partitioned tables might
have rows that break relational integrity, on tables that were once
partitions.
Another possible problem is that trying to reattach a table
that had been detached would fail indicating that internal triggers
cannot be found, which from the user's point of view is nonsensical.
In branches 15 and above, we fix this by creating a new helper function
addFkConstraint() which is in charge of creating a standalone
pg_constraint row, and repurposing addFkRecurseReferencing() and
addFkRecurseReferenced() so that they're only the recursive routine for
each side of the FK, and they call addFkConstraint() to create
pg_constraint at each partitioning level and add the necessary triggers.
These new routines can be used during partition creation, partition
attach and detach, and foreign key creation. This reduces redundant
code and simplifies the flow.
In branches 14 and 13, we have a much simpler fix that consists on
simply removing the constraint on detach. The reason is that those
branches are missing commit f4566345cf40, which reworked the way this
works in a way that we didn't consider back-patchable at the time. I
doubt we want to change that now, so this is probably the best we can do
there.
We opted to leave branch 12 alone, because it's going to end maintenance
very soon, and any rushed patch might be worse.
In the future we might want to rethink the catalog representation to
avoid this problem, but for now the code seems to do what's required to
make the constraints operate correctly.
Co-authored-by: Jehan-Guillaume de Rorthais <jgdr@dalibo.com>
Co-authored-by: Tender Wang <tndrwang@gmail.com>
Co-authored-by: Alvaro Herrera <alvherre@alvh.no-ip.org>
Reported-by: Guillaume Lelarge <guillaume@lelarge.info>
Reported-by: Jehan-Guillaume de Rorthais <jgdr@dalibo.com>
Reported-by: Thomas Baehler (SBB CFF FFS) <thomas.baehler2@sbb.ch>
Discussion: https://postgr.es/m/20230420144344.40744130@karst
Discussion: https://postgr.es/m/20230705233028.2f554f73@karst
Discussion: https://postgr.es/m/GVAP278MB02787E7134FD691861635A8BC9032@GVAP278MB0278.CHEP278.PROD.OUTLOOK.COM
Discussion: https://postgr.es/m/18541-628a61bc267cd2d3@postgresql.org
---
src/backend/commands/tablecmds.c | 630 ++++++++++++++--------
src/test/regress/expected/foreign_key.out | 96 ++++
src/test/regress/sql/foreign_key.sql | 57 ++
src/tools/pgindent/typedefs.list | 1 +
4 files changed, 549 insertions(+), 235 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 0ad0dfbcc57..c9ccdae1187 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -345,6 +345,14 @@ typedef struct ForeignTruncateInfo
List *rels;
} ForeignTruncateInfo;
+/* Partial or complete FK creation in addFkConstraint() */
+typedef enum addFkConstraintSides
+{
+ addFkReferencedSide,
+ addFkReferencingSide,
+ addFkBothSides,
+} addFkConstraintSides;
+
/*
* Partition tables are expected to be dropped when the parent partitioned
* table gets dropped. Hence for partitioning we use AUTO dependency.
@@ -504,16 +512,25 @@ static ObjectAddress ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *
Relation rel, Constraint *fkconstraint,
bool recurse, bool recursing,
LOCKMODE lockmode);
-static ObjectAddress addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint,
- Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
- int numfks, int16 *pkattnum, int16 *fkattnum,
- Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
- int numfkdelsetcols, int16 *fkdelsetcols,
- bool old_check_ok,
- Oid parentDelTrigger, Oid parentUpdTrigger);
static void validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
int numfksetcols, const int16 *fksetcolsattnums,
List *fksetcols);
+static ObjectAddress addFkConstraint(addFkConstraintSides fkside,
+ char *constraintname,
+ Constraint *fkconstraint, Relation rel,
+ Relation pkrel, Oid indexOid,
+ Oid parentConstr,
+ int numfks, int16 *pkattnum, int16 *fkattnum,
+ Oid *pfeqoperators, Oid *ppeqoperators,
+ Oid *ffeqoperators, int numfkdelsetcols,
+ int16 *fkdelsetcols, bool is_internal);
+static void addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint,
+ Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
+ int numfks, int16 *pkattnum, int16 *fkattnum,
+ Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
+ int numfkdelsetcols, int16 *fkdelsetcols,
+ bool old_check_ok,
+ Oid parentDelTrigger, Oid parentUpdTrigger);
static void addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint,
Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks, int16 *pkattnum, int16 *fkattnum,
@@ -644,9 +661,11 @@ static void DropClonedTriggersFromPartition(Oid partitionId);
static ObjectAddress ATExecDetachPartition(List **wqueue, AlteredTableInfo *tab,
Relation rel, RangeVar *name,
bool concurrent);
-static void DetachPartitionFinalize(Relation rel, Relation partRel,
- bool concurrent, Oid defaultPartOid);
-static ObjectAddress ATExecDetachPartitionFinalize(Relation rel, RangeVar *name);
+static void DetachPartitionFinalize(List **wqueue, Relation rel,
+ Relation partRel, bool concurrent,
+ Oid defaultPartOid);
+static ObjectAddress ATExecDetachPartitionFinalize(List **wqueue, Relation rel,
+ RangeVar *name);
static ObjectAddress ATExecAttachPartitionIdx(List **wqueue, Relation parentIdx,
RangeVar *name);
static void validatePartitionedIndex(Relation partedIdx, Relation partedTbl);
@@ -5512,7 +5531,7 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
((PartitionCmd *) cmd->def)->concurrent);
break;
case AT_DetachPartitionFinalize:
- address = ATExecDetachPartitionFinalize(rel, ((PartitionCmd *) cmd->def)->name);
+ address = ATExecDetachPartitionFinalize(wqueue, rel, ((PartitionCmd *) cmd->def)->name);
break;
default: /* oops */
elog(ERROR, "unrecognized alter table type: %d",
@@ -9931,25 +9950,37 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
ffeqoperators[i] = ffeqop;
}
- /*
- * Create all the constraint and trigger objects, recursing to partitions
- * as necessary. First handle the referenced side.
- */
- address = addFkRecurseReferenced(wqueue, fkconstraint, rel, pkrel,
- indexOid,
- InvalidOid, /* no parent constraint */
- numfks,
- pkattnum,
- fkattnum,
- pfeqoperators,
- ppeqoperators,
- ffeqoperators,
- numfkdelsetcols,
- fkdelsetcols,
- old_check_ok,
- InvalidOid, InvalidOid);
+ /* First, create the constraint catalog entry itself. */
+ address = addFkConstraint(addFkBothSides,
+ fkconstraint->conname, fkconstraint, rel, pkrel,
+ indexOid,
+ InvalidOid, /* no parent constraint */
+ numfks,
+ pkattnum,
+ fkattnum,
+ pfeqoperators,
+ ppeqoperators,
+ ffeqoperators,
+ numfkdelsetcols,
+ fkdelsetcols,
+ false);
- /* Now handle the referencing side. */
+ /* Next process the action triggers at the referenced side and recurse */
+ addFkRecurseReferenced(wqueue, fkconstraint, rel, pkrel,
+ indexOid,
+ address.objectId,
+ numfks,
+ pkattnum,
+ fkattnum,
+ pfeqoperators,
+ ppeqoperators,
+ ffeqoperators,
+ numfkdelsetcols,
+ fkdelsetcols,
+ old_check_ok,
+ InvalidOid, InvalidOid);
+
+ /* Lastly create the check triggers at the referencing side and recurse */
addFkRecurseReferencing(wqueue, fkconstraint, rel, pkrel,
indexOid,
address.objectId,
@@ -10009,46 +10040,43 @@ validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
}
/*
- * addFkRecurseReferenced
- * subroutine for ATAddForeignKeyConstraint; recurses on the referenced
- * side of the constraint
+ * addFkConstraint
+ * Install pg_constraint entries to implement a foreign key constraint.
+ * Caller must separately invoke addFkRecurseReferenced and
+ * addFkRecurseReferencing, as appropriate, to install pg_trigger entries
+ * and (for partitioned tables) recurse to partitions.
*
- * Create pg_constraint rows for the referenced side of the constraint,
- * referencing the parent of the referencing side; also create action triggers
- * on leaf partitions. If the table is partitioned, recurse to handle each
- * partition.
- *
- * wqueue is the ALTER TABLE work queue; can be NULL when not running as part
- * of an ALTER TABLE sequence.
- * fkconstraint is the constraint being added.
- * rel is the root referencing relation.
- * pkrel is the referenced relation; might be a partition, if recursing.
- * indexOid is the OID of the index (on pkrel) implementing this constraint.
- * parentConstr is the OID of a parent constraint; InvalidOid if this is a
- * top-level constraint.
- * numfks is the number of columns in the foreign key
- * pkattnum is the attnum array of referenced attributes.
- * fkattnum is the attnum array of referencing attributes.
- * numfkdelsetcols is the number of columns in the ON DELETE SET NULL/DEFAULT
- * (...) clause
- * fkdelsetcols is the attnum array of the columns in the ON DELETE SET
- * NULL/DEFAULT clause
- * pf/pp/ffeqoperators are OID array of operators between columns.
- * old_check_ok signals that this constraint replaces an existing one that
- * was already validated (thus this one doesn't need validation).
- * parentDelTrigger and parentUpdTrigger, when being recursively called on
- * a partition, are the OIDs of the parent action triggers for DELETE and
- * UPDATE respectively.
+ * * fkside indicates which side of the FK (or both) to create. Caller must
+ * call addFkRecurseReferenced if it's addFkReferencedSide,
+ * addFkRecurseReferencing if it's addFkReferencingSide, or both if it's
+ * addFkBothSides.
+ * * constraintname the base name for the constraint being added. This is
+ * copied to fkconstraint->conname if the latter is not set.
+ * * fkconstraint is the constraint being added.
+ * * rel is the root referencing relation.
+ * * pkrel is the referenced relation; might be a partition, if recursing.
+ * * indexOid is the OID of the index (on pkrel) implementing this constraint.
+ * * parentConstr is the OID of a parent constraint; InvalidOid if this is a
+ * top-level constraint.
+ * * numfks is the number of columns in the foreign key.
+ * * pkattnum is the attnum array of referenced attributes.
+ * * fkattnum is the attnum array of referencing attributes.
+ * * pf/pp/ffeqoperators are OID array of operators between columns.
+ * * numfkdelsetcols is the number of columns in the ON DELETE SET NULL/DEFAULT
+ * (...) clause.
+ * * fkdelsetcols is the attnum array of the columns in the ON DELETE SET
+ * NULL/DEFAULT clause.
+ * * is_internal: whether to mark the constraint as internal (used during
+ * recursion)
*/
static ObjectAddress
-addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
- Relation pkrel, Oid indexOid, Oid parentConstr,
- int numfks,
- int16 *pkattnum, int16 *fkattnum, Oid *pfeqoperators,
- Oid *ppeqoperators, Oid *ffeqoperators,
- int numfkdelsetcols, int16 *fkdelsetcols,
- bool old_check_ok,
- Oid parentDelTrigger, Oid parentUpdTrigger)
+addFkConstraint(addFkConstraintSides fkside,
+ char *constraintname, Constraint *fkconstraint,
+ Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
+ int numfks, int16 *pkattnum,
+ int16 *fkattnum, Oid *pfeqoperators, Oid *ppeqoperators,
+ Oid *ffeqoperators, int numfkdelsetcols, int16 *fkdelsetcols,
+ bool is_internal)
{
ObjectAddress address;
Oid constrOid;
@@ -10056,8 +10084,6 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
bool conislocal;
int coninhcount;
bool connoinherit;
- Oid deleteTriggerOid,
- updateTriggerOid;
/*
* Verify relkind for each referenced partition. At the top level, this
@@ -10076,13 +10102,16 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
*/
if (ConstraintNameIsUsed(CONSTRAINT_RELATION,
RelationGetRelid(rel),
- fkconstraint->conname))
+ constraintname))
conname = ChooseConstraintName(RelationGetRelationName(rel),
ChooseForeignKeyConstraintNameAddition(fkconstraint->fk_attrs),
"fkey",
RelationGetNamespace(rel), NIL);
else
- conname = fkconstraint->conname;
+ conname = constraintname;
+
+ if (fkconstraint->conname == NULL)
+ fkconstraint->conname = pstrdup(conname);
if (OidIsValid(parentConstr))
{
@@ -10134,33 +10163,95 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
conislocal, /* islocal */
coninhcount, /* inhcount */
connoinherit, /* conNoInherit */
- false); /* is_internal */
+ is_internal); /* is_internal */
ObjectAddressSet(address, ConstraintRelationId, constrOid);
- /*
- * Mark the child constraint as part of the parent constraint; it must not
- * be dropped on its own. (This constraint is deleted when the partition
- * is detached, but a special check needs to occur that the partition
- * contains no referenced values.)
- */
if (OidIsValid(parentConstr))
{
ObjectAddress referenced;
ObjectAddressSet(referenced, ConstraintRelationId, parentConstr);
- recordDependencyOn(&address, &referenced, DEPENDENCY_INTERNAL);
+ if (fkside == addFkReferencedSide)
+ {
+ /*
+ * Mark the child constraint as part of the parent constraint; it
+ * must not be dropped on its own. (This constraint is deleted
+ * when the partition is detached, but a special check needs to
+ * occur that the partition contains no referenced values.)
+ */
+ recordDependencyOn(&address, &referenced, DEPENDENCY_INTERNAL);
+ }
+ else
+ {
+ /*
+ * Give this constraint partition-type dependencies on the parent
+ * constraint as well as the table.
+ */
+ recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_PRI);
+ ObjectAddressSet(referenced, RelationRelationId, RelationGetRelid(rel));
+ recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_SEC);
+ }
}
/* make new constraint visible, in case we add more */
CommandCounterIncrement();
+ return address;
+}
+
+/*
+ * addFkRecurseReferenced
+ * Recursive helper for the referenced side of foreign key creation,
+ * which creates the action triggers and recurses
+ *
+ * If the referenced relation is a plain relation, create the necessary action
+ * triggers that implement the constraint. If the referenced relation is a
+ * partitioned table, then we create a pg_constraint row referencing the parent
+ * of the referencing side for it and recurse on this routine for each
+ * partition.
+ *
+ * wqueue is the ALTER TABLE work queue; can be NULL when not running as part
+ * of an ALTER TABLE sequence.
+ * fkconstraint is the constraint being added.
+ * rel is the root referencing relation.
+ * pkrel is the referenced relation; might be a partition, if recursing.
+ * indexOid is the OID of the index (on pkrel) implementing this constraint.
+ * parentConstr is the OID of a parent constraint; InvalidOid if this is a
+ * top-level constraint.
+ * numfks is the number of columns in the foreign key
+ * pkattnum is the attnum array of referenced attributes.
+ * fkattnum is the attnum array of referencing attributes.
+ * numfkdelsetcols is the number of columns in the ON DELETE SET NULL/DEFAULT
+ * (...) clause
+ * fkdelsetcols is the attnum array of the columns in the ON DELETE SET
+ * NULL/DEFAULT clause
+ * pf/pp/ffeqoperators are OID array of operators between columns.
+ * old_check_ok signals that this constraint replaces an existing one that
+ * was already validated (thus this one doesn't need validation).
+ * parentDelTrigger and parentUpdTrigger, when being recursively called on
+ * a partition, are the OIDs of the parent action triggers for DELETE and
+ * UPDATE respectively.
+ */
+static void
+addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
+ Relation pkrel, Oid indexOid, Oid parentConstr,
+ int numfks,
+ int16 *pkattnum, int16 *fkattnum, Oid *pfeqoperators,
+ Oid *ppeqoperators, Oid *ffeqoperators,
+ int numfkdelsetcols, int16 *fkdelsetcols,
+ bool old_check_ok,
+ Oid parentDelTrigger, Oid parentUpdTrigger)
+{
+ Oid deleteTriggerOid,
+ updateTriggerOid;
+
/*
* Create the action triggers that enforce the constraint.
*/
createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
fkconstraint,
- constrOid, indexOid,
+ parentConstr, indexOid,
parentDelTrigger, parentUpdTrigger,
&deleteTriggerOid, &updateTriggerOid);
@@ -10179,6 +10270,7 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
AttrMap *map;
AttrNumber *mapped_pkattnum;
Oid partIndexId;
+ ObjectAddress address;
partRel = table_open(pd->oids[i], ShareRowExclusiveLock);
@@ -10198,13 +10290,23 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
else
mapped_pkattnum = pkattnum;
- /* do the deed */
+ /* Determine the index to use at this level */
partIndexId = index_get_partition(partRel, indexOid);
if (!OidIsValid(partIndexId))
elog(ERROR, "index for %u not found in partition %s",
indexOid, RelationGetRelationName(partRel));
+
+ /* Create entry at this level ... */
+ address = addFkConstraint(addFkReferencedSide,
+ fkconstraint->conname, fkconstraint, rel,
+ partRel, partIndexId, parentConstr,
+ numfks, mapped_pkattnum,
+ fkattnum, pfeqoperators, ppeqoperators,
+ ffeqoperators, numfkdelsetcols,
+ fkdelsetcols, true);
+ /* ... and recurse to our children */
addFkRecurseReferenced(wqueue, fkconstraint, rel, partRel,
- partIndexId, constrOid, numfks,
+ partIndexId, address.objectId, numfks,
mapped_pkattnum, fkattnum,
pfeqoperators, ppeqoperators, ffeqoperators,
numfkdelsetcols, fkdelsetcols,
@@ -10220,13 +10322,12 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
}
}
}
-
- return address;
}
/*
* addFkRecurseReferencing
- * subroutine for ATAddForeignKeyConstraint and CloneFkReferencing
+ * Recursive helper for the referencing side of foreign key creation,
+ * which creates the check triggers and recurses
*
* If the referencing relation is a plain relation, create the necessary check
* triggers that implement the constraint, and set up for Phase 3 constraint
@@ -10346,10 +10447,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
AttrMap *attmap;
AttrNumber mapped_fkattnum[INDEX_MAX_KEYS];
bool attached;
- char *conname;
- Oid constrOid;
- ObjectAddress address,
- referenced;
+ ObjectAddress address;
ListCell *cell;
CheckAlterTableIsSafe(partition);
@@ -10392,65 +10490,18 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
/*
* No luck finding a good constraint to reuse; create our own.
*/
- if (ConstraintNameIsUsed(CONSTRAINT_RELATION,
- RelationGetRelid(partition),
- fkconstraint->conname))
- conname = ChooseConstraintName(RelationGetRelationName(partition),
- ChooseForeignKeyConstraintNameAddition(fkconstraint->fk_attrs),
- "fkey",
- RelationGetNamespace(partition), NIL);
- else
- conname = fkconstraint->conname;
- constrOid =
- CreateConstraintEntry(conname,
- RelationGetNamespace(partition),
- CONSTRAINT_FOREIGN,
- fkconstraint->deferrable,
- fkconstraint->initdeferred,
- fkconstraint->initially_valid,
- parentConstr,
- partitionId,
- mapped_fkattnum,
- numfks,
- numfks,
- InvalidOid,
- indexOid,
- RelationGetRelid(pkrel),
- pkattnum,
- pfeqoperators,
- ppeqoperators,
- ffeqoperators,
- numfks,
- fkconstraint->fk_upd_action,
- fkconstraint->fk_del_action,
- fkdelsetcols,
- numfkdelsetcols,
- fkconstraint->fk_matchtype,
- NULL,
- NULL,
- NULL,
- false,
- 1,
- false,
- false);
-
- /*
- * Give this constraint partition-type dependencies on the parent
- * constraint as well as the table.
- */
- ObjectAddressSet(address, ConstraintRelationId, constrOid);
- ObjectAddressSet(referenced, ConstraintRelationId, parentConstr);
- recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_PRI);
- ObjectAddressSet(referenced, RelationRelationId, partitionId);
- recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_SEC);
-
- /* Make all this visible before recursing */
- CommandCounterIncrement();
+ address = addFkConstraint(addFkReferencingSide,
+ fkconstraint->conname, fkconstraint,
+ partition, pkrel, indexOid, parentConstr,
+ numfks, pkattnum,
+ mapped_fkattnum, pfeqoperators,
+ ppeqoperators, ffeqoperators,
+ numfkdelsetcols, fkdelsetcols, true);
/* call ourselves to finalize the creation and we're done */
addFkRecurseReferencing(wqueue, fkconstraint, partition, pkrel,
indexOid,
- constrOid,
+ address.objectId,
numfks,
pkattnum,
mapped_fkattnum,
@@ -10583,6 +10634,7 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
int numfkdelsetcols;
AttrNumber confdelsetcols[INDEX_MAX_KEYS];
Constraint *fkconstraint;
+ ObjectAddress address;
Oid deleteTriggerOid,
updateTriggerOid;
@@ -10616,7 +10668,7 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
* Because we're only expanding the key space at the referenced side,
* we don't need to prevent any operation in the referencing table, so
* AccessShareLock suffices (assumes that dropping the constraint
- * acquires AEL).
+ * acquires AccessExclusiveLock).
*/
fkRel = table_open(constrForm->conrelid, AccessShareLock);
@@ -10682,12 +10734,20 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
constrForm->confrelid, constrForm->conrelid,
&deleteTriggerOid, &updateTriggerOid);
+ /* Add this constraint ... */
+ address = addFkConstraint(addFkReferencedSide,
+ fkconstraint->conname, fkconstraint, fkRel,
+ partitionRel, partIndexId, constrOid,
+ numfks, mapped_confkey,
+ conkey, conpfeqop, conppeqop, conffeqop,
+ numfkdelsetcols, confdelsetcols, false);
+ /* ... and recurse */
addFkRecurseReferenced(NULL,
fkconstraint,
fkRel,
partitionRel,
partIndexId,
- constrOid,
+ address.objectId,
numfks,
mapped_confkey,
conkey,
@@ -10717,8 +10777,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
* child.
*
* If wqueue is given, it is used to set up phase-3 verification for each
- * cloned constraint; if omitted, we assume that such verification is not
- * needed (example: the partition is being created anew).
+ * cloned constraint; omit it if such verification is not needed
+ * (example: the partition is being created anew).
*/
static void
CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
@@ -10802,9 +10862,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
Constraint *fkconstraint;
bool attached;
Oid indexOid;
- Oid constrOid;
- ObjectAddress address,
- referenced;
+ ObjectAddress address;
ListCell *lc;
Oid insertTriggerOid,
updateTriggerOid;
@@ -10901,7 +10959,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
fkconstraint->old_conpfeqop = NIL;
fkconstraint->old_pktable_oid = InvalidOid;
fkconstraint->skip_validation = false;
- fkconstraint->initially_valid = true;
+ fkconstraint->initially_valid = constrForm->convalidated;
for (int i = 0; i < numfks; i++)
{
Form_pg_attribute att;
@@ -10911,71 +10969,29 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
fkconstraint->fk_attrs = lappend(fkconstraint->fk_attrs,
makeString(NameStr(att->attname)));
}
- if (ConstraintNameIsUsed(CONSTRAINT_RELATION,
- RelationGetRelid(partRel),
- NameStr(constrForm->conname)))
- fkconstraint->conname =
- ChooseConstraintName(RelationGetRelationName(partRel),
- ChooseForeignKeyConstraintNameAddition(fkconstraint->fk_attrs),
- "fkey",
- RelationGetNamespace(partRel), NIL);
- else
- fkconstraint->conname = pstrdup(NameStr(constrForm->conname));
indexOid = constrForm->conindid;
- constrOid =
- CreateConstraintEntry(fkconstraint->conname,
- constrForm->connamespace,
- CONSTRAINT_FOREIGN,
- fkconstraint->deferrable,
- fkconstraint->initdeferred,
- constrForm->convalidated,
- parentConstrOid,
- RelationGetRelid(partRel),
- mapped_conkey,
- numfks,
- numfks,
- InvalidOid, /* not a domain constraint */
- indexOid,
- constrForm->confrelid, /* same foreign rel */
- confkey,
- conpfeqop,
- conppeqop,
- conffeqop,
- numfks,
- fkconstraint->fk_upd_action,
- fkconstraint->fk_del_action,
- confdelsetcols,
- numfkdelsetcols,
- fkconstraint->fk_matchtype,
- NULL,
- NULL,
- NULL,
- false, /* islocal */
- 1, /* inhcount */
- false, /* conNoInherit */
- true);
- /* Set up partition dependencies for the new constraint */
- ObjectAddressSet(address, ConstraintRelationId, constrOid);
- ObjectAddressSet(referenced, ConstraintRelationId, parentConstrOid);
- recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_PRI);
- ObjectAddressSet(referenced, RelationRelationId,
- RelationGetRelid(partRel));
- recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_SEC);
+ /* Create the pg_constraint entry at this level */
+ address = addFkConstraint(addFkReferencingSide,
+ NameStr(constrForm->conname), fkconstraint,
+ partRel, pkrel, indexOid, parentConstrOid,
+ numfks, confkey,
+ mapped_conkey, conpfeqop,
+ conppeqop, conffeqop,
+ numfkdelsetcols, confdelsetcols,
+ false);
/* Done with the cloned constraint's tuple */
ReleaseSysCache(tuple);
- /* Make all this visible before recursing */
- CommandCounterIncrement();
-
+ /* Create the check triggers, and recurse to partitions, if any */
addFkRecurseReferencing(wqueue,
fkconstraint,
partRel,
pkrel,
indexOid,
- constrOid,
+ address.objectId,
numfks,
confkey,
mapped_conkey,
@@ -11139,6 +11155,81 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
partRelid);
+ /*
+ * If the referenced table is partitioned, then the partition we're
+ * attaching now has extra pg_constraint rows and action triggers that are
+ * no longer needed. Remove those.
+ */
+ if (get_rel_relkind(fk->confrelid) == RELKIND_PARTITIONED_TABLE)
+ {
+ Relation pg_constraint = table_open(ConstraintRelationId, RowShareLock);
+ ObjectAddresses *objs;
+ HeapTuple consttup;
+
+ ScanKeyInit(&key,
+ Anum_pg_constraint_conrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(fk->conrelid));
+
+ scan = systable_beginscan(pg_constraint,
+ ConstraintRelidTypidNameIndexId,
+ true, NULL, 1, &key);
+ objs = new_object_addresses();
+ while ((consttup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_constraint conform = (Form_pg_constraint) GETSTRUCT(consttup);
+
+ if (conform->conparentid != fk->conoid)
+ continue;
+ else
+ {
+ ObjectAddress addr;
+ SysScanDesc scan2;
+ ScanKeyData key2;
+ int n PG_USED_FOR_ASSERTS_ONLY;
+
+ ObjectAddressSet(addr, ConstraintRelationId, conform->oid);
+ add_exact_object_address(&addr, objs);
+
+ /*
+ * First we must delete the dependency record that binds the
+ * constraint records together.
+ */
+ n = deleteDependencyRecordsForSpecific(ConstraintRelationId,
+ conform->oid,
+ DEPENDENCY_INTERNAL,
+ ConstraintRelationId,
+ fk->conoid);
+ Assert(n == 1); /* actually only one is expected */
+
+ /*
+ * Now search for the triggers for this constraint and set
+ * them up for deletion too
+ */
+ ScanKeyInit(&key2,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conform->oid));
+ scan2 = systable_beginscan(trigrel, TriggerConstraintIndexId,
+ true, NULL, 1, &key2);
+ while ((trigtup = systable_getnext(scan2)) != NULL)
+ {
+ ObjectAddressSet(addr, TriggerRelationId,
+ ((Form_pg_trigger) GETSTRUCT(trigtup))->oid);
+ add_exact_object_address(&addr, objs);
+ }
+ systable_endscan(scan2);
+ }
+ }
+ /* make the dependency deletions visible */
+ CommandCounterIncrement();
+ performMultipleDeletions(objs, DROP_RESTRICT,
+ PERFORM_DELETION_INTERNAL);
+ systable_endscan(scan);
+
+ table_close(pg_constraint, RowShareLock);
+ }
+
CommandCounterIncrement();
return true;
}
@@ -19132,7 +19223,7 @@ ATExecDetachPartition(List **wqueue, AlteredTableInfo *tab, Relation rel,
}
/* Do the final part of detaching */
- DetachPartitionFinalize(rel, partRel, concurrent, defaultPartOid);
+ DetachPartitionFinalize(wqueue, rel, partRel, concurrent, defaultPartOid);
ObjectAddressSet(address, RelationRelationId, RelationGetRelid(partRel));
@@ -19149,8 +19240,8 @@ ATExecDetachPartition(List **wqueue, AlteredTableInfo *tab, Relation rel,
* transaction of the concurrent algorithm fails (crash or abort).
*/
static void
-DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
- Oid defaultPartOid)
+DetachPartitionFinalize(List **wqueue, Relation rel, Relation partRel,
+ bool concurrent, Oid defaultPartOid)
{
Relation classRel;
List *fks;
@@ -19186,8 +19277,11 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
{
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
+ HeapTuple parentConTup;
Form_pg_constraint conform;
+ Form_pg_constraint parentConForm;
Constraint *fkconstraint;
+ Oid parentConstrOid;
Oid insertTriggerOid,
updateTriggerOid;
@@ -19204,7 +19298,20 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
continue;
}
- /* unset conparentid and adjust conislocal, coninhcount, etc. */
+ parentConstrOid = conform->conparentid;
+
+ Assert(OidIsValid(conform->conparentid));
+ parentConTup = SearchSysCache1(CONSTROID,
+ ObjectIdGetDatum(parentConstrOid));
+ if (!HeapTupleIsValid(parentConTup))
+ elog(ERROR, "cache lookup failed for constraint %u",
+ conform->conparentid);
+ parentConForm = (Form_pg_constraint) GETSTRUCT(parentConTup);
+
+ /*
+ * The constraint on this table must be marked no longer a child of
+ * the parent's constraint, as do its check triggers.
+ */
ConstraintSetParentConstraint(fk->conoid, InvalidOid, InvalidOid);
/*
@@ -19222,35 +19329,88 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
RelationGetRelid(partRel));
/*
- * Make the action triggers on the referenced relation. When this was
- * a partition the action triggers pointed to the parent rel (they
- * still do), but now we need separate ones of our own.
+ * If the referenced side is partitioned (which we know because our
+ * parent's constraint points to a different relation than ours) then
+ * we must, in addition to the above, create pg_constraint rows that
+ * point to each partition, each with its own action triggers.
*/
- fkconstraint = makeNode(Constraint);
- fkconstraint->contype = CONSTRAINT_FOREIGN;
- fkconstraint->conname = pstrdup(NameStr(conform->conname));
- fkconstraint->deferrable = conform->condeferrable;
- fkconstraint->initdeferred = conform->condeferred;
- fkconstraint->location = -1;
- fkconstraint->pktable = NULL;
- fkconstraint->fk_attrs = NIL;
- fkconstraint->pk_attrs = NIL;
- fkconstraint->fk_matchtype = conform->confmatchtype;
- fkconstraint->fk_upd_action = conform->confupdtype;
- fkconstraint->fk_del_action = conform->confdeltype;
- fkconstraint->fk_del_set_cols = NIL;
- fkconstraint->old_conpfeqop = NIL;
- fkconstraint->old_pktable_oid = InvalidOid;
- fkconstraint->skip_validation = false;
- fkconstraint->initially_valid = true;
+ if (parentConForm->conrelid != conform->conrelid)
+ {
+ int numfks;
+ AttrNumber conkey[INDEX_MAX_KEYS];
+ AttrMap *attmap;
+ AttrNumber confkey[INDEX_MAX_KEYS];
+ Oid conpfeqop[INDEX_MAX_KEYS];
+ Oid conppeqop[INDEX_MAX_KEYS];
+ Oid conffeqop[INDEX_MAX_KEYS];
+ int numfkdelsetcols;
+ AttrNumber confdelsetcols[INDEX_MAX_KEYS];
+ Relation refdRel;
- createForeignKeyActionTriggers(partRel, conform->confrelid,
- fkconstraint, fk->conoid,
- conform->conindid,
- InvalidOid, InvalidOid,
- NULL, NULL);
+ DeconstructFkConstraintRow(contup,
+ &numfks,
+ conkey,
+ confkey,
+ conpfeqop,
+ conppeqop,
+ conffeqop,
+ &numfkdelsetcols,
+ confdelsetcols);
+
+ /* Create a synthetic node we'll use throughout */
+ fkconstraint = makeNode(Constraint);
+ fkconstraint->contype = CONSTRAINT_FOREIGN;
+ fkconstraint->conname = pstrdup(NameStr(conform->conname));
+ fkconstraint->deferrable = conform->condeferrable;
+ fkconstraint->initdeferred = conform->condeferred;
+ fkconstraint->skip_validation = true;
+ fkconstraint->initially_valid = true;
+ /* a few irrelevant fields omitted here */
+ fkconstraint->pktable = NULL;
+ fkconstraint->fk_attrs = NIL;
+ fkconstraint->pk_attrs = NIL;
+ fkconstraint->fk_matchtype = conform->confmatchtype;
+ fkconstraint->fk_upd_action = conform->confupdtype;
+ fkconstraint->fk_del_action = conform->confdeltype;
+ fkconstraint->fk_del_set_cols = NIL;
+ fkconstraint->old_conpfeqop = NIL;
+ fkconstraint->old_pktable_oid = InvalidOid;
+ fkconstraint->location = -1;
+
+ attmap = build_attrmap_by_name(RelationGetDescr(partRel),
+ RelationGetDescr(rel),
+ false);
+ for (int i = 0; i < numfks; i++)
+ {
+ Form_pg_attribute att;
+
+ att = TupleDescAttr(RelationGetDescr(partRel),
+ attmap->attnums[conkey[i] - 1] - 1);
+ fkconstraint->fk_attrs = lappend(fkconstraint->fk_attrs,
+ makeString(NameStr(att->attname)));
+ }
+
+ refdRel = table_open(fk->confrelid, AccessShareLock);
+
+ addFkRecurseReferenced(wqueue, fkconstraint, partRel,
+ refdRel,
+ conform->conindid,
+ fk->conoid,
+ numfks,
+ confkey,
+ conkey,
+ conpfeqop,
+ conppeqop,
+ conffeqop,
+ numfkdelsetcols,
+ confdelsetcols,
+ true,
+ InvalidOid, InvalidOid);
+ table_close(refdRel, AccessShareLock);
+ }
ReleaseSysCache(contup);
+ ReleaseSysCache(parentConTup);
}
list_free_deep(fks);
if (trigrel)
@@ -19397,7 +19557,7 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
* completion; this completes the detaching process.
*/
static ObjectAddress
-ATExecDetachPartitionFinalize(Relation rel, RangeVar *name)
+ATExecDetachPartitionFinalize(List **wqueue, Relation rel, RangeVar *name)
{
Relation partRel;
ObjectAddress address;
@@ -19415,7 +19575,7 @@ ATExecDetachPartitionFinalize(Relation rel, RangeVar *name)
*/
WaitForOlderSnapshots(snap->xmin, false);
- DetachPartitionFinalize(rel, partRel, true, InvalidOid);
+ DetachPartitionFinalize(wqueue, rel, partRel, true, InvalidOid);
ObjectAddressSet(address, RelationRelationId, RelationGetRelid(partRel));
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 8c04a24b37d..b73e7dced8f 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -2934,3 +2934,99 @@ DETAIL: drop cascades to table fkpart11.pk
drop cascades to table fkpart11.fk_parted
drop cascades to table fkpart11.fk_another
drop cascades to function fkpart11.print_row()
+-- When a table is attached as partition to a partitioned table that has
+-- a foreign key to another partitioned table, it acquires a clone of the
+-- FK. Upon detach, this clone is not removed, but instead becomes an
+-- independent FK. If it then attaches to the partitioned table again,
+-- the FK from the parent "takes over" ownership of the independent FK rather
+-- than creating a separate one.
+CREATE SCHEMA fkpart12
+ CREATE TABLE fk_p ( id int, jd int, PRIMARY KEY(id, jd)) PARTITION BY list (id)
+ CREATE TABLE fk_p_1 PARTITION OF fk_p FOR VALUES IN (1) PARTITION BY list (jd)
+ CREATE TABLE fk_p_1_1 PARTITION OF fk_p_1 FOR VALUES IN (1)
+ CREATE TABLE fk_p_1_2 PARTITION OF fk_p_1 FOR VALUES IN (2)
+ CREATE TABLE fk_p_2 PARTITION OF fk_p FOR VALUES IN (2) PARTITION BY list (jd)
+ CREATE TABLE fk_p_2_1 PARTITION OF fk_p_2 FOR VALUES IN (1)
+ CREATE TABLE fk_p_2_2 PARTITION OF fk_p_2 FOR VALUES IN (2)
+ CREATE TABLE fk_r_1 ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL)
+ CREATE TABLE fk_r_2 ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL) PARTITION BY list (id)
+ CREATE TABLE fk_r_2_1 PARTITION OF fk_r_2 FOR VALUES IN (2, 1)
+ CREATE TABLE fk_r ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL,
+ FOREIGN KEY (p_id, p_jd) REFERENCES fk_p (id, jd)
+ ) PARTITION BY list (id);
+SET search_path TO fkpart12;
+INSERT INTO fk_p VALUES (1, 1);
+ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1);
+ALTER TABLE fk_r ATTACH PARTITION fk_r_2 FOR VALUES IN (2);
+\d fk_r_2
+ Partitioned table "fkpart12.fk_r_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ id | integer | | not null |
+ p_id | integer | | not null |
+ p_jd | integer | | not null |
+Partition of: fk_r FOR VALUES IN (2)
+Partition key: LIST (id)
+Indexes:
+ "fk_r_2_pkey" PRIMARY KEY, btree (id)
+Foreign-key constraints:
+ TABLE "fk_r" CONSTRAINT "fk_r_p_id_p_jd_fkey" FOREIGN KEY (p_id, p_jd) REFERENCES fk_p(id, jd)
+Number of partitions: 1 (Use \d+ to list them.)
+
+INSERT INTO fk_r VALUES (1, 1, 1);
+INSERT INTO fk_r VALUES (2, 2, 1);
+ERROR: insert or update on table "fk_r_2_1" violates foreign key constraint "fk_r_p_id_p_jd_fkey"
+DETAIL: Key (p_id, p_jd)=(2, 1) is not present in table "fk_p".
+ALTER TABLE fk_r DETACH PARTITION fk_r_1;
+ALTER TABLE fk_r DETACH PARTITION fk_r_2;
+\d fk_r_2
+ Partitioned table "fkpart12.fk_r_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ id | integer | | not null |
+ p_id | integer | | not null |
+ p_jd | integer | | not null |
+Partition key: LIST (id)
+Indexes:
+ "fk_r_2_pkey" PRIMARY KEY, btree (id)
+Foreign-key constraints:
+ "fk_r_p_id_p_jd_fkey" FOREIGN KEY (p_id, p_jd) REFERENCES fk_p(id, jd)
+Number of partitions: 1 (Use \d+ to list them.)
+
+INSERT INTO fk_r_1 VALUES (2, 1, 2); -- should fail
+ERROR: insert or update on table "fk_r_1" violates foreign key constraint "fk_r_p_id_p_jd_fkey"
+DETAIL: Key (p_id, p_jd)=(1, 2) is not present in table "fk_p".
+DELETE FROM fk_p; -- should fail
+ERROR: update or delete on table "fk_p_1_1" violates foreign key constraint "fk_r_1_p_id_p_jd_fkey1" on table "fk_r_1"
+DETAIL: Key (id, jd)=(1, 1) is still referenced from table "fk_r_1".
+ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1);
+ALTER TABLE fk_r ATTACH PARTITION fk_r_2 FOR VALUES IN (2);
+\d fk_r_2
+ Partitioned table "fkpart12.fk_r_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ id | integer | | not null |
+ p_id | integer | | not null |
+ p_jd | integer | | not null |
+Partition of: fk_r FOR VALUES IN (2)
+Partition key: LIST (id)
+Indexes:
+ "fk_r_2_pkey" PRIMARY KEY, btree (id)
+Foreign-key constraints:
+ TABLE "fk_r" CONSTRAINT "fk_r_p_id_p_jd_fkey" FOREIGN KEY (p_id, p_jd) REFERENCES fk_p(id, jd)
+Number of partitions: 1 (Use \d+ to list them.)
+
+DELETE FROM fk_p; -- should fail
+ERROR: update or delete on table "fk_p_1_1" violates foreign key constraint "fk_r_p_id_p_jd_fkey2" on table "fk_r"
+DETAIL: Key (id, jd)=(1, 1) is still referenced from table "fk_r".
+-- these should all fail
+ALTER TABLE fk_r_1 DROP CONSTRAINT fk_r_p_id_p_jd_fkey;
+ERROR: cannot drop inherited constraint "fk_r_p_id_p_jd_fkey" of relation "fk_r_1"
+ALTER TABLE fk_r DROP CONSTRAINT fk_r_p_id_p_jd_fkey1;
+ERROR: cannot drop inherited constraint "fk_r_p_id_p_jd_fkey1" of relation "fk_r"
+ALTER TABLE fk_r_2 DROP CONSTRAINT fk_r_p_id_p_jd_fkey;
+ERROR: cannot drop inherited constraint "fk_r_p_id_p_jd_fkey" of relation "fk_r_2"
+SET client_min_messages TO warning;
+DROP SCHEMA fkpart12 CASCADE;
+RESET client_min_messages;
+RESET search_path;
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index d1aac5357f0..9b2a6b6bff7 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -2086,3 +2086,60 @@ UPDATE fkpart11.pk SET a = 3 WHERE a = 4;
UPDATE fkpart11.pk SET a = 1 WHERE a = 2;
DROP SCHEMA fkpart11 CASCADE;
+
+-- When a table is attached as partition to a partitioned table that has
+-- a foreign key to another partitioned table, it acquires a clone of the
+-- FK. Upon detach, this clone is not removed, but instead becomes an
+-- independent FK. If it then attaches to the partitioned table again,
+-- the FK from the parent "takes over" ownership of the independent FK rather
+-- than creating a separate one.
+CREATE SCHEMA fkpart12
+ CREATE TABLE fk_p ( id int, jd int, PRIMARY KEY(id, jd)) PARTITION BY list (id)
+ CREATE TABLE fk_p_1 PARTITION OF fk_p FOR VALUES IN (1) PARTITION BY list (jd)
+ CREATE TABLE fk_p_1_1 PARTITION OF fk_p_1 FOR VALUES IN (1)
+ CREATE TABLE fk_p_1_2 PARTITION OF fk_p_1 FOR VALUES IN (2)
+ CREATE TABLE fk_p_2 PARTITION OF fk_p FOR VALUES IN (2) PARTITION BY list (jd)
+ CREATE TABLE fk_p_2_1 PARTITION OF fk_p_2 FOR VALUES IN (1)
+ CREATE TABLE fk_p_2_2 PARTITION OF fk_p_2 FOR VALUES IN (2)
+ CREATE TABLE fk_r_1 ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL)
+ CREATE TABLE fk_r_2 ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL) PARTITION BY list (id)
+ CREATE TABLE fk_r_2_1 PARTITION OF fk_r_2 FOR VALUES IN (2, 1)
+ CREATE TABLE fk_r ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL,
+ FOREIGN KEY (p_id, p_jd) REFERENCES fk_p (id, jd)
+ ) PARTITION BY list (id);
+SET search_path TO fkpart12;
+
+INSERT INTO fk_p VALUES (1, 1);
+
+ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1);
+ALTER TABLE fk_r ATTACH PARTITION fk_r_2 FOR VALUES IN (2);
+
+\d fk_r_2
+
+INSERT INTO fk_r VALUES (1, 1, 1);
+INSERT INTO fk_r VALUES (2, 2, 1);
+
+ALTER TABLE fk_r DETACH PARTITION fk_r_1;
+ALTER TABLE fk_r DETACH PARTITION fk_r_2;
+
+\d fk_r_2
+
+INSERT INTO fk_r_1 VALUES (2, 1, 2); -- should fail
+DELETE FROM fk_p; -- should fail
+
+ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1);
+ALTER TABLE fk_r ATTACH PARTITION fk_r_2 FOR VALUES IN (2);
+
+\d fk_r_2
+
+DELETE FROM fk_p; -- should fail
+
+-- these should all fail
+ALTER TABLE fk_r_1 DROP CONSTRAINT fk_r_p_id_p_jd_fkey;
+ALTER TABLE fk_r DROP CONSTRAINT fk_r_p_id_p_jd_fkey1;
+ALTER TABLE fk_r_2 DROP CONSTRAINT fk_r_p_id_p_jd_fkey;
+
+SET client_min_messages TO warning;
+DROP SCHEMA fkpart12 CASCADE;
+RESET client_min_messages;
+RESET search_path;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index d4e9515e9f4..5628b3f09f4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -3250,6 +3250,7 @@ _stringlist
access_vector_t
acquireLocksOnSubLinks_context
add_nulling_relids_context
+addFkConstraintSides
adjust_appendrel_attrs_context
allocfunc
amadjustmembers_function
--
2.39.5
v4_master-0001-Redo-foreign-key-maintenance-during-partit.patchtext/x-diff; charset=utf-8Download
From ae828d23c6158f31d41c0fdba215807f14cada38 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=81lvaro=20Herrera?= <alvherre@alvh.no-ip.org>
Date: Fri, 18 Oct 2024 16:38:34 +0200
Subject: [PATCH v4_master] Redo foreign key maintenance during partition
ATTACH/DETACH
... when the reference table is partitioned.
It turns out that the catalog representation we chose for foreign keys
connecting partitioned tables is suboptimal, particularly in the sense
that a standalone table has a different way to represent it when
referencing a partitioned table, than when the same table becomes a
partition (and vice versa). This difference means we need to create
additional catalog rows on detach (pg_constraint and pg_trigger), and
remove them on attach.
As a very obvious symptom, we were missing action triggers after
detach, which means that you could update/delete rows from the
referenced partitioned table that still had referencing rows at the
other side previously detached, and fail to throw the required errors.
!!!!!!!!!
Note that this means existing FKs that refer to partitioned tables might
have rows that break relational integrity, on tables that were once
partitions.
Another possible problem is that trying to reattach a table
that had been detached would fail indicating that internal triggers
cannot be found, which from the user's point of view is nonsensical.
In branches 15 and above, we fix this by creating a new helper function
addFkConstraint() which is in charge of creating a standalone
pg_constraint row, and repurposing addFkRecurseReferencing() and
addFkRecurseReferenced() so that they're only the recursive routine for
each side of the FK, and they call addFkConstraint() to create
pg_constraint at each partitioning level and add the necessary triggers.
These new routines can be used during partition creation, partition
attach and detach, and foreign key creation. This reduces redundant
code and simplifies the flow.
In branches 14 and 13, we have a much simpler fix that consists on
simply removing the constraint on detach. The reason is that those
branches are missing commit f4566345cf40, which reworked the way this
works in a way that we didn't consider back-patchable at the time. I
doubt we want to change that now, so this is probably the best we can do
there.
We opted to leave branch 12 alone, because it's going to end maintenance
very soon, and any rushed patch might be worse.
In the future we might want to rethink the catalog representation to
avoid this problem, but for now the code seems to do what's required to
make the constraints operate correctly.
Co-authored-by: Jehan-Guillaume de Rorthais <jgdr@dalibo.com>
Co-authored-by: Tender Wang <tndrwang@gmail.com>
Co-authored-by: Alvaro Herrera <alvherre@alvh.no-ip.org>
Reported-by: Guillaume Lelarge <guillaume@lelarge.info>
Reported-by: Jehan-Guillaume de Rorthais <jgdr@dalibo.com>
Reported-by: Thomas Baehler (SBB CFF FFS) <thomas.baehler2@sbb.ch>
Discussion: https://postgr.es/m/20230420144344.40744130@karst
Discussion: https://postgr.es/m/20230705233028.2f554f73@karst
Discussion: https://postgr.es/m/GVAP278MB02787E7134FD691861635A8BC9032@GVAP278MB0278.CHEP278.PROD.OUTLOOK.COM
Discussion: https://postgr.es/m/18541-628a61bc267cd2d3@postgresql.org
---
src/backend/commands/tablecmds.c | 644 ++++++++++++++--------
src/test/regress/expected/foreign_key.out | 96 ++++
src/test/regress/sql/foreign_key.sql | 57 ++
src/tools/pgindent/typedefs.list | 1 +
4 files changed, 557 insertions(+), 241 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 1ccc80087c3..1fb701fbc55 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -349,6 +349,14 @@ typedef struct ForeignTruncateInfo
List *rels;
} ForeignTruncateInfo;
+/* Partial or complete FK creation in addFkConstraint() */
+typedef enum addFkConstraintSides
+{
+ addFkReferencedSide,
+ addFkReferencingSide,
+ addFkBothSides,
+} addFkConstraintSides;
+
/*
* Partition tables are expected to be dropped when the parent partitioned
* table gets dropped. Hence for partitioning we use AUTO dependency.
@@ -508,17 +516,27 @@ static ObjectAddress ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *
Relation rel, Constraint *fkconstraint,
bool recurse, bool recursing,
LOCKMODE lockmode);
-static ObjectAddress addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint,
- Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
- int numfks, int16 *pkattnum, int16 *fkattnum,
- Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
- int numfkdelsetcols, int16 *fkdelsetcols,
- bool old_check_ok,
- Oid parentDelTrigger, Oid parentUpdTrigger,
- bool with_period);
static void validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
int numfksetcols, const int16 *fksetcolsattnums,
List *fksetcols);
+static ObjectAddress addFkConstraint(addFkConstraintSides fkside,
+ char *constraintname,
+ Constraint *fkconstraint, Relation rel,
+ Relation pkrel, Oid indexOid,
+ Oid parentConstr,
+ int numfks, int16 *pkattnum, int16 *fkattnum,
+ Oid *pfeqoperators, Oid *ppeqoperators,
+ Oid *ffeqoperators, int numfkdelsetcols,
+ int16 *fkdelsetcols, bool is_internal,
+ bool with_period);
+static void addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint,
+ Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
+ int numfks, int16 *pkattnum, int16 *fkattnum,
+ Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
+ int numfkdelsetcols, int16 *fkdelsetcols,
+ bool old_check_ok,
+ Oid parentDelTrigger, Oid parentUpdTrigger,
+ bool with_period);
static void addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint,
Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks, int16 *pkattnum, int16 *fkattnum,
@@ -527,7 +545,6 @@ static void addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint,
bool old_check_ok, LOCKMODE lockmode,
Oid parentInsTrigger, Oid parentUpdTrigger,
bool with_period);
-
static void CloneForeignKeyConstraints(List **wqueue, Relation parentRel,
Relation partitionRel);
static void CloneFkReferenced(Relation parentRel, Relation partitionRel);
@@ -652,9 +669,11 @@ static void DropClonedTriggersFromPartition(Oid partitionId);
static ObjectAddress ATExecDetachPartition(List **wqueue, AlteredTableInfo *tab,
Relation rel, RangeVar *name,
bool concurrent);
-static void DetachPartitionFinalize(Relation rel, Relation partRel,
- bool concurrent, Oid defaultPartOid);
-static ObjectAddress ATExecDetachPartitionFinalize(Relation rel, RangeVar *name);
+static void DetachPartitionFinalize(List **wqueue, Relation rel,
+ Relation partRel, bool concurrent,
+ Oid defaultPartOid);
+static ObjectAddress ATExecDetachPartitionFinalize(List **wqueue, Relation rel,
+ RangeVar *name);
static ObjectAddress ATExecAttachPartitionIdx(List **wqueue, Relation parentIdx,
RangeVar *name);
static void validatePartitionedIndex(Relation partedIdx, Relation partedTbl);
@@ -5525,7 +5544,7 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
((PartitionCmd *) cmd->def)->concurrent);
break;
case AT_DetachPartitionFinalize:
- address = ATExecDetachPartitionFinalize(rel, ((PartitionCmd *) cmd->def)->name);
+ address = ATExecDetachPartitionFinalize(wqueue, rel, ((PartitionCmd *) cmd->def)->name);
break;
default: /* oops */
elog(ERROR, "unrecognized alter table type: %d",
@@ -10045,26 +10064,39 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
FindFKPeriodOpers(opclasses[numpks - 1], &periodoperoid, &aggedperiodoperoid);
}
- /*
- * Create all the constraint and trigger objects, recursing to partitions
- * as necessary. First handle the referenced side.
- */
- address = addFkRecurseReferenced(wqueue, fkconstraint, rel, pkrel,
- indexOid,
- InvalidOid, /* no parent constraint */
- numfks,
- pkattnum,
- fkattnum,
- pfeqoperators,
- ppeqoperators,
- ffeqoperators,
- numfkdelsetcols,
- fkdelsetcols,
- old_check_ok,
- InvalidOid, InvalidOid,
- with_period);
+ /* First, create the constraint catalog entry itself. */
+ address = addFkConstraint(addFkBothSides,
+ fkconstraint->conname, fkconstraint, rel, pkrel,
+ indexOid,
+ InvalidOid, /* no parent constraint */
+ numfks,
+ pkattnum,
+ fkattnum,
+ pfeqoperators,
+ ppeqoperators,
+ ffeqoperators,
+ numfkdelsetcols,
+ fkdelsetcols,
+ false,
+ with_period);
- /* Now handle the referencing side. */
+ /* Next process the action triggers at the referenced side and recurse */
+ addFkRecurseReferenced(wqueue, fkconstraint, rel, pkrel,
+ indexOid,
+ address.objectId,
+ numfks,
+ pkattnum,
+ fkattnum,
+ pfeqoperators,
+ ppeqoperators,
+ ffeqoperators,
+ numfkdelsetcols,
+ fkdelsetcols,
+ old_check_ok,
+ InvalidOid, InvalidOid,
+ with_period);
+
+ /* Lastly create the check triggers at the referencing side and recurse */
addFkRecurseReferencing(wqueue, fkconstraint, rel, pkrel,
indexOid,
address.objectId,
@@ -10125,47 +10157,42 @@ validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
}
/*
- * addFkRecurseReferenced
- * subroutine for ATAddForeignKeyConstraint; recurses on the referenced
- * side of the constraint
+ * addFkConstraint
+ * Install pg_constraint entries to implement a foreign key constraint.
+ * Caller must separately invoke addFkRecurseReferenced and
+ * addFkRecurseReferencing, as appropriate, to install pg_trigger entries
+ * and (for partitioned tables) recurse to partitions.
*
- * Create pg_constraint rows for the referenced side of the constraint,
- * referencing the parent of the referencing side; also create action triggers
- * on leaf partitions. If the table is partitioned, recurse to handle each
- * partition.
- *
- * wqueue is the ALTER TABLE work queue; can be NULL when not running as part
- * of an ALTER TABLE sequence.
- * fkconstraint is the constraint being added.
- * rel is the root referencing relation.
- * pkrel is the referenced relation; might be a partition, if recursing.
- * indexOid is the OID of the index (on pkrel) implementing this constraint.
- * parentConstr is the OID of a parent constraint; InvalidOid if this is a
- * top-level constraint.
- * numfks is the number of columns in the foreign key
- * pkattnum is the attnum array of referenced attributes.
- * fkattnum is the attnum array of referencing attributes.
- * numfkdelsetcols is the number of columns in the ON DELETE SET NULL/DEFAULT
- * (...) clause
- * fkdelsetcols is the attnum array of the columns in the ON DELETE SET
- * NULL/DEFAULT clause
- * pf/pp/ffeqoperators are OID array of operators between columns.
- * old_check_ok signals that this constraint replaces an existing one that
- * was already validated (thus this one doesn't need validation).
- * parentDelTrigger and parentUpdTrigger, when being recursively called on
- * a partition, are the OIDs of the parent action triggers for DELETE and
- * UPDATE respectively.
+ * * fkside indicates which side of the FK (or both) to create. Caller must
+ * call addFkRecurseReferenced if it's addFkReferencedSide,
+ * addFkRecurseReferencing if it's addFkReferencingSide, or both if it's
+ * addFkBothSides.
+ * * constraintname the base name for the constraint being added. This is
+ * copied to fkconstraint->conname if the latter is not set.
+ * * fkconstraint is the constraint being added.
+ * * rel is the root referencing relation.
+ * * pkrel is the referenced relation; might be a partition, if recursing.
+ * * indexOid is the OID of the index (on pkrel) implementing this constraint.
+ * * parentConstr is the OID of a parent constraint; InvalidOid if this is a
+ * top-level constraint.
+ * * numfks is the number of columns in the foreign key.
+ * * pkattnum is the attnum array of referenced attributes.
+ * * fkattnum is the attnum array of referencing attributes.
+ * * pf/pp/ffeqoperators are OID array of operators between columns.
+ * * numfkdelsetcols is the number of columns in the ON DELETE SET NULL/DEFAULT
+ * (...) clause.
+ * * fkdelsetcols is the attnum array of the columns in the ON DELETE SET
+ * NULL/DEFAULT clause.
+ * - with_period: true if this is a temporal FKs
*/
static ObjectAddress
-addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
- Relation pkrel, Oid indexOid, Oid parentConstr,
- int numfks,
- int16 *pkattnum, int16 *fkattnum, Oid *pfeqoperators,
- Oid *ppeqoperators, Oid *ffeqoperators,
- int numfkdelsetcols, int16 *fkdelsetcols,
- bool old_check_ok,
- Oid parentDelTrigger, Oid parentUpdTrigger,
- bool with_period)
+addFkConstraint(addFkConstraintSides fkside,
+ char *constraintname, Constraint *fkconstraint,
+ Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
+ int numfks, int16 *pkattnum,
+ int16 *fkattnum, Oid *pfeqoperators, Oid *ppeqoperators,
+ Oid *ffeqoperators, int numfkdelsetcols, int16 *fkdelsetcols,
+ bool is_internal, bool with_period)
{
ObjectAddress address;
Oid constrOid;
@@ -10173,8 +10200,6 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
bool conislocal;
int16 coninhcount;
bool connoinherit;
- Oid deleteTriggerOid,
- updateTriggerOid;
/*
* Verify relkind for each referenced partition. At the top level, this
@@ -10193,13 +10218,16 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
*/
if (ConstraintNameIsUsed(CONSTRAINT_RELATION,
RelationGetRelid(rel),
- fkconstraint->conname))
+ constraintname))
conname = ChooseConstraintName(RelationGetRelationName(rel),
ChooseForeignKeyConstraintNameAddition(fkconstraint->fk_attrs),
"fkey",
RelationGetNamespace(rel), NIL);
else
- conname = fkconstraint->conname;
+ conname = constraintname;
+
+ if (fkconstraint->conname == NULL)
+ fkconstraint->conname = pstrdup(conname);
if (OidIsValid(parentConstr))
{
@@ -10252,33 +10280,97 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
coninhcount, /* inhcount */
connoinherit, /* conNoInherit */
with_period, /* conPeriod */
- false); /* is_internal */
+ is_internal); /* is_internal */
ObjectAddressSet(address, ConstraintRelationId, constrOid);
- /*
- * Mark the child constraint as part of the parent constraint; it must not
- * be dropped on its own. (This constraint is deleted when the partition
- * is detached, but a special check needs to occur that the partition
- * contains no referenced values.)
- */
if (OidIsValid(parentConstr))
{
ObjectAddress referenced;
ObjectAddressSet(referenced, ConstraintRelationId, parentConstr);
- recordDependencyOn(&address, &referenced, DEPENDENCY_INTERNAL);
+ if (fkside == addFkReferencedSide)
+ {
+ /*
+ * Mark the child constraint as part of the parent constraint; it
+ * must not be dropped on its own. (This constraint is deleted
+ * when the partition is detached, but a special check needs to
+ * occur that the partition contains no referenced values.)
+ */
+ recordDependencyOn(&address, &referenced, DEPENDENCY_INTERNAL);
+ }
+ else
+ {
+ /*
+ * Give this constraint partition-type dependencies on the parent
+ * constraint as well as the table.
+ */
+ recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_PRI);
+ ObjectAddressSet(referenced, RelationRelationId, RelationGetRelid(rel));
+ recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_SEC);
+ }
}
/* make new constraint visible, in case we add more */
CommandCounterIncrement();
+ return address;
+}
+
+/*
+ * addFkRecurseReferenced
+ * Recursive helper for the referenced side of foreign key creation,
+ * which creates the action triggers and recurses
+ *
+ * If the referenced relation is a plain relation, create the necessary action
+ * triggers that implement the constraint. If the referenced relation is a
+ * partitioned table, then we create a pg_constraint row referencing the parent
+ * of the referencing side for it and recurse on this routine for each
+ * partition.
+ *
+ * wqueue is the ALTER TABLE work queue; can be NULL when not running as part
+ * of an ALTER TABLE sequence.
+ * fkconstraint is the constraint being added.
+ * rel is the root referencing relation.
+ * pkrel is the referenced relation; might be a partition, if recursing.
+ * indexOid is the OID of the index (on pkrel) implementing this constraint.
+ * parentConstr is the OID of a parent constraint; InvalidOid if this is a
+ * top-level constraint.
+ * numfks is the number of columns in the foreign key
+ * pkattnum is the attnum array of referenced attributes.
+ * fkattnum is the attnum array of referencing attributes.
+ * numfkdelsetcols is the number of columns in the ON DELETE SET NULL/DEFAULT
+ * (...) clause
+ * fkdelsetcols is the attnum array of the columns in the ON DELETE SET
+ * NULL/DEFAULT clause
+ * pf/pp/ffeqoperators are OID array of operators between columns.
+ * old_check_ok signals that this constraint replaces an existing one that
+ * was already validated (thus this one doesn't need validation).
+ * parentDelTrigger and parentUpdTrigger, when being recursively called on
+ * a partition, are the OIDs of the parent action triggers for DELETE and
+ * UPDATE respectively.
+ * with_period: true if this is a temporal FKs
+ */
+static void
+addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
+ Relation pkrel, Oid indexOid, Oid parentConstr,
+ int numfks,
+ int16 *pkattnum, int16 *fkattnum, Oid *pfeqoperators,
+ Oid *ppeqoperators, Oid *ffeqoperators,
+ int numfkdelsetcols, int16 *fkdelsetcols,
+ bool old_check_ok,
+ Oid parentDelTrigger, Oid parentUpdTrigger,
+ bool with_period)
+{
+ Oid deleteTriggerOid,
+ updateTriggerOid;
+
/*
* Create the action triggers that enforce the constraint.
*/
createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
fkconstraint,
- constrOid, indexOid,
+ parentConstr, indexOid,
parentDelTrigger, parentUpdTrigger,
&deleteTriggerOid, &updateTriggerOid);
@@ -10297,6 +10389,7 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
AttrMap *map;
AttrNumber *mapped_pkattnum;
Oid partIndexId;
+ ObjectAddress address;
partRel = table_open(pd->oids[i], ShareRowExclusiveLock);
@@ -10316,13 +10409,23 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
else
mapped_pkattnum = pkattnum;
- /* do the deed */
+ /* Determine the index to use at this level */
partIndexId = index_get_partition(partRel, indexOid);
if (!OidIsValid(partIndexId))
elog(ERROR, "index for %u not found in partition %s",
indexOid, RelationGetRelationName(partRel));
+
+ /* Create entry at this level ... */
+ address = addFkConstraint(addFkReferencedSide,
+ fkconstraint->conname, fkconstraint, rel,
+ partRel, partIndexId, parentConstr,
+ numfks, mapped_pkattnum,
+ fkattnum, pfeqoperators, ppeqoperators,
+ ffeqoperators, numfkdelsetcols,
+ fkdelsetcols, true, with_period);
+ /* ... and recurse to our children */
addFkRecurseReferenced(wqueue, fkconstraint, rel, partRel,
- partIndexId, constrOid, numfks,
+ partIndexId, address.objectId, numfks,
mapped_pkattnum, fkattnum,
pfeqoperators, ppeqoperators, ffeqoperators,
numfkdelsetcols, fkdelsetcols,
@@ -10339,13 +10442,12 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
}
}
}
-
- return address;
}
/*
* addFkRecurseReferencing
- * subroutine for ATAddForeignKeyConstraint and CloneFkReferencing
+ * Recursive helper for the referencing side of foreign key creation,
+ * which creates the check triggers and recurses
*
* If the referencing relation is a plain relation, create the necessary check
* triggers that implement the constraint, and set up for Phase 3 constraint
@@ -10467,10 +10569,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
AttrMap *attmap;
AttrNumber mapped_fkattnum[INDEX_MAX_KEYS];
bool attached;
- char *conname;
- Oid constrOid;
- ObjectAddress address,
- referenced;
+ ObjectAddress address;
ListCell *cell;
CheckAlterTableIsSafe(partition);
@@ -10513,66 +10612,19 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
/*
* No luck finding a good constraint to reuse; create our own.
*/
- if (ConstraintNameIsUsed(CONSTRAINT_RELATION,
- RelationGetRelid(partition),
- fkconstraint->conname))
- conname = ChooseConstraintName(RelationGetRelationName(partition),
- ChooseForeignKeyConstraintNameAddition(fkconstraint->fk_attrs),
- "fkey",
- RelationGetNamespace(partition), NIL);
- else
- conname = fkconstraint->conname;
- constrOid =
- CreateConstraintEntry(conname,
- RelationGetNamespace(partition),
- CONSTRAINT_FOREIGN,
- fkconstraint->deferrable,
- fkconstraint->initdeferred,
- fkconstraint->initially_valid,
- parentConstr,
- partitionId,
- mapped_fkattnum,
- numfks,
- numfks,
- InvalidOid,
- indexOid,
- RelationGetRelid(pkrel),
- pkattnum,
- pfeqoperators,
- ppeqoperators,
- ffeqoperators,
- numfks,
- fkconstraint->fk_upd_action,
- fkconstraint->fk_del_action,
- fkdelsetcols,
- numfkdelsetcols,
- fkconstraint->fk_matchtype,
- NULL,
- NULL,
- NULL,
- false, /* conIsLocal */
- 1, /* conInhCount */
- false, /* conNoInherit */
- with_period, /* conPeriod */
- false);
-
- /*
- * Give this constraint partition-type dependencies on the parent
- * constraint as well as the table.
- */
- ObjectAddressSet(address, ConstraintRelationId, constrOid);
- ObjectAddressSet(referenced, ConstraintRelationId, parentConstr);
- recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_PRI);
- ObjectAddressSet(referenced, RelationRelationId, partitionId);
- recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_SEC);
-
- /* Make all this visible before recursing */
- CommandCounterIncrement();
+ address = addFkConstraint(addFkReferencingSide,
+ fkconstraint->conname, fkconstraint,
+ partition, pkrel, indexOid, parentConstr,
+ numfks, pkattnum,
+ mapped_fkattnum, pfeqoperators,
+ ppeqoperators, ffeqoperators,
+ numfkdelsetcols, fkdelsetcols, true,
+ with_period);
/* call ourselves to finalize the creation and we're done */
addFkRecurseReferencing(wqueue, fkconstraint, partition, pkrel,
indexOid,
- constrOid,
+ address.objectId,
numfks,
pkattnum,
mapped_fkattnum,
@@ -10706,6 +10758,7 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
int numfkdelsetcols;
AttrNumber confdelsetcols[INDEX_MAX_KEYS];
Constraint *fkconstraint;
+ ObjectAddress address;
Oid deleteTriggerOid,
updateTriggerOid;
@@ -10739,7 +10792,7 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
* Because we're only expanding the key space at the referenced side,
* we don't need to prevent any operation in the referencing table, so
* AccessShareLock suffices (assumes that dropping the constraint
- * acquires AEL).
+ * acquires AccessExclusiveLock).
*/
fkRel = table_open(constrForm->conrelid, AccessShareLock);
@@ -10805,12 +10858,21 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
constrForm->confrelid, constrForm->conrelid,
&deleteTriggerOid, &updateTriggerOid);
+ /* Add this constraint ... */
+ address = addFkConstraint(addFkReferencedSide,
+ fkconstraint->conname, fkconstraint, fkRel,
+ partitionRel, partIndexId, constrOid,
+ numfks, mapped_confkey,
+ conkey, conpfeqop, conppeqop, conffeqop,
+ numfkdelsetcols, confdelsetcols, false,
+ constrForm->conperiod);
+ /* ... and recurse */
addFkRecurseReferenced(NULL,
fkconstraint,
fkRel,
partitionRel,
partIndexId,
- constrOid,
+ address.objectId,
numfks,
mapped_confkey,
conkey,
@@ -10841,8 +10903,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
* child.
*
* If wqueue is given, it is used to set up phase-3 verification for each
- * cloned constraint; if omitted, we assume that such verification is not
- * needed (example: the partition is being created anew).
+ * cloned constraint; omit it if such verification is not needed
+ * (example: the partition is being created anew).
*/
static void
CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
@@ -10926,9 +10988,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
Constraint *fkconstraint;
bool attached;
Oid indexOid;
- Oid constrOid;
- ObjectAddress address,
- referenced;
+ ObjectAddress address;
ListCell *lc;
Oid insertTriggerOid,
updateTriggerOid;
@@ -11026,7 +11086,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
fkconstraint->old_conpfeqop = NIL;
fkconstraint->old_pktable_oid = InvalidOid;
fkconstraint->skip_validation = false;
- fkconstraint->initially_valid = true;
+ fkconstraint->initially_valid = constrForm->convalidated;
for (int i = 0; i < numfks; i++)
{
Form_pg_attribute att;
@@ -11036,73 +11096,30 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
fkconstraint->fk_attrs = lappend(fkconstraint->fk_attrs,
makeString(NameStr(att->attname)));
}
- if (ConstraintNameIsUsed(CONSTRAINT_RELATION,
- RelationGetRelid(partRel),
- NameStr(constrForm->conname)))
- fkconstraint->conname =
- ChooseConstraintName(RelationGetRelationName(partRel),
- ChooseForeignKeyConstraintNameAddition(fkconstraint->fk_attrs),
- "fkey",
- RelationGetNamespace(partRel), NIL);
- else
- fkconstraint->conname = pstrdup(NameStr(constrForm->conname));
indexOid = constrForm->conindid;
with_period = constrForm->conperiod;
- constrOid =
- CreateConstraintEntry(fkconstraint->conname,
- constrForm->connamespace,
- CONSTRAINT_FOREIGN,
- fkconstraint->deferrable,
- fkconstraint->initdeferred,
- constrForm->convalidated,
- parentConstrOid,
- RelationGetRelid(partRel),
- mapped_conkey,
- numfks,
- numfks,
- InvalidOid, /* not a domain constraint */
- indexOid,
- constrForm->confrelid, /* same foreign rel */
- confkey,
- conpfeqop,
- conppeqop,
- conffeqop,
- numfks,
- fkconstraint->fk_upd_action,
- fkconstraint->fk_del_action,
- confdelsetcols,
- numfkdelsetcols,
- fkconstraint->fk_matchtype,
- NULL,
- NULL,
- NULL,
- false, /* conIsLocal */
- 1, /* conInhCount */
- false, /* conNoInherit */
- with_period, /* conPeriod */
- true);
- /* Set up partition dependencies for the new constraint */
- ObjectAddressSet(address, ConstraintRelationId, constrOid);
- ObjectAddressSet(referenced, ConstraintRelationId, parentConstrOid);
- recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_PRI);
- ObjectAddressSet(referenced, RelationRelationId,
- RelationGetRelid(partRel));
- recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_SEC);
+ /* Create the pg_constraint entry at this level */
+ address = addFkConstraint(addFkReferencingSide,
+ NameStr(constrForm->conname), fkconstraint,
+ partRel, pkrel, indexOid, parentConstrOid,
+ numfks, confkey,
+ mapped_conkey, conpfeqop,
+ conppeqop, conffeqop,
+ numfkdelsetcols, confdelsetcols,
+ false, with_period);
/* Done with the cloned constraint's tuple */
ReleaseSysCache(tuple);
- /* Make all this visible before recursing */
- CommandCounterIncrement();
-
+ /* Create the check triggers, and recurse to partitions, if any */
addFkRecurseReferencing(wqueue,
fkconstraint,
partRel,
pkrel,
indexOid,
- constrOid,
+ address.objectId,
numfks,
confkey,
mapped_conkey,
@@ -11267,6 +11284,81 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
partRelid);
+ /*
+ * If the referenced table is partitioned, then the partition we're
+ * attaching now has extra pg_constraint rows and action triggers that are
+ * no longer needed. Remove those.
+ */
+ if (get_rel_relkind(fk->confrelid) == RELKIND_PARTITIONED_TABLE)
+ {
+ Relation pg_constraint = table_open(ConstraintRelationId, RowShareLock);
+ ObjectAddresses *objs;
+ HeapTuple consttup;
+
+ ScanKeyInit(&key,
+ Anum_pg_constraint_conrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(fk->conrelid));
+
+ scan = systable_beginscan(pg_constraint,
+ ConstraintRelidTypidNameIndexId,
+ true, NULL, 1, &key);
+ objs = new_object_addresses();
+ while ((consttup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_constraint conform = (Form_pg_constraint) GETSTRUCT(consttup);
+
+ if (conform->conparentid != fk->conoid)
+ continue;
+ else
+ {
+ ObjectAddress addr;
+ SysScanDesc scan2;
+ ScanKeyData key2;
+ int n PG_USED_FOR_ASSERTS_ONLY;
+
+ ObjectAddressSet(addr, ConstraintRelationId, conform->oid);
+ add_exact_object_address(&addr, objs);
+
+ /*
+ * First we must delete the dependency record that binds the
+ * constraint records together.
+ */
+ n = deleteDependencyRecordsForSpecific(ConstraintRelationId,
+ conform->oid,
+ DEPENDENCY_INTERNAL,
+ ConstraintRelationId,
+ fk->conoid);
+ Assert(n == 1); /* actually only one is expected */
+
+ /*
+ * Now search for the triggers for this constraint and set
+ * them up for deletion too
+ */
+ ScanKeyInit(&key2,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conform->oid));
+ scan2 = systable_beginscan(trigrel, TriggerConstraintIndexId,
+ true, NULL, 1, &key2);
+ while ((trigtup = systable_getnext(scan2)) != NULL)
+ {
+ ObjectAddressSet(addr, TriggerRelationId,
+ ((Form_pg_trigger) GETSTRUCT(trigtup))->oid);
+ add_exact_object_address(&addr, objs);
+ }
+ systable_endscan(scan2);
+ }
+ }
+ /* make the dependency deletions visible */
+ CommandCounterIncrement();
+ performMultipleDeletions(objs, DROP_RESTRICT,
+ PERFORM_DELETION_INTERNAL);
+ systable_endscan(scan);
+
+ table_close(pg_constraint, RowShareLock);
+ }
+
CommandCounterIncrement();
return true;
}
@@ -19284,7 +19376,7 @@ ATExecDetachPartition(List **wqueue, AlteredTableInfo *tab, Relation rel,
}
/* Do the final part of detaching */
- DetachPartitionFinalize(rel, partRel, concurrent, defaultPartOid);
+ DetachPartitionFinalize(wqueue, rel, partRel, concurrent, defaultPartOid);
ObjectAddressSet(address, RelationRelationId, RelationGetRelid(partRel));
@@ -19301,8 +19393,8 @@ ATExecDetachPartition(List **wqueue, AlteredTableInfo *tab, Relation rel,
* transaction of the concurrent algorithm fails (crash or abort).
*/
static void
-DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
- Oid defaultPartOid)
+DetachPartitionFinalize(List **wqueue, Relation rel, Relation partRel,
+ bool concurrent, Oid defaultPartOid)
{
Relation classRel;
List *fks;
@@ -19338,8 +19430,11 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
{
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
+ HeapTuple parentConTup;
Form_pg_constraint conform;
+ Form_pg_constraint parentConForm;
Constraint *fkconstraint;
+ Oid parentConstrOid;
Oid insertTriggerOid,
updateTriggerOid;
@@ -19356,7 +19451,20 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
continue;
}
- /* unset conparentid and adjust conislocal, coninhcount, etc. */
+ parentConstrOid = conform->conparentid;
+
+ Assert(OidIsValid(conform->conparentid));
+ parentConTup = SearchSysCache1(CONSTROID,
+ ObjectIdGetDatum(parentConstrOid));
+ if (!HeapTupleIsValid(parentConTup))
+ elog(ERROR, "cache lookup failed for constraint %u",
+ conform->conparentid);
+ parentConForm = (Form_pg_constraint) GETSTRUCT(parentConTup);
+
+ /*
+ * The constraint on this table must be marked no longer a child of
+ * the parent's constraint, as do its check triggers.
+ */
ConstraintSetParentConstraint(fk->conoid, InvalidOid, InvalidOid);
/*
@@ -19374,35 +19482,89 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
RelationGetRelid(partRel));
/*
- * Make the action triggers on the referenced relation. When this was
- * a partition the action triggers pointed to the parent rel (they
- * still do), but now we need separate ones of our own.
+ * If the referenced side is partitioned (which we know because our
+ * parent's constraint points to a different relation than ours) then
+ * we must, in addition to the above, create pg_constraint rows that
+ * point to each partition, each with its own action triggers.
*/
- fkconstraint = makeNode(Constraint);
- fkconstraint->contype = CONSTRAINT_FOREIGN;
- fkconstraint->conname = pstrdup(NameStr(conform->conname));
- fkconstraint->deferrable = conform->condeferrable;
- fkconstraint->initdeferred = conform->condeferred;
- fkconstraint->location = -1;
- fkconstraint->pktable = NULL;
- fkconstraint->fk_attrs = NIL;
- fkconstraint->pk_attrs = NIL;
- fkconstraint->fk_matchtype = conform->confmatchtype;
- fkconstraint->fk_upd_action = conform->confupdtype;
- fkconstraint->fk_del_action = conform->confdeltype;
- fkconstraint->fk_del_set_cols = NIL;
- fkconstraint->old_conpfeqop = NIL;
- fkconstraint->old_pktable_oid = InvalidOid;
- fkconstraint->skip_validation = false;
- fkconstraint->initially_valid = true;
+ if (parentConForm->conrelid != conform->conrelid)
+ {
+ int numfks;
+ AttrNumber conkey[INDEX_MAX_KEYS];
+ AttrMap *attmap;
+ AttrNumber confkey[INDEX_MAX_KEYS];
+ Oid conpfeqop[INDEX_MAX_KEYS];
+ Oid conppeqop[INDEX_MAX_KEYS];
+ Oid conffeqop[INDEX_MAX_KEYS];
+ int numfkdelsetcols;
+ AttrNumber confdelsetcols[INDEX_MAX_KEYS];
+ Relation refdRel;
- createForeignKeyActionTriggers(partRel, conform->confrelid,
- fkconstraint, fk->conoid,
- conform->conindid,
- InvalidOid, InvalidOid,
- NULL, NULL);
+ DeconstructFkConstraintRow(contup,
+ &numfks,
+ conkey,
+ confkey,
+ conpfeqop,
+ conppeqop,
+ conffeqop,
+ &numfkdelsetcols,
+ confdelsetcols);
+
+ /* Create a synthetic node we'll use throughout */
+ fkconstraint = makeNode(Constraint);
+ fkconstraint->contype = CONSTRAINT_FOREIGN;
+ fkconstraint->conname = pstrdup(NameStr(conform->conname));
+ fkconstraint->deferrable = conform->condeferrable;
+ fkconstraint->initdeferred = conform->condeferred;
+ fkconstraint->skip_validation = true;
+ fkconstraint->initially_valid = true;
+ /* a few irrelevant fields omitted here */
+ fkconstraint->pktable = NULL;
+ fkconstraint->fk_attrs = NIL;
+ fkconstraint->pk_attrs = NIL;
+ fkconstraint->fk_matchtype = conform->confmatchtype;
+ fkconstraint->fk_upd_action = conform->confupdtype;
+ fkconstraint->fk_del_action = conform->confdeltype;
+ fkconstraint->fk_del_set_cols = NIL;
+ fkconstraint->old_conpfeqop = NIL;
+ fkconstraint->old_pktable_oid = InvalidOid;
+ fkconstraint->location = -1;
+
+ attmap = build_attrmap_by_name(RelationGetDescr(partRel),
+ RelationGetDescr(rel),
+ false);
+ for (int i = 0; i < numfks; i++)
+ {
+ Form_pg_attribute att;
+
+ att = TupleDescAttr(RelationGetDescr(partRel),
+ attmap->attnums[conkey[i] - 1] - 1);
+ fkconstraint->fk_attrs = lappend(fkconstraint->fk_attrs,
+ makeString(NameStr(att->attname)));
+ }
+
+ refdRel = table_open(fk->confrelid, AccessShareLock);
+
+ addFkRecurseReferenced(wqueue, fkconstraint, partRel,
+ refdRel,
+ conform->conindid,
+ fk->conoid,
+ numfks,
+ confkey,
+ conkey,
+ conpfeqop,
+ conppeqop,
+ conffeqop,
+ numfkdelsetcols,
+ confdelsetcols,
+ true,
+ InvalidOid, InvalidOid,
+ conform->conperiod);
+ table_close(refdRel, AccessShareLock);
+ }
ReleaseSysCache(contup);
+ ReleaseSysCache(parentConTup);
}
list_free_deep(fks);
if (trigrel)
@@ -19549,7 +19711,7 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
* completion; this completes the detaching process.
*/
static ObjectAddress
-ATExecDetachPartitionFinalize(Relation rel, RangeVar *name)
+ATExecDetachPartitionFinalize(List **wqueue, Relation rel, RangeVar *name)
{
Relation partRel;
ObjectAddress address;
@@ -19567,7 +19729,7 @@ ATExecDetachPartitionFinalize(Relation rel, RangeVar *name)
*/
WaitForOlderSnapshots(snap->xmin, false);
- DetachPartitionFinalize(rel, partRel, true, InvalidOid);
+ DetachPartitionFinalize(wqueue, rel, partRel, true, InvalidOid);
ObjectAddressSet(address, RelationRelationId, RelationGetRelid(partRel));
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 8c04a24b37d..b73e7dced8f 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -2934,3 +2934,99 @@ DETAIL: drop cascades to table fkpart11.pk
drop cascades to table fkpart11.fk_parted
drop cascades to table fkpart11.fk_another
drop cascades to function fkpart11.print_row()
+-- When a table is attached as partition to a partitioned table that has
+-- a foreign key to another partitioned table, it acquires a clone of the
+-- FK. Upon detach, this clone is not removed, but instead becomes an
+-- independent FK. If it then attaches to the partitioned table again,
+-- the FK from the parent "takes over" ownership of the independent FK rather
+-- than creating a separate one.
+CREATE SCHEMA fkpart12
+ CREATE TABLE fk_p ( id int, jd int, PRIMARY KEY(id, jd)) PARTITION BY list (id)
+ CREATE TABLE fk_p_1 PARTITION OF fk_p FOR VALUES IN (1) PARTITION BY list (jd)
+ CREATE TABLE fk_p_1_1 PARTITION OF fk_p_1 FOR VALUES IN (1)
+ CREATE TABLE fk_p_1_2 PARTITION OF fk_p_1 FOR VALUES IN (2)
+ CREATE TABLE fk_p_2 PARTITION OF fk_p FOR VALUES IN (2) PARTITION BY list (jd)
+ CREATE TABLE fk_p_2_1 PARTITION OF fk_p_2 FOR VALUES IN (1)
+ CREATE TABLE fk_p_2_2 PARTITION OF fk_p_2 FOR VALUES IN (2)
+ CREATE TABLE fk_r_1 ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL)
+ CREATE TABLE fk_r_2 ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL) PARTITION BY list (id)
+ CREATE TABLE fk_r_2_1 PARTITION OF fk_r_2 FOR VALUES IN (2, 1)
+ CREATE TABLE fk_r ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL,
+ FOREIGN KEY (p_id, p_jd) REFERENCES fk_p (id, jd)
+ ) PARTITION BY list (id);
+SET search_path TO fkpart12;
+INSERT INTO fk_p VALUES (1, 1);
+ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1);
+ALTER TABLE fk_r ATTACH PARTITION fk_r_2 FOR VALUES IN (2);
+\d fk_r_2
+ Partitioned table "fkpart12.fk_r_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ id | integer | | not null |
+ p_id | integer | | not null |
+ p_jd | integer | | not null |
+Partition of: fk_r FOR VALUES IN (2)
+Partition key: LIST (id)
+Indexes:
+ "fk_r_2_pkey" PRIMARY KEY, btree (id)
+Foreign-key constraints:
+ TABLE "fk_r" CONSTRAINT "fk_r_p_id_p_jd_fkey" FOREIGN KEY (p_id, p_jd) REFERENCES fk_p(id, jd)
+Number of partitions: 1 (Use \d+ to list them.)
+
+INSERT INTO fk_r VALUES (1, 1, 1);
+INSERT INTO fk_r VALUES (2, 2, 1);
+ERROR: insert or update on table "fk_r_2_1" violates foreign key constraint "fk_r_p_id_p_jd_fkey"
+DETAIL: Key (p_id, p_jd)=(2, 1) is not present in table "fk_p".
+ALTER TABLE fk_r DETACH PARTITION fk_r_1;
+ALTER TABLE fk_r DETACH PARTITION fk_r_2;
+\d fk_r_2
+ Partitioned table "fkpart12.fk_r_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ id | integer | | not null |
+ p_id | integer | | not null |
+ p_jd | integer | | not null |
+Partition key: LIST (id)
+Indexes:
+ "fk_r_2_pkey" PRIMARY KEY, btree (id)
+Foreign-key constraints:
+ "fk_r_p_id_p_jd_fkey" FOREIGN KEY (p_id, p_jd) REFERENCES fk_p(id, jd)
+Number of partitions: 1 (Use \d+ to list them.)
+
+INSERT INTO fk_r_1 VALUES (2, 1, 2); -- should fail
+ERROR: insert or update on table "fk_r_1" violates foreign key constraint "fk_r_p_id_p_jd_fkey"
+DETAIL: Key (p_id, p_jd)=(1, 2) is not present in table "fk_p".
+DELETE FROM fk_p; -- should fail
+ERROR: update or delete on table "fk_p_1_1" violates foreign key constraint "fk_r_1_p_id_p_jd_fkey1" on table "fk_r_1"
+DETAIL: Key (id, jd)=(1, 1) is still referenced from table "fk_r_1".
+ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1);
+ALTER TABLE fk_r ATTACH PARTITION fk_r_2 FOR VALUES IN (2);
+\d fk_r_2
+ Partitioned table "fkpart12.fk_r_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ id | integer | | not null |
+ p_id | integer | | not null |
+ p_jd | integer | | not null |
+Partition of: fk_r FOR VALUES IN (2)
+Partition key: LIST (id)
+Indexes:
+ "fk_r_2_pkey" PRIMARY KEY, btree (id)
+Foreign-key constraints:
+ TABLE "fk_r" CONSTRAINT "fk_r_p_id_p_jd_fkey" FOREIGN KEY (p_id, p_jd) REFERENCES fk_p(id, jd)
+Number of partitions: 1 (Use \d+ to list them.)
+
+DELETE FROM fk_p; -- should fail
+ERROR: update or delete on table "fk_p_1_1" violates foreign key constraint "fk_r_p_id_p_jd_fkey2" on table "fk_r"
+DETAIL: Key (id, jd)=(1, 1) is still referenced from table "fk_r".
+-- these should all fail
+ALTER TABLE fk_r_1 DROP CONSTRAINT fk_r_p_id_p_jd_fkey;
+ERROR: cannot drop inherited constraint "fk_r_p_id_p_jd_fkey" of relation "fk_r_1"
+ALTER TABLE fk_r DROP CONSTRAINT fk_r_p_id_p_jd_fkey1;
+ERROR: cannot drop inherited constraint "fk_r_p_id_p_jd_fkey1" of relation "fk_r"
+ALTER TABLE fk_r_2 DROP CONSTRAINT fk_r_p_id_p_jd_fkey;
+ERROR: cannot drop inherited constraint "fk_r_p_id_p_jd_fkey" of relation "fk_r_2"
+SET client_min_messages TO warning;
+DROP SCHEMA fkpart12 CASCADE;
+RESET client_min_messages;
+RESET search_path;
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index d1aac5357f0..9b2a6b6bff7 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -2086,3 +2086,60 @@ UPDATE fkpart11.pk SET a = 3 WHERE a = 4;
UPDATE fkpart11.pk SET a = 1 WHERE a = 2;
DROP SCHEMA fkpart11 CASCADE;
+
+-- When a table is attached as partition to a partitioned table that has
+-- a foreign key to another partitioned table, it acquires a clone of the
+-- FK. Upon detach, this clone is not removed, but instead becomes an
+-- independent FK. If it then attaches to the partitioned table again,
+-- the FK from the parent "takes over" ownership of the independent FK rather
+-- than creating a separate one.
+CREATE SCHEMA fkpart12
+ CREATE TABLE fk_p ( id int, jd int, PRIMARY KEY(id, jd)) PARTITION BY list (id)
+ CREATE TABLE fk_p_1 PARTITION OF fk_p FOR VALUES IN (1) PARTITION BY list (jd)
+ CREATE TABLE fk_p_1_1 PARTITION OF fk_p_1 FOR VALUES IN (1)
+ CREATE TABLE fk_p_1_2 PARTITION OF fk_p_1 FOR VALUES IN (2)
+ CREATE TABLE fk_p_2 PARTITION OF fk_p FOR VALUES IN (2) PARTITION BY list (jd)
+ CREATE TABLE fk_p_2_1 PARTITION OF fk_p_2 FOR VALUES IN (1)
+ CREATE TABLE fk_p_2_2 PARTITION OF fk_p_2 FOR VALUES IN (2)
+ CREATE TABLE fk_r_1 ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL)
+ CREATE TABLE fk_r_2 ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL) PARTITION BY list (id)
+ CREATE TABLE fk_r_2_1 PARTITION OF fk_r_2 FOR VALUES IN (2, 1)
+ CREATE TABLE fk_r ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL,
+ FOREIGN KEY (p_id, p_jd) REFERENCES fk_p (id, jd)
+ ) PARTITION BY list (id);
+SET search_path TO fkpart12;
+
+INSERT INTO fk_p VALUES (1, 1);
+
+ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1);
+ALTER TABLE fk_r ATTACH PARTITION fk_r_2 FOR VALUES IN (2);
+
+\d fk_r_2
+
+INSERT INTO fk_r VALUES (1, 1, 1);
+INSERT INTO fk_r VALUES (2, 2, 1);
+
+ALTER TABLE fk_r DETACH PARTITION fk_r_1;
+ALTER TABLE fk_r DETACH PARTITION fk_r_2;
+
+\d fk_r_2
+
+INSERT INTO fk_r_1 VALUES (2, 1, 2); -- should fail
+DELETE FROM fk_p; -- should fail
+
+ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1);
+ALTER TABLE fk_r ATTACH PARTITION fk_r_2 FOR VALUES IN (2);
+
+\d fk_r_2
+
+DELETE FROM fk_p; -- should fail
+
+-- these should all fail
+ALTER TABLE fk_r_1 DROP CONSTRAINT fk_r_p_id_p_jd_fkey;
+ALTER TABLE fk_r DROP CONSTRAINT fk_r_p_id_p_jd_fkey1;
+ALTER TABLE fk_r_2 DROP CONSTRAINT fk_r_p_id_p_jd_fkey;
+
+SET client_min_messages TO warning;
+DROP SCHEMA fkpart12 CASCADE;
+RESET client_min_messages;
+RESET search_path;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 57de1acff3a..49a0d58bd98 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -3271,6 +3271,7 @@ _stringlist
access_vector_t
acquireLocksOnSubLinks_context
add_nulling_relids_context
+addFkConstraintSides
adjust_appendrel_attrs_context
allocfunc
amadjustmembers_function
--
2.39.5
Alvaro Herrera <alvherre@alvh.no-ip.org> 于2024年10月18日周五 22:52写道:
On 2024-Sep-26, Jehan-Guillaume de Rorthais wrote:
REL_14_STABLE backport doesn't seem trivial, so I'll wait for some
feedback, review & decision before going further down in backpatching.Hi, thanks for these patches. I have made some edits of my own. In the
end I decided that I quite liked the new structure of the
addFkConstraint() function and friends. I added a bunch of comments and
changed names somewhat. Also, I think the benefit of the refactoring is
more obvious when all patches are squashed together, so I did that.For branch 14, I opted to make it delete the constraints on detach.
This isn't ideal but unless somebody wants to spend a lot more time on
this, it seems the best we can do. Leaving broken constraints around
seems worse. The patch for 14 doesn't apply to 13 sadly. I didn't have
time to verify why, but it seems there's some rather larger conflict in
one spot.I hope to be able to get these pushed over the weekend. That would give
us a couple of weeks before the November releases (but my availability
in those two weeks might be spotty.)I spent some time going through your test additions and ended up with
your functions in this form:-- displays constraints in schema fkpart12
CREATE OR REPLACE FUNCTION
fkpart12_constraints(OUT conrel regclass, OUT conname name,
OUT confrelid regclass, OUT conparent text)
RETURNS SETOF record AS $$
WITH RECURSIVE arrh AS (
SELECT oid, conrelid, conname, confrelid, NULL::name AS conparent
FROM pg_constraint
WHERE connamespace = 'fkpart12'::regnamespace AND
contype = 'f' AND conparentid = 0
UNION ALL
SELECT c.oid, c.conrelid, c.conname, c.confrelid,
(pg_identify_object('pg_constraint'::regclass, arrh.oid,
0)).identity
FROM pg_constraint c
JOIN arrh ON c.conparentid = arrh.oid
) SELECT conrelid::regclass, conname, confrelid::regclass, conparent
FROM arrh
ORDER BY conrelid::regclass::text, conname;
$$
LANGUAGE SQL;-- displays triggers in tables in schema fkpart12
CREATE OR REPLACE FUNCTION
fkpart12_triggers(OUT tablename regclass, OUT constr text,
OUT samefunc boolean, OUT parent text)
RETURNS SETOF record AS $$
WITH RECURSIVE arrh AS (
SELECT t.oid, t.tgrelid::regclass as tablename, tgname,
t.tgfoid::regproc as trigfn,
(pg_identify_object('pg_constraint'::regclass, c.oid,
0)).identity as constr,
NULL::bool as samefunc,
NULL::name AS parent
FROM pg_trigger t
LEFT JOIN pg_constraint c ON c.oid = t.tgconstraint
WHERE (SELECT relnamespace FROM pg_class WHERE oid = t.tgrelid) =
'fkpart12'::regnamespace
AND c.contype = 'f' AND t.tgparentid = 0
UNION ALL
SELECT t2.oid, t2.tgrelid::regclass as tablename, t2.tgname,
t2.tgfoid::regproc as trigfn,
(pg_identify_object('pg_constraint'::regclass, c2.oid,
0)).identity,
arrh.trigfn = t2.tgfoid as samefunc,
replace((pg_identify_object('pg_trigger'::regclass,
t2.tgparentid, 0)).identity,
t2.tgparentid::text, 'TGOID')
FROM pg_trigger t2
LEFT JOIN pg_constraint c2 ON c2.oid = t2.tgconstraint
JOIN arrh ON t2.tgparentid = arrh.oid
) SELECT tablename, constr, samefunc, parent
FROM arrh
ORDER BY tablename::text, constr;
$$
LANGUAGE SQL;However, in the end I think this is a very good technique to verify that
the fix works correctly, but it's excessive to include these results in
the tests forevermore. So I've left them out for now. Maybe we should
reconsider on the older branches?Hi,
I looked at the patch on master. I gave a little comment in [1]/messages/by-id/CAHewXNkuU2V7GfgFkwd265RJ99+BfJueNEZhrHSk39o3thqxNA@mail.gmail.com
I reconsider the codes. I suspect that we don't need the below if
statement anymore.
/*
* If the referenced side is partitioned (which we know because our
* parent's constraint points to a different relation than ours) then
* we must, in addition to the above, create pg_constraint rows that
* point to each partition, each with its own action triggers.
*/
if (parentConForm->conrelid != conform->conrelid)
{
...
}
The above contidion is always true according to my test.
I haven't figured out an anti-case.
Any thoughts?
[1]: /messages/by-id/CAHewXNkuU2V7GfgFkwd265RJ99+BfJueNEZhrHSk39o3thqxNA@mail.gmail.com
/messages/by-id/CAHewXNkuU2V7GfgFkwd265RJ99+BfJueNEZhrHSk39o3thqxNA@mail.gmail.com
--
Thanks,
Tender Wang
On 2024-Oct-21, Tender Wang wrote:
I suspect that we don't need the below if
statement anymore.
/*
* If the referenced side is partitioned (which we know because our
* parent's constraint points to a different relation than ours) then
* we must, in addition to the above, create pg_constraint rows that
* point to each partition, each with its own action triggers.
*/
if (parentConForm->conrelid != conform->conrelid)
{
...
}The above contidion is always true according to my test.
I haven't figured out an anti-case.
You're right, this is useless, we can remove the 'if' line. I kept the
block though, to have a place for all those local variable declarations
(otherwise the code looks messier than it needs to).
I also noticed that addFkRecurseReferenced() is uselessly taking a List
**wqueue argument but doesn't use it, so I removed it (as fallout, other
routines don't need it either, especially DetachPartitionFinalize). I
added some commentary on the creation of dependencies in
addFkConstraint().
I also include a few other cosmetic changes; just comment layout
changes.
This time I attach the patch for master only; the others have changed
identically. 14 is unchanged from before. I figured that the conflict
from 14 to 13 was trivial to resolve -- it was just because of DETACH
CONCURRENTLY, so some code moves around, but that's all that happens.
--
Álvaro Herrera Breisgau, Deutschland — https://www.EnterpriseDB.com/
Attachments:
v5-0001-Restructure-foreign-key-handling-code-for-ATTACH-.patchtext/x-diff; charset=utf-8Download
From 205f6867eb93a47ba1db3977cfce9cb37389ddc5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=81lvaro=20Herrera?= <alvherre@alvh.no-ip.org>
Date: Mon, 21 Oct 2024 23:14:12 +0200
Subject: [PATCH v5] Restructure foreign key handling code for ATTACH/DETACH
... to fix bugs when the referenced table is partitioned.
The catalog representation we chose for foreign keys connecting
partitioned tables (in commit f56f8f8da6af) is inconvenient, in the
sense that a standalone table has a different way to represent the
constraint when referencing a partitioned table, than when the same
table becomes a partition (and vice versa). Because of this, we need to
create additional catalog rows on detach (pg_constraint and pg_trigger),
and remove them on attach. We were doing some of those things, but not
all of them, leading to missing catalog rows in certain cases.
The worst problem seems to be that we are missing action triggers after
detaching a partition, which means that you could update/delete rows
from the referenced partitioned table that still had referencing rows on
that table, the server failing to throw the required errors.
!!!
Note that this means existing databases with FKs that reference
partitioned tables might have rows that break relational integrity, on
tables that were once partitions on the referencing side of the FK.
Another possible problem is that trying to reattach a table
that had been detached would fail indicating that internal triggers
cannot be found, which from the user's point of view is nonsensical.
In branches 15 and above, we fix this by creating a new helper function
addFkConstraint() which is in charge of creating a standalone
pg_constraint row, and repurposing addFkRecurseReferencing() and
addFkRecurseReferenced() so that they're only the recursive routine for
each side of the FK, and they call addFkConstraint() to create
pg_constraint at each partitioning level and add the necessary triggers.
These new routines can be used during partition creation, partition
attach and detach, and foreign key creation. This reduces redundant
code and simplifies the flow.
In branches 14 and 13, we have a much simpler fix that consists on
simply removing the constraint on detach. The reason is that those
branches are missing commit f4566345cf40, which reworked the way this
works in a way that we didn't consider back-patchable at the time.
We opted to leave branch 12 alone, because it's different from branch 13
enough that the fix doesn't apply; and because it is going in EOL mode
very soon, patching it now might be worse since there's no way to undo
the damage if it goes wrong.
Existing databases might need to be repaired.
In the future we might want to rethink the catalog representation to
avoid this problem, but for now the code seems to do what's required to
make the constraints operate correctly.
Co-authored-by: Jehan-Guillaume de Rorthais <jgdr@dalibo.com>
Co-authored-by: Tender Wang <tndrwang@gmail.com>
Co-authored-by: Alvaro Herrera <alvherre@alvh.no-ip.org>
Reported-by: Guillaume Lelarge <guillaume@lelarge.info>
Reported-by: Jehan-Guillaume de Rorthais <jgdr@dalibo.com>
Reported-by: Thomas Baehler (SBB CFF FFS) <thomas.baehler2@sbb.ch>
Discussion: https://postgr.es/m/20230420144344.40744130@karst
Discussion: https://postgr.es/m/20230705233028.2f554f73@karst
Discussion: https://postgr.es/m/GVAP278MB02787E7134FD691861635A8BC9032@GVAP278MB0278.CHEP278.PROD.OUTLOOK.COM
Discussion: https://postgr.es/m/18541-628a61bc267cd2d3@postgresql.org
---
src/backend/commands/tablecmds.c | 671 ++++++++++++++--------
src/test/regress/expected/foreign_key.out | 96 ++++
src/test/regress/sql/foreign_key.sql | 57 ++
src/tools/pgindent/typedefs.list | 1 +
4 files changed, 573 insertions(+), 252 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 1ccc80087c3..e14bc0c0548 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -349,6 +349,14 @@ typedef struct ForeignTruncateInfo
List *rels;
} ForeignTruncateInfo;
+/* Partial or complete FK creation in addFkConstraint() */
+typedef enum addFkConstraintSides
+{
+ addFkReferencedSide,
+ addFkReferencingSide,
+ addFkBothSides,
+} addFkConstraintSides;
+
/*
* Partition tables are expected to be dropped when the parent partitioned
* table gets dropped. Hence for partitioning we use AUTO dependency.
@@ -508,17 +516,27 @@ static ObjectAddress ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *
Relation rel, Constraint *fkconstraint,
bool recurse, bool recursing,
LOCKMODE lockmode);
-static ObjectAddress addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint,
- Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
- int numfks, int16 *pkattnum, int16 *fkattnum,
- Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
- int numfkdelsetcols, int16 *fkdelsetcols,
- bool old_check_ok,
- Oid parentDelTrigger, Oid parentUpdTrigger,
- bool with_period);
static void validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
int numfksetcols, const int16 *fksetcolsattnums,
List *fksetcols);
+static ObjectAddress addFkConstraint(addFkConstraintSides fkside,
+ char *constraintname,
+ Constraint *fkconstraint, Relation rel,
+ Relation pkrel, Oid indexOid,
+ Oid parentConstr,
+ int numfks, int16 *pkattnum, int16 *fkattnum,
+ Oid *pfeqoperators, Oid *ppeqoperators,
+ Oid *ffeqoperators, int numfkdelsetcols,
+ int16 *fkdelsetcols, bool is_internal,
+ bool with_period);
+static void addFkRecurseReferenced(Constraint *fkconstraint,
+ Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
+ int numfks, int16 *pkattnum, int16 *fkattnum,
+ Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
+ int numfkdelsetcols, int16 *fkdelsetcols,
+ bool old_check_ok,
+ Oid parentDelTrigger, Oid parentUpdTrigger,
+ bool with_period);
static void addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint,
Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks, int16 *pkattnum, int16 *fkattnum,
@@ -527,7 +545,6 @@ static void addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint,
bool old_check_ok, LOCKMODE lockmode,
Oid parentInsTrigger, Oid parentUpdTrigger,
bool with_period);
-
static void CloneForeignKeyConstraints(List **wqueue, Relation parentRel,
Relation partitionRel);
static void CloneFkReferenced(Relation parentRel, Relation partitionRel);
@@ -10045,26 +10062,39 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
FindFKPeriodOpers(opclasses[numpks - 1], &periodoperoid, &aggedperiodoperoid);
}
- /*
- * Create all the constraint and trigger objects, recursing to partitions
- * as necessary. First handle the referenced side.
- */
- address = addFkRecurseReferenced(wqueue, fkconstraint, rel, pkrel,
- indexOid,
- InvalidOid, /* no parent constraint */
- numfks,
- pkattnum,
- fkattnum,
- pfeqoperators,
- ppeqoperators,
- ffeqoperators,
- numfkdelsetcols,
- fkdelsetcols,
- old_check_ok,
- InvalidOid, InvalidOid,
- with_period);
+ /* First, create the constraint catalog entry itself. */
+ address = addFkConstraint(addFkBothSides,
+ fkconstraint->conname, fkconstraint, rel, pkrel,
+ indexOid,
+ InvalidOid, /* no parent constraint */
+ numfks,
+ pkattnum,
+ fkattnum,
+ pfeqoperators,
+ ppeqoperators,
+ ffeqoperators,
+ numfkdelsetcols,
+ fkdelsetcols,
+ false,
+ with_period);
- /* Now handle the referencing side. */
+ /* Next process the action triggers at the referenced side and recurse */
+ addFkRecurseReferenced(fkconstraint, rel, pkrel,
+ indexOid,
+ address.objectId,
+ numfks,
+ pkattnum,
+ fkattnum,
+ pfeqoperators,
+ ppeqoperators,
+ ffeqoperators,
+ numfkdelsetcols,
+ fkdelsetcols,
+ old_check_ok,
+ InvalidOid, InvalidOid,
+ with_period);
+
+ /* Lastly create the check triggers at the referencing side and recurse */
addFkRecurseReferencing(wqueue, fkconstraint, rel, pkrel,
indexOid,
address.objectId,
@@ -10125,47 +10155,42 @@ validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
}
/*
- * addFkRecurseReferenced
- * subroutine for ATAddForeignKeyConstraint; recurses on the referenced
- * side of the constraint
+ * addFkConstraint
+ * Install pg_constraint entries to implement a foreign key constraint.
+ * Caller must separately invoke addFkRecurseReferenced and
+ * addFkRecurseReferencing, as appropriate, to install pg_trigger entries
+ * and (for partitioned tables) recurse to partitions.
*
- * Create pg_constraint rows for the referenced side of the constraint,
- * referencing the parent of the referencing side; also create action triggers
- * on leaf partitions. If the table is partitioned, recurse to handle each
- * partition.
- *
- * wqueue is the ALTER TABLE work queue; can be NULL when not running as part
- * of an ALTER TABLE sequence.
- * fkconstraint is the constraint being added.
- * rel is the root referencing relation.
- * pkrel is the referenced relation; might be a partition, if recursing.
- * indexOid is the OID of the index (on pkrel) implementing this constraint.
- * parentConstr is the OID of a parent constraint; InvalidOid if this is a
- * top-level constraint.
- * numfks is the number of columns in the foreign key
- * pkattnum is the attnum array of referenced attributes.
- * fkattnum is the attnum array of referencing attributes.
- * numfkdelsetcols is the number of columns in the ON DELETE SET NULL/DEFAULT
+ * fkside: the side of the FK (or both) to create. Caller should
+ * call addFkRecurseReferenced if this is addFkReferencedSide,
+ * addFkRecurseReferencing if it's addFkReferencingSide, or both if it's
+ * addFkBothSides.
+ * constraintname: the base name for the constraint being added,
+ * copied to fkconstraint->conname if the latter is not set
+ * fkconstraint: the constraint being added
+ * rel: the root referencing relation
+ * pkrel: the referenced relation; might be a partition, if recursing
+ * indexOid: the OID of the index (on pkrel) implementing this constraint
+ * parentConstr: the OID of a parent constraint; InvalidOid if this is a
+ * top-level constraint
+ * numfks: the number of columns in the foreign key
+ * pkattnum: the attnum array of referenced attributes
+ * fkattnum: the attnum array of referencing attributes
+ * pf/pp/ffeqoperators: OID array of operators between columns
+ * numfkdelsetcols: the number of columns in the ON DELETE SET NULL/DEFAULT
* (...) clause
- * fkdelsetcols is the attnum array of the columns in the ON DELETE SET
+ * fkdelsetcols: the attnum array of the columns in the ON DELETE SET
* NULL/DEFAULT clause
- * pf/pp/ffeqoperators are OID array of operators between columns.
- * old_check_ok signals that this constraint replaces an existing one that
- * was already validated (thus this one doesn't need validation).
- * parentDelTrigger and parentUpdTrigger, when being recursively called on
- * a partition, are the OIDs of the parent action triggers for DELETE and
- * UPDATE respectively.
+ * with_period: true if this is a temporal FK
*/
static ObjectAddress
-addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
- Relation pkrel, Oid indexOid, Oid parentConstr,
- int numfks,
- int16 *pkattnum, int16 *fkattnum, Oid *pfeqoperators,
- Oid *ppeqoperators, Oid *ffeqoperators,
- int numfkdelsetcols, int16 *fkdelsetcols,
- bool old_check_ok,
- Oid parentDelTrigger, Oid parentUpdTrigger,
- bool with_period)
+addFkConstraint(addFkConstraintSides fkside,
+ char *constraintname, Constraint *fkconstraint,
+ Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
+ int numfks, int16 *pkattnum,
+ int16 *fkattnum, Oid *pfeqoperators, Oid *ppeqoperators,
+ Oid *ffeqoperators, int numfkdelsetcols, int16 *fkdelsetcols,
+ bool is_internal, bool with_period)
{
ObjectAddress address;
Oid constrOid;
@@ -10173,8 +10198,6 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
bool conislocal;
int16 coninhcount;
bool connoinherit;
- Oid deleteTriggerOid,
- updateTriggerOid;
/*
* Verify relkind for each referenced partition. At the top level, this
@@ -10193,13 +10216,16 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
*/
if (ConstraintNameIsUsed(CONSTRAINT_RELATION,
RelationGetRelid(rel),
- fkconstraint->conname))
+ constraintname))
conname = ChooseConstraintName(RelationGetRelationName(rel),
ChooseForeignKeyConstraintNameAddition(fkconstraint->fk_attrs),
"fkey",
RelationGetNamespace(rel), NIL);
else
- conname = fkconstraint->conname;
+ conname = constraintname;
+
+ if (fkconstraint->conname == NULL)
+ fkconstraint->conname = pstrdup(conname);
if (OidIsValid(parentConstr))
{
@@ -10252,33 +10278,106 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
coninhcount, /* inhcount */
connoinherit, /* conNoInherit */
with_period, /* conPeriod */
- false); /* is_internal */
+ is_internal); /* is_internal */
ObjectAddressSet(address, ConstraintRelationId, constrOid);
/*
- * Mark the child constraint as part of the parent constraint; it must not
- * be dropped on its own. (This constraint is deleted when the partition
- * is detached, but a special check needs to occur that the partition
- * contains no referenced values.)
+ * In partitioning cases, create the dependency entries for this
+ * constraint. (For non-partitioned cases, relevant entries were created
+ * by CreateConstraintEntry.)
+ *
+ * On the referenced side, we need the constraint to have an internal
+ * dependency on its parent constraint; this means that this constraint
+ * cannot be dropped on its own -- only through the parent constraint. It
+ * also means the containing partition cannot be dropped on its own, but
+ * it can be detached, at which point this dependency is removed (after
+ * verifying that no rows are referenced via this FK.)
+ *
+ * When processing the referencing side, we link the constraint via the
+ * special partitioning dependencies: the parent constraint is the primary
+ * dependent, and the partition on which the foreign key exists is the
+ * secondary dependency. That way, this constraint is dropped if either
+ * of these objects is.
+ *
+ * Note that this is only necessary for the subsidiary pg_constraint rows
+ * in partitions; the topmost row doesn't need any of this.
*/
if (OidIsValid(parentConstr))
{
ObjectAddress referenced;
ObjectAddressSet(referenced, ConstraintRelationId, parentConstr);
- recordDependencyOn(&address, &referenced, DEPENDENCY_INTERNAL);
+
+ Assert(fkside != addFkBothSides);
+ if (fkside == addFkReferencedSide)
+ recordDependencyOn(&address, &referenced, DEPENDENCY_INTERNAL);
+ else
+ {
+ recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_PRI);
+ ObjectAddressSet(referenced, RelationRelationId, RelationGetRelid(rel));
+ recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_SEC);
+ }
}
/* make new constraint visible, in case we add more */
CommandCounterIncrement();
+ return address;
+}
+
+/*
+ * addFkRecurseReferenced
+ * Recursive helper for the referenced side of foreign key creation,
+ * which creates the action triggers and recurses
+ *
+ * If the referenced relation is a plain relation, create the necessary action
+ * triggers that implement the constraint. If the referenced relation is a
+ * partitioned table, then we create a pg_constraint row referencing the parent
+ * of the referencing side for it and recurse on this routine for each
+ * partition.
+ *
+ * fkconstraint: the constraint being added
+ * rel: the root referencing relation
+ * pkrel: the referenced relation; might be a partition, if recursing
+ * indexOid: the OID of the index (on pkrel) implementing this constraint
+ * parentConstr: the OID of a parent constraint; InvalidOid if this is a
+ * top-level constraint
+ * numfks: the number of columns in the foreign key
+ * pkattnum: the attnum array of referenced attributes
+ * fkattnum: the attnum array of referencing attributes
+ * numfkdelsetcols: the number of columns in the ON DELETE SET
+ * NULL/DEFAULT (...) clause
+ * fkdelsetcols: the attnum array of the columns in the ON DELETE SET
+ * NULL/DEFAULT clause
+ * pf/pp/ffeqoperators: OID array of operators between columns
+ * old_check_ok: true if this constraint replaces an existing one that
+ * was already validated (thus this one doesn't need validation)
+ * parentDelTrigger and parentUpdTrigger: when recursively called on a
+ * partition, the OIDs of the parent action triggers for DELETE and
+ * UPDATE respectively.
+ * with_period: true if this is a temporal FK
+ */
+static void
+addFkRecurseReferenced(Constraint *fkconstraint, Relation rel,
+ Relation pkrel, Oid indexOid, Oid parentConstr,
+ int numfks,
+ int16 *pkattnum, int16 *fkattnum, Oid *pfeqoperators,
+ Oid *ppeqoperators, Oid *ffeqoperators,
+ int numfkdelsetcols, int16 *fkdelsetcols,
+ bool old_check_ok,
+ Oid parentDelTrigger, Oid parentUpdTrigger,
+ bool with_period)
+{
+ Oid deleteTriggerOid,
+ updateTriggerOid;
+
/*
* Create the action triggers that enforce the constraint.
*/
createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
fkconstraint,
- constrOid, indexOid,
+ parentConstr, indexOid,
parentDelTrigger, parentUpdTrigger,
&deleteTriggerOid, &updateTriggerOid);
@@ -10297,6 +10396,7 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
AttrMap *map;
AttrNumber *mapped_pkattnum;
Oid partIndexId;
+ ObjectAddress address;
partRel = table_open(pd->oids[i], ShareRowExclusiveLock);
@@ -10316,13 +10416,23 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
else
mapped_pkattnum = pkattnum;
- /* do the deed */
+ /* Determine the index to use at this level */
partIndexId = index_get_partition(partRel, indexOid);
if (!OidIsValid(partIndexId))
elog(ERROR, "index for %u not found in partition %s",
indexOid, RelationGetRelationName(partRel));
- addFkRecurseReferenced(wqueue, fkconstraint, rel, partRel,
- partIndexId, constrOid, numfks,
+
+ /* Create entry at this level ... */
+ address = addFkConstraint(addFkReferencedSide,
+ fkconstraint->conname, fkconstraint, rel,
+ partRel, partIndexId, parentConstr,
+ numfks, mapped_pkattnum,
+ fkattnum, pfeqoperators, ppeqoperators,
+ ffeqoperators, numfkdelsetcols,
+ fkdelsetcols, true, with_period);
+ /* ... and recurse to our children */
+ addFkRecurseReferenced(fkconstraint, rel, partRel,
+ partIndexId, address.objectId, numfks,
mapped_pkattnum, fkattnum,
pfeqoperators, ppeqoperators, ffeqoperators,
numfkdelsetcols, fkdelsetcols,
@@ -10339,13 +10449,12 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
}
}
}
-
- return address;
}
/*
* addFkRecurseReferencing
- * subroutine for ATAddForeignKeyConstraint and CloneFkReferencing
+ * Recursive helper for the referencing side of foreign key creation,
+ * which creates the check triggers and recurses
*
* If the referencing relation is a plain relation, create the necessary check
* triggers that implement the constraint, and set up for Phase 3 constraint
@@ -10357,27 +10466,28 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
* deletions. If it's a partitioned relation, every partition must be so
* locked.
*
- * wqueue is the ALTER TABLE work queue; can be NULL when not running as part
- * of an ALTER TABLE sequence.
- * fkconstraint is the constraint being added.
- * rel is the referencing relation; might be a partition, if recursing.
- * pkrel is the root referenced relation.
- * indexOid is the OID of the index (on pkrel) implementing this constraint.
- * parentConstr is the OID of the parent constraint (there is always one).
- * numfks is the number of columns in the foreign key
- * pkattnum is the attnum array of referenced attributes.
- * fkattnum is the attnum array of referencing attributes.
- * pf/pp/ffeqoperators are OID array of operators between columns.
- * numfkdelsetcols is the number of columns in the ON DELETE SET NULL/DEFAULT
+ * wqueue: the ALTER TABLE work queue; NULL when not running as part
+ * of an ALTER TABLE sequence.
+ * fkconstraint: the constraint being added
+ * rel: the referencing relation; might be a partition, if recursing
+ * pkrel: the root referenced relation
+ * indexOid: the OID of the index (on pkrel) implementing this constraint
+ * parentConstr: the OID of the parent constraint (there is always one)
+ * numfks: the number of columns in the foreign key
+ * pkattnum: the attnum array of referenced attributes
+ * fkattnum: the attnum array of referencing attributes
+ * pf/pp/ffeqoperators: OID array of operators between columns
+ * numfkdelsetcols: the number of columns in the ON DELETE SET NULL/DEFAULT
* (...) clause
- * fkdelsetcols is the attnum array of the columns in the ON DELETE SET
+ * fkdelsetcols: the attnum array of the columns in the ON DELETE SET
* NULL/DEFAULT clause
- * old_check_ok signals that this constraint replaces an existing one that
- * was already validated (thus this one doesn't need validation).
- * lockmode is the lockmode to acquire on partitions when recursing.
- * parentInsTrigger and parentUpdTrigger, when being recursively called on
- * a partition, are the OIDs of the parent check triggers for INSERT and
- * UPDATE respectively.
+ * old_check_ok: true if this constraint replaces an existing one that
+ * was already validated (thus this one doesn't need validation)
+ * lockmode: the lockmode to acquire on partitions when recursing
+ * parentInsTrigger and parentUpdTrigger: when being recursively called on
+ * a partition, the OIDs of the parent check triggers for INSERT and
+ * UPDATE respectively.
+ * with_period: true if this is a temporal FK
*/
static void
addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
@@ -10467,10 +10577,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
AttrMap *attmap;
AttrNumber mapped_fkattnum[INDEX_MAX_KEYS];
bool attached;
- char *conname;
- Oid constrOid;
- ObjectAddress address,
- referenced;
+ ObjectAddress address;
ListCell *cell;
CheckAlterTableIsSafe(partition);
@@ -10513,66 +10620,19 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
/*
* No luck finding a good constraint to reuse; create our own.
*/
- if (ConstraintNameIsUsed(CONSTRAINT_RELATION,
- RelationGetRelid(partition),
- fkconstraint->conname))
- conname = ChooseConstraintName(RelationGetRelationName(partition),
- ChooseForeignKeyConstraintNameAddition(fkconstraint->fk_attrs),
- "fkey",
- RelationGetNamespace(partition), NIL);
- else
- conname = fkconstraint->conname;
- constrOid =
- CreateConstraintEntry(conname,
- RelationGetNamespace(partition),
- CONSTRAINT_FOREIGN,
- fkconstraint->deferrable,
- fkconstraint->initdeferred,
- fkconstraint->initially_valid,
- parentConstr,
- partitionId,
- mapped_fkattnum,
- numfks,
- numfks,
- InvalidOid,
- indexOid,
- RelationGetRelid(pkrel),
- pkattnum,
- pfeqoperators,
- ppeqoperators,
- ffeqoperators,
- numfks,
- fkconstraint->fk_upd_action,
- fkconstraint->fk_del_action,
- fkdelsetcols,
- numfkdelsetcols,
- fkconstraint->fk_matchtype,
- NULL,
- NULL,
- NULL,
- false, /* conIsLocal */
- 1, /* conInhCount */
- false, /* conNoInherit */
- with_period, /* conPeriod */
- false);
-
- /*
- * Give this constraint partition-type dependencies on the parent
- * constraint as well as the table.
- */
- ObjectAddressSet(address, ConstraintRelationId, constrOid);
- ObjectAddressSet(referenced, ConstraintRelationId, parentConstr);
- recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_PRI);
- ObjectAddressSet(referenced, RelationRelationId, partitionId);
- recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_SEC);
-
- /* Make all this visible before recursing */
- CommandCounterIncrement();
+ address = addFkConstraint(addFkReferencingSide,
+ fkconstraint->conname, fkconstraint,
+ partition, pkrel, indexOid, parentConstr,
+ numfks, pkattnum,
+ mapped_fkattnum, pfeqoperators,
+ ppeqoperators, ffeqoperators,
+ numfkdelsetcols, fkdelsetcols, true,
+ with_period);
/* call ourselves to finalize the creation and we're done */
addFkRecurseReferencing(wqueue, fkconstraint, partition, pkrel,
indexOid,
- constrOid,
+ address.objectId,
numfks,
pkattnum,
mapped_fkattnum,
@@ -10706,6 +10766,7 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
int numfkdelsetcols;
AttrNumber confdelsetcols[INDEX_MAX_KEYS];
Constraint *fkconstraint;
+ ObjectAddress address;
Oid deleteTriggerOid,
updateTriggerOid;
@@ -10739,7 +10800,7 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
* Because we're only expanding the key space at the referenced side,
* we don't need to prevent any operation in the referencing table, so
* AccessShareLock suffices (assumes that dropping the constraint
- * acquires AEL).
+ * acquires AccessExclusiveLock).
*/
fkRel = table_open(constrForm->conrelid, AccessShareLock);
@@ -10805,12 +10866,20 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
constrForm->confrelid, constrForm->conrelid,
&deleteTriggerOid, &updateTriggerOid);
- addFkRecurseReferenced(NULL,
- fkconstraint,
+ /* Add this constraint ... */
+ address = addFkConstraint(addFkReferencedSide,
+ fkconstraint->conname, fkconstraint, fkRel,
+ partitionRel, partIndexId, constrOid,
+ numfks, mapped_confkey,
+ conkey, conpfeqop, conppeqop, conffeqop,
+ numfkdelsetcols, confdelsetcols, false,
+ constrForm->conperiod);
+ /* ... and recurse */
+ addFkRecurseReferenced(fkconstraint,
fkRel,
partitionRel,
partIndexId,
- constrOid,
+ address.objectId,
numfks,
mapped_confkey,
conkey,
@@ -10841,8 +10910,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
* child.
*
* If wqueue is given, it is used to set up phase-3 verification for each
- * cloned constraint; if omitted, we assume that such verification is not
- * needed (example: the partition is being created anew).
+ * cloned constraint; omit it if such verification is not needed
+ * (example: the partition is being created anew).
*/
static void
CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
@@ -10926,9 +10995,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
Constraint *fkconstraint;
bool attached;
Oid indexOid;
- Oid constrOid;
- ObjectAddress address,
- referenced;
+ ObjectAddress address;
ListCell *lc;
Oid insertTriggerOid,
updateTriggerOid;
@@ -11026,7 +11093,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
fkconstraint->old_conpfeqop = NIL;
fkconstraint->old_pktable_oid = InvalidOid;
fkconstraint->skip_validation = false;
- fkconstraint->initially_valid = true;
+ fkconstraint->initially_valid = constrForm->convalidated;
for (int i = 0; i < numfks; i++)
{
Form_pg_attribute att;
@@ -11036,73 +11103,30 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
fkconstraint->fk_attrs = lappend(fkconstraint->fk_attrs,
makeString(NameStr(att->attname)));
}
- if (ConstraintNameIsUsed(CONSTRAINT_RELATION,
- RelationGetRelid(partRel),
- NameStr(constrForm->conname)))
- fkconstraint->conname =
- ChooseConstraintName(RelationGetRelationName(partRel),
- ChooseForeignKeyConstraintNameAddition(fkconstraint->fk_attrs),
- "fkey",
- RelationGetNamespace(partRel), NIL);
- else
- fkconstraint->conname = pstrdup(NameStr(constrForm->conname));
indexOid = constrForm->conindid;
with_period = constrForm->conperiod;
- constrOid =
- CreateConstraintEntry(fkconstraint->conname,
- constrForm->connamespace,
- CONSTRAINT_FOREIGN,
- fkconstraint->deferrable,
- fkconstraint->initdeferred,
- constrForm->convalidated,
- parentConstrOid,
- RelationGetRelid(partRel),
- mapped_conkey,
- numfks,
- numfks,
- InvalidOid, /* not a domain constraint */
- indexOid,
- constrForm->confrelid, /* same foreign rel */
- confkey,
- conpfeqop,
- conppeqop,
- conffeqop,
- numfks,
- fkconstraint->fk_upd_action,
- fkconstraint->fk_del_action,
- confdelsetcols,
- numfkdelsetcols,
- fkconstraint->fk_matchtype,
- NULL,
- NULL,
- NULL,
- false, /* conIsLocal */
- 1, /* conInhCount */
- false, /* conNoInherit */
- with_period, /* conPeriod */
- true);
- /* Set up partition dependencies for the new constraint */
- ObjectAddressSet(address, ConstraintRelationId, constrOid);
- ObjectAddressSet(referenced, ConstraintRelationId, parentConstrOid);
- recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_PRI);
- ObjectAddressSet(referenced, RelationRelationId,
- RelationGetRelid(partRel));
- recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_SEC);
+ /* Create the pg_constraint entry at this level */
+ address = addFkConstraint(addFkReferencingSide,
+ NameStr(constrForm->conname), fkconstraint,
+ partRel, pkrel, indexOid, parentConstrOid,
+ numfks, confkey,
+ mapped_conkey, conpfeqop,
+ conppeqop, conffeqop,
+ numfkdelsetcols, confdelsetcols,
+ false, with_period);
/* Done with the cloned constraint's tuple */
ReleaseSysCache(tuple);
- /* Make all this visible before recursing */
- CommandCounterIncrement();
-
+ /* Create the check triggers, and recurse to partitions, if any */
addFkRecurseReferencing(wqueue,
fkconstraint,
partRel,
pkrel,
indexOid,
- constrOid,
+ address.objectId,
numfks,
confkey,
mapped_conkey,
@@ -11267,6 +11291,81 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
partRelid);
+ /*
+ * If the referenced table is partitioned, then the partition we're
+ * attaching now has extra pg_constraint rows and action triggers that are
+ * no longer needed. Remove those.
+ */
+ if (get_rel_relkind(fk->confrelid) == RELKIND_PARTITIONED_TABLE)
+ {
+ Relation pg_constraint = table_open(ConstraintRelationId, RowShareLock);
+ ObjectAddresses *objs;
+ HeapTuple consttup;
+
+ ScanKeyInit(&key,
+ Anum_pg_constraint_conrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(fk->conrelid));
+
+ scan = systable_beginscan(pg_constraint,
+ ConstraintRelidTypidNameIndexId,
+ true, NULL, 1, &key);
+ objs = new_object_addresses();
+ while ((consttup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_constraint conform = (Form_pg_constraint) GETSTRUCT(consttup);
+
+ if (conform->conparentid != fk->conoid)
+ continue;
+ else
+ {
+ ObjectAddress addr;
+ SysScanDesc scan2;
+ ScanKeyData key2;
+ int n PG_USED_FOR_ASSERTS_ONLY;
+
+ ObjectAddressSet(addr, ConstraintRelationId, conform->oid);
+ add_exact_object_address(&addr, objs);
+
+ /*
+ * First we must delete the dependency record that binds the
+ * constraint records together.
+ */
+ n = deleteDependencyRecordsForSpecific(ConstraintRelationId,
+ conform->oid,
+ DEPENDENCY_INTERNAL,
+ ConstraintRelationId,
+ fk->conoid);
+ Assert(n == 1); /* actually only one is expected */
+
+ /*
+ * Now search for the triggers for this constraint and set
+ * them up for deletion too
+ */
+ ScanKeyInit(&key2,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conform->oid));
+ scan2 = systable_beginscan(trigrel, TriggerConstraintIndexId,
+ true, NULL, 1, &key2);
+ while ((trigtup = systable_getnext(scan2)) != NULL)
+ {
+ ObjectAddressSet(addr, TriggerRelationId,
+ ((Form_pg_trigger) GETSTRUCT(trigtup))->oid);
+ add_exact_object_address(&addr, objs);
+ }
+ systable_endscan(scan2);
+ }
+ }
+ /* make the dependency deletions visible */
+ CommandCounterIncrement();
+ performMultipleDeletions(objs, DROP_RESTRICT,
+ PERFORM_DELETION_INTERNAL);
+ systable_endscan(scan);
+
+ table_close(pg_constraint, RowShareLock);
+ }
+
CommandCounterIncrement();
return true;
}
@@ -19337,9 +19436,9 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
foreach(cell, fks)
{
ForeignKeyCacheInfo *fk = lfirst(cell);
- HeapTuple contup;
+ HeapTuple contup,
+ parentConTup;
Form_pg_constraint conform;
- Constraint *fkconstraint;
Oid insertTriggerOid,
updateTriggerOid;
@@ -19356,7 +19455,17 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
continue;
}
- /* unset conparentid and adjust conislocal, coninhcount, etc. */
+ Assert(OidIsValid(conform->conparentid));
+ parentConTup = SearchSysCache1(CONSTROID,
+ ObjectIdGetDatum(conform->conparentid));
+ if (!HeapTupleIsValid(parentConTup))
+ elog(ERROR, "cache lookup failed for constraint %u",
+ conform->conparentid);
+
+ /*
+ * The constraint on this table must be marked no longer a child of
+ * the parent's constraint, as do its check triggers.
+ */
ConstraintSetParentConstraint(fk->conoid, InvalidOid, InvalidOid);
/*
@@ -19374,35 +19483,93 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
RelationGetRelid(partRel));
/*
- * Make the action triggers on the referenced relation. When this was
- * a partition the action triggers pointed to the parent rel (they
- * still do), but now we need separate ones of our own.
+ * Lastly, create the action triggers on the referenced table, using
+ * addFkRecurseReferenced, which requires some elaborate setup (so put
+ * it in a separate block). While at it, if the table is partitioned,
+ * that function will recurse to create the pg_constraint rows and
+ * action triggers for each partition.
+ *
+ * Note there's no need to do addFkConstraint() here, because the
+ * pg_constraint row already exists.
*/
- fkconstraint = makeNode(Constraint);
- fkconstraint->contype = CONSTRAINT_FOREIGN;
- fkconstraint->conname = pstrdup(NameStr(conform->conname));
- fkconstraint->deferrable = conform->condeferrable;
- fkconstraint->initdeferred = conform->condeferred;
- fkconstraint->location = -1;
- fkconstraint->pktable = NULL;
- fkconstraint->fk_attrs = NIL;
- fkconstraint->pk_attrs = NIL;
- fkconstraint->fk_matchtype = conform->confmatchtype;
- fkconstraint->fk_upd_action = conform->confupdtype;
- fkconstraint->fk_del_action = conform->confdeltype;
- fkconstraint->fk_del_set_cols = NIL;
- fkconstraint->old_conpfeqop = NIL;
- fkconstraint->old_pktable_oid = InvalidOid;
- fkconstraint->skip_validation = false;
- fkconstraint->initially_valid = true;
+ {
+ Constraint *fkconstraint;
+ int numfks;
+ AttrNumber conkey[INDEX_MAX_KEYS];
+ AttrNumber confkey[INDEX_MAX_KEYS];
+ Oid conpfeqop[INDEX_MAX_KEYS];
+ Oid conppeqop[INDEX_MAX_KEYS];
+ Oid conffeqop[INDEX_MAX_KEYS];
+ int numfkdelsetcols;
+ AttrNumber confdelsetcols[INDEX_MAX_KEYS];
+ AttrMap *attmap;
+ Relation refdRel;
- createForeignKeyActionTriggers(partRel, conform->confrelid,
- fkconstraint, fk->conoid,
- conform->conindid,
- InvalidOid, InvalidOid,
- NULL, NULL);
+ DeconstructFkConstraintRow(contup,
+ &numfks,
+ conkey,
+ confkey,
+ conpfeqop,
+ conppeqop,
+ conffeqop,
+ &numfkdelsetcols,
+ confdelsetcols);
+
+ /* Create a synthetic node we'll use throughout */
+ fkconstraint = makeNode(Constraint);
+ fkconstraint->contype = CONSTRAINT_FOREIGN;
+ fkconstraint->conname = pstrdup(NameStr(conform->conname));
+ fkconstraint->deferrable = conform->condeferrable;
+ fkconstraint->initdeferred = conform->condeferred;
+ fkconstraint->skip_validation = true;
+ fkconstraint->initially_valid = true;
+ /* a few irrelevant fields omitted here */
+ fkconstraint->pktable = NULL;
+ fkconstraint->fk_attrs = NIL;
+ fkconstraint->pk_attrs = NIL;
+ fkconstraint->fk_matchtype = conform->confmatchtype;
+ fkconstraint->fk_upd_action = conform->confupdtype;
+ fkconstraint->fk_del_action = conform->confdeltype;
+ fkconstraint->fk_del_set_cols = NIL;
+ fkconstraint->old_conpfeqop = NIL;
+ fkconstraint->old_pktable_oid = InvalidOid;
+ fkconstraint->location = -1;
+
+ attmap = build_attrmap_by_name(RelationGetDescr(partRel),
+ RelationGetDescr(rel),
+ false);
+ for (int i = 0; i < numfks; i++)
+ {
+ Form_pg_attribute att;
+
+ att = TupleDescAttr(RelationGetDescr(partRel),
+ attmap->attnums[conkey[i] - 1] - 1);
+ fkconstraint->fk_attrs = lappend(fkconstraint->fk_attrs,
+ makeString(NameStr(att->attname)));
+ }
+
+ refdRel = table_open(fk->confrelid, AccessShareLock);
+
+ addFkRecurseReferenced(fkconstraint, partRel,
+ refdRel,
+ conform->conindid,
+ fk->conoid,
+ numfks,
+ confkey,
+ conkey,
+ conpfeqop,
+ conppeqop,
+ conffeqop,
+ numfkdelsetcols,
+ confdelsetcols,
+ true,
+ InvalidOid, InvalidOid,
+ conform->conperiod);
+ table_close(refdRel, AccessShareLock);
+ }
ReleaseSysCache(contup);
+ ReleaseSysCache(parentConTup);
}
list_free_deep(fks);
if (trigrel)
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 8c04a24b37d..b73e7dced8f 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -2934,3 +2934,99 @@ DETAIL: drop cascades to table fkpart11.pk
drop cascades to table fkpart11.fk_parted
drop cascades to table fkpart11.fk_another
drop cascades to function fkpart11.print_row()
+-- When a table is attached as partition to a partitioned table that has
+-- a foreign key to another partitioned table, it acquires a clone of the
+-- FK. Upon detach, this clone is not removed, but instead becomes an
+-- independent FK. If it then attaches to the partitioned table again,
+-- the FK from the parent "takes over" ownership of the independent FK rather
+-- than creating a separate one.
+CREATE SCHEMA fkpart12
+ CREATE TABLE fk_p ( id int, jd int, PRIMARY KEY(id, jd)) PARTITION BY list (id)
+ CREATE TABLE fk_p_1 PARTITION OF fk_p FOR VALUES IN (1) PARTITION BY list (jd)
+ CREATE TABLE fk_p_1_1 PARTITION OF fk_p_1 FOR VALUES IN (1)
+ CREATE TABLE fk_p_1_2 PARTITION OF fk_p_1 FOR VALUES IN (2)
+ CREATE TABLE fk_p_2 PARTITION OF fk_p FOR VALUES IN (2) PARTITION BY list (jd)
+ CREATE TABLE fk_p_2_1 PARTITION OF fk_p_2 FOR VALUES IN (1)
+ CREATE TABLE fk_p_2_2 PARTITION OF fk_p_2 FOR VALUES IN (2)
+ CREATE TABLE fk_r_1 ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL)
+ CREATE TABLE fk_r_2 ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL) PARTITION BY list (id)
+ CREATE TABLE fk_r_2_1 PARTITION OF fk_r_2 FOR VALUES IN (2, 1)
+ CREATE TABLE fk_r ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL,
+ FOREIGN KEY (p_id, p_jd) REFERENCES fk_p (id, jd)
+ ) PARTITION BY list (id);
+SET search_path TO fkpart12;
+INSERT INTO fk_p VALUES (1, 1);
+ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1);
+ALTER TABLE fk_r ATTACH PARTITION fk_r_2 FOR VALUES IN (2);
+\d fk_r_2
+ Partitioned table "fkpart12.fk_r_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ id | integer | | not null |
+ p_id | integer | | not null |
+ p_jd | integer | | not null |
+Partition of: fk_r FOR VALUES IN (2)
+Partition key: LIST (id)
+Indexes:
+ "fk_r_2_pkey" PRIMARY KEY, btree (id)
+Foreign-key constraints:
+ TABLE "fk_r" CONSTRAINT "fk_r_p_id_p_jd_fkey" FOREIGN KEY (p_id, p_jd) REFERENCES fk_p(id, jd)
+Number of partitions: 1 (Use \d+ to list them.)
+
+INSERT INTO fk_r VALUES (1, 1, 1);
+INSERT INTO fk_r VALUES (2, 2, 1);
+ERROR: insert or update on table "fk_r_2_1" violates foreign key constraint "fk_r_p_id_p_jd_fkey"
+DETAIL: Key (p_id, p_jd)=(2, 1) is not present in table "fk_p".
+ALTER TABLE fk_r DETACH PARTITION fk_r_1;
+ALTER TABLE fk_r DETACH PARTITION fk_r_2;
+\d fk_r_2
+ Partitioned table "fkpart12.fk_r_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ id | integer | | not null |
+ p_id | integer | | not null |
+ p_jd | integer | | not null |
+Partition key: LIST (id)
+Indexes:
+ "fk_r_2_pkey" PRIMARY KEY, btree (id)
+Foreign-key constraints:
+ "fk_r_p_id_p_jd_fkey" FOREIGN KEY (p_id, p_jd) REFERENCES fk_p(id, jd)
+Number of partitions: 1 (Use \d+ to list them.)
+
+INSERT INTO fk_r_1 VALUES (2, 1, 2); -- should fail
+ERROR: insert or update on table "fk_r_1" violates foreign key constraint "fk_r_p_id_p_jd_fkey"
+DETAIL: Key (p_id, p_jd)=(1, 2) is not present in table "fk_p".
+DELETE FROM fk_p; -- should fail
+ERROR: update or delete on table "fk_p_1_1" violates foreign key constraint "fk_r_1_p_id_p_jd_fkey1" on table "fk_r_1"
+DETAIL: Key (id, jd)=(1, 1) is still referenced from table "fk_r_1".
+ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1);
+ALTER TABLE fk_r ATTACH PARTITION fk_r_2 FOR VALUES IN (2);
+\d fk_r_2
+ Partitioned table "fkpart12.fk_r_2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ id | integer | | not null |
+ p_id | integer | | not null |
+ p_jd | integer | | not null |
+Partition of: fk_r FOR VALUES IN (2)
+Partition key: LIST (id)
+Indexes:
+ "fk_r_2_pkey" PRIMARY KEY, btree (id)
+Foreign-key constraints:
+ TABLE "fk_r" CONSTRAINT "fk_r_p_id_p_jd_fkey" FOREIGN KEY (p_id, p_jd) REFERENCES fk_p(id, jd)
+Number of partitions: 1 (Use \d+ to list them.)
+
+DELETE FROM fk_p; -- should fail
+ERROR: update or delete on table "fk_p_1_1" violates foreign key constraint "fk_r_p_id_p_jd_fkey2" on table "fk_r"
+DETAIL: Key (id, jd)=(1, 1) is still referenced from table "fk_r".
+-- these should all fail
+ALTER TABLE fk_r_1 DROP CONSTRAINT fk_r_p_id_p_jd_fkey;
+ERROR: cannot drop inherited constraint "fk_r_p_id_p_jd_fkey" of relation "fk_r_1"
+ALTER TABLE fk_r DROP CONSTRAINT fk_r_p_id_p_jd_fkey1;
+ERROR: cannot drop inherited constraint "fk_r_p_id_p_jd_fkey1" of relation "fk_r"
+ALTER TABLE fk_r_2 DROP CONSTRAINT fk_r_p_id_p_jd_fkey;
+ERROR: cannot drop inherited constraint "fk_r_p_id_p_jd_fkey" of relation "fk_r_2"
+SET client_min_messages TO warning;
+DROP SCHEMA fkpart12 CASCADE;
+RESET client_min_messages;
+RESET search_path;
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index d1aac5357f0..9b2a6b6bff7 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -2086,3 +2086,60 @@ UPDATE fkpart11.pk SET a = 3 WHERE a = 4;
UPDATE fkpart11.pk SET a = 1 WHERE a = 2;
DROP SCHEMA fkpart11 CASCADE;
+
+-- When a table is attached as partition to a partitioned table that has
+-- a foreign key to another partitioned table, it acquires a clone of the
+-- FK. Upon detach, this clone is not removed, but instead becomes an
+-- independent FK. If it then attaches to the partitioned table again,
+-- the FK from the parent "takes over" ownership of the independent FK rather
+-- than creating a separate one.
+CREATE SCHEMA fkpart12
+ CREATE TABLE fk_p ( id int, jd int, PRIMARY KEY(id, jd)) PARTITION BY list (id)
+ CREATE TABLE fk_p_1 PARTITION OF fk_p FOR VALUES IN (1) PARTITION BY list (jd)
+ CREATE TABLE fk_p_1_1 PARTITION OF fk_p_1 FOR VALUES IN (1)
+ CREATE TABLE fk_p_1_2 PARTITION OF fk_p_1 FOR VALUES IN (2)
+ CREATE TABLE fk_p_2 PARTITION OF fk_p FOR VALUES IN (2) PARTITION BY list (jd)
+ CREATE TABLE fk_p_2_1 PARTITION OF fk_p_2 FOR VALUES IN (1)
+ CREATE TABLE fk_p_2_2 PARTITION OF fk_p_2 FOR VALUES IN (2)
+ CREATE TABLE fk_r_1 ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL)
+ CREATE TABLE fk_r_2 ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL) PARTITION BY list (id)
+ CREATE TABLE fk_r_2_1 PARTITION OF fk_r_2 FOR VALUES IN (2, 1)
+ CREATE TABLE fk_r ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL,
+ FOREIGN KEY (p_id, p_jd) REFERENCES fk_p (id, jd)
+ ) PARTITION BY list (id);
+SET search_path TO fkpart12;
+
+INSERT INTO fk_p VALUES (1, 1);
+
+ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1);
+ALTER TABLE fk_r ATTACH PARTITION fk_r_2 FOR VALUES IN (2);
+
+\d fk_r_2
+
+INSERT INTO fk_r VALUES (1, 1, 1);
+INSERT INTO fk_r VALUES (2, 2, 1);
+
+ALTER TABLE fk_r DETACH PARTITION fk_r_1;
+ALTER TABLE fk_r DETACH PARTITION fk_r_2;
+
+\d fk_r_2
+
+INSERT INTO fk_r_1 VALUES (2, 1, 2); -- should fail
+DELETE FROM fk_p; -- should fail
+
+ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1);
+ALTER TABLE fk_r ATTACH PARTITION fk_r_2 FOR VALUES IN (2);
+
+\d fk_r_2
+
+DELETE FROM fk_p; -- should fail
+
+-- these should all fail
+ALTER TABLE fk_r_1 DROP CONSTRAINT fk_r_p_id_p_jd_fkey;
+ALTER TABLE fk_r DROP CONSTRAINT fk_r_p_id_p_jd_fkey1;
+ALTER TABLE fk_r_2 DROP CONSTRAINT fk_r_p_id_p_jd_fkey;
+
+SET client_min_messages TO warning;
+DROP SCHEMA fkpart12 CASCADE;
+RESET client_min_messages;
+RESET search_path;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 57de1acff3a..49a0d58bd98 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -3271,6 +3271,7 @@ _stringlist
access_vector_t
acquireLocksOnSubLinks_context
add_nulling_relids_context
+addFkConstraintSides
adjust_appendrel_attrs_context
allocfunc
amadjustmembers_function
--
2.39.5
Alvaro Herrera <alvherre@alvh.no-ip.org> 于2024年10月22日周二 05:52写道:
On 2024-Oct-21, Tender Wang wrote:
I suspect that we don't need the below if
statement anymore.
/*
* If the referenced side is partitioned (which we know because our
* parent's constraint points to a different relation than ours) then
* we must, in addition to the above, create pg_constraint rows that
* point to each partition, each with its own action triggers.
*/
if (parentConForm->conrelid != conform->conrelid)
{
...
}The above contidion is always true according to my test.
I haven't figured out an anti-case.You're right, this is useless, we can remove the 'if' line. I kept the
block though, to have a place for all those local variable declarations
(otherwise the code looks messier than it needs to).
Agree.
I also noticed that addFkRecurseReferenced() is uselessly taking a List
**wqueue argument but doesn't use it, so I removed it (as fallout, other
routines don't need it either, especially DetachPartitionFinalize). I
added some commentary on the creation of dependencies in
addFkConstraint().
Yeah, I also noticed this before.
I also include a few other cosmetic changes; just comment layout
changes.This time I attach the patch for master only; the others have changed
identically. 14 is unchanged from before. I figured that the conflict
from 14 to 13 was trivial to resolve -- it was just because of DETACH
CONCURRENTLY, so some code moves around, but that's all that happens.
I don't find any other problems with the newest patch.
--
Thanks,
Tender Wang
On Fri, 18 Oct 2024 16:50:59 +0200
Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
On 2024-Sep-26, Jehan-Guillaume de Rorthais wrote:
REL_14_STABLE backport doesn't seem trivial, so I'll wait for some
feedback, review & decision before going further down in backpatching.Hi, thanks for these patches. I have made some edits of my own. In the
end I decided that I quite liked the new structure of the
addFkConstraint() function and friends.
Yes, I especially like the fact that addFkRecurseReferencing() and
addFkRecurseReferenced() have now the same logic/behavior.
I added a bunch of comments and changed names somewhat.
checked.
Also, I think the benefit of the refactoring is
more obvious when all patches are squashed together, so I did that.
OK.
For branch 14, I opted to make it delete the constraints on detach.
This isn't ideal but unless somebody wants to spend a lot more time on
this, it seems the best we can do. Leaving broken constraints around
seems worse.
Keep that in mind, and move ahead to the next level: self-FK on partitioned
table! ;-)
/messages/by-id/20230707175859.17c91538@karst
[…]
I spent some time going through your test additions and ended up with
your functions in this form:
[…]However, in the end I think this is a very good technique to verify that
the fix works correctly, but it's excessive to include these results in
the tests forevermore. So I've left them out for now. Maybe we should
reconsider on the older branches?
The point here was to make sure futur work/refactoring don't forget/break
anything in the catalog representation of FK on partitions.
Regards,
On 2024-Oct-22, Jehan-Guillaume de Rorthais wrote:
On Fri, 18 Oct 2024 16:50:59 +0200
Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
For branch 14, I opted to make it delete the constraints on detach.
This isn't ideal but unless somebody wants to spend a lot more time on
this, it seems the best we can do. Leaving broken constraints around
seems worse.Keep that in mind, and move ahead to the next level: self-FK on partitioned
table! ;-)
Yeah. I pushed these patches finally, thanks!
Now we can discuss what to do about self-referencing FKs ... maybe we
should consider dropping the FK on detach in problematic cases.
However, in the end I think this is a very good technique to verify that
the fix works correctly, but it's excessive to include these results in
the tests forevermore. So I've left them out for now. Maybe we should
reconsider on the older branches?The point here was to make sure futur work/refactoring don't forget/break
anything in the catalog representation of FK on partitions.
There is that ... but I think testing for user-visible symptoms is
better. I'm not sure I want to bet against the odds that this will
become make-work to keep the output of internal catalog state up to date
for whatever other changes we need to do.
--
Álvaro Herrera Breisgau, Deutschland — https://www.EnterpriseDB.com/
Are you not unsure you want to delete Firefox?
[Not unsure] [Not not unsure] [Cancel]
http://smylers.hates-software.com/2008/01/03/566e45b2.html
On Mon, 21 Oct 2024 23:52:18 +0200
Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
On 2024-Oct-21, Tender Wang wrote:
I suspect that we don't need the below if
statement anymore.
/*
* If the referenced side is partitioned (which we know because our
* parent's constraint points to a different relation than ours) then
* we must, in addition to the above, create pg_constraint rows that
* point to each partition, each with its own action triggers.
*/
if (parentConForm->conrelid != conform->conrelid)
{
...
}The above contidion is always true according to my test.
I haven't figured out an anti-case.You're right, this is useless, we can remove the 'if' line. I kept the
block though, to have a place for all those local variable declarations
(otherwise the code looks messier than it needs to).
I'm confused the original patch was considering the trigger creation
on the referenced side ONLY when it was partitioned, which was the point of
this conditional block (as the comment said), no matter if the test was always
true or not.
Maybe this was a leftover of some refactoring…
I also noticed that addFkRecurseReferenced() is uselessly taking a List
**wqueue argument but doesn't use it, so I removed it (as fallout, other
routines don't need it either, especially DetachPartitionFinalize). I
added some commentary on the creation of dependencies in
addFkConstraint().
Good catch.
Looking at this, on a side note, I realize now addFkRecurseReferenced() and
addFkRecurseReferencing() are not exactly mirroring each other as the later
is doing more than the former.
Either this is fine, or maybe this is the sign something might need some
refactoring as addFkRecurseReferencing() carry more than it should. I'm not sure
it deserve more than a comment for futur work/study, if only this is justified.
Regards,
On Tue, 22 Oct 2024 16:32:33 +0200
Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
On 2024-Oct-22, Jehan-Guillaume de Rorthais wrote:
On Fri, 18 Oct 2024 16:50:59 +0200
Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:For branch 14, I opted to make it delete the constraints on detach.
This isn't ideal but unless somebody wants to spend a lot more time on
this, it seems the best we can do. Leaving broken constraints around
seems worse.Keep that in mind, and move ahead to the next level: self-FK on partitioned
table! ;-)Yeah. I pushed these patches finally, thanks!
Great! Many thanks Alvaro and Tender for your work on this issue.
Hello Alvaro,
22.10.2024 17:32, Alvaro Herrera wrote:
Yeah. I pushed these patches finally, thanks!
Please look at a new anomaly introduced with 53af9491a. When running the
following script:
CREATE TABLE t (a int, b int, PRIMARY KEY (a, b));
CREATE TABLE pt (a int, b int, FOREIGN KEY (a, b) REFERENCES t(a, b))
PARTITION BY LIST (a);
CREATE TABLE tp1 (x int, a int, b int);
ALTER TABLE tp1 DROP COLUMN x;
ALTER TABLE pt ATTACH PARTITION tp1 FOR VALUES IN (1);
ALTER TABLE pt DETACH PARTITION tp1;
I get a memory access error detected by Valgrind:
2024-10-24 12:05:04.645 UTC [1079077] LOG: statement: ALTER TABLE pt DETACH PARTITION tp1;
==00:00:00:07.887 1079077== Invalid read of size 2
==00:00:00:07.887 1079077== at 0x4A61DD: DetachPartitionFinalize (tablecmds.c:19545)
==00:00:00:07.887 1079077== by 0x4A5C11: ATExecDetachPartition (tablecmds.c:19386)
==00:00:00:07.887 1079077== by 0x48561E: ATExecCmd (tablecmds.c:5540)
==00:00:00:07.887 1079077== by 0x4845DE: ATRewriteCatalogs (tablecmds.c:5203)
==00:00:00:07.887 1079077== by 0x4838EC: ATController (tablecmds.c:4758)
==00:00:00:07.887 1079077== by 0x4834F1: AlterTable (tablecmds.c:4404)
==00:00:00:07.887 1079077== by 0x7D6D52: ProcessUtilitySlow (utility.c:1318)
==00:00:00:07.887 1079077== by 0x7D65F7: standard_ProcessUtility (utility.c:1067)
==00:00:00:07.887 1079077== by 0x7D54F7: ProcessUtility (utility.c:523)
==00:00:00:07.887 1079077== by 0x7D3D70: PortalRunUtility (pquery.c:1158)
==00:00:00:07.887 1079077== by 0x7D3FE7: PortalRunMulti (pquery.c:1316)
==00:00:00:07.887 1079077== by 0x7D3431: PortalRun (pquery.c:791)
Reproduced on REL_15_STABLE .. master.
Best regards,
Alexander
Alexander Lakhin <exclusion@gmail.com> 于2024年10月24日周四 22:00写道:
Hello Alvaro,
22.10.2024 17:32, Alvaro Herrera wrote:
Yeah. I pushed these patches finally, thanks!
Please look at a new anomaly introduced with 53af9491a. When running the
following script:
CREATE TABLE t (a int, b int, PRIMARY KEY (a, b));
CREATE TABLE pt (a int, b int, FOREIGN KEY (a, b) REFERENCES t(a, b))
PARTITION BY LIST (a);CREATE TABLE tp1 (x int, a int, b int);
ALTER TABLE tp1 DROP COLUMN x;ALTER TABLE pt ATTACH PARTITION tp1 FOR VALUES IN (1);
ALTER TABLE pt DETACH PARTITION tp1;
I get a memory access error detected by Valgrind:
2024-10-24 12:05:04.645 UTC [1079077] LOG: statement: ALTER TABLE pt
DETACH PARTITION tp1;
==00:00:00:07.887 1079077== Invalid read of size 2
==00:00:00:07.887 1079077== at 0x4A61DD: DetachPartitionFinalize
(tablecmds.c:19545)
==00:00:00:07.887 1079077== by 0x4A5C11: ATExecDetachPartition
(tablecmds.c:19386)
==00:00:00:07.887 1079077== by 0x48561E: ATExecCmd (tablecmds.c:5540)
==00:00:00:07.887 1079077== by 0x4845DE: ATRewriteCatalogs
(tablecmds.c:5203)
==00:00:00:07.887 1079077== by 0x4838EC: ATController (tablecmds.c:4758)
==00:00:00:07.887 1079077== by 0x4834F1: AlterTable (tablecmds.c:4404)
==00:00:00:07.887 1079077== by 0x7D6D52: ProcessUtilitySlow
(utility.c:1318)
==00:00:00:07.887 1079077== by 0x7D65F7: standard_ProcessUtility
(utility.c:1067)
==00:00:00:07.887 1079077== by 0x7D54F7: ProcessUtility (utility.c:523)
==00:00:00:07.887 1079077== by 0x7D3D70: PortalRunUtility
(pquery.c:1158)
==00:00:00:07.887 1079077== by 0x7D3FE7: PortalRunMulti (pquery.c:1316)
==00:00:00:07.887 1079077== by 0x7D3431: PortalRun (pquery.c:791)Reproduced on REL_15_STABLE .. master.
Sorry, I can't reproduce this leak on master.
--
Thanks,
Tender Wang
Alexander Lakhin <exclusion@gmail.com> 于2024年10月24日周四 22:00写道:
Hello Alvaro,
22.10.2024 17:32, Alvaro Herrera wrote:
Yeah. I pushed these patches finally, thanks!
Please look at a new anomaly introduced with 53af9491a. When running the
following script:
CREATE TABLE t (a int, b int, PRIMARY KEY (a, b));
CREATE TABLE pt (a int, b int, FOREIGN KEY (a, b) REFERENCES t(a, b))
PARTITION BY LIST (a);CREATE TABLE tp1 (x int, a int, b int);
ALTER TABLE tp1 DROP COLUMN x;ALTER TABLE pt ATTACH PARTITION tp1 FOR VALUES IN (1);
ALTER TABLE pt DETACH PARTITION tp1;
I get a memory access error detected by Valgrind:
2024-10-24 12:05:04.645 UTC [1079077] LOG: statement: ALTER TABLE pt
DETACH PARTITION tp1;
==00:00:00:07.887 1079077== Invalid read of size 2
==00:00:00:07.887 1079077== at 0x4A61DD: DetachPartitionFinalize
(tablecmds.c:19545)
==00:00:00:07.887 1079077== by 0x4A5C11: ATExecDetachPartition
(tablecmds.c:19386)
==00:00:00:07.887 1079077== by 0x48561E: ATExecCmd (tablecmds.c:5540)
==00:00:00:07.887 1079077== by 0x4845DE: ATRewriteCatalogs
(tablecmds.c:5203)
==00:00:00:07.887 1079077== by 0x4838EC: ATController (tablecmds.c:4758)
==00:00:00:07.887 1079077== by 0x4834F1: AlterTable (tablecmds.c:4404)
==00:00:00:07.887 1079077== by 0x7D6D52: ProcessUtilitySlow
(utility.c:1318)
==00:00:00:07.887 1079077== by 0x7D65F7: standard_ProcessUtility
(utility.c:1067)
==00:00:00:07.887 1079077== by 0x7D54F7: ProcessUtility (utility.c:523)
==00:00:00:07.887 1079077== by 0x7D3D70: PortalRunUtility
(pquery.c:1158)
==00:00:00:07.887 1079077== by 0x7D3FE7: PortalRunMulti (pquery.c:1316)
==00:00:00:07.887 1079077== by 0x7D3431: PortalRun (pquery.c:791)Reproduced on REL_15_STABLE .. master.
Thanks for reporting. I can reproduce this memory invalid access on HEAD.
After debuging codes, I found the root cause.
In DetachPartitionFinalize(), below code:
att = TupleDescAttr(RelationGetDescr(partRel),
attmap->attnums[conkey[i] - 1] - 1);
We should use confkey[i] -1 not conkey[i] - 1;
In this case, the length of attmap is 2, conkey= {2,3}, confkey={1,2},
attmap->attnums[conkey[1] -1] will trigger
memory invalid access.
In the block, we process referenced side, so wo should use confkey.
I attach a patch to fix this.
--
Thanks,
Tender Wang
Attachments:
0001-Fix-memory-invalid-access-due-to-using-wrong-conkey.patchapplication/octet-stream; name=0001-Fix-memory-invalid-access-due-to-using-wrong-conkey.patchDownload
From 34df95163f09f9d4a32dc17a244b8b839de4f90f Mon Sep 17 00:00:00 2001
From: Tender Wang <tndrwang@gmail.com>
Date: Fri, 25 Oct 2024 14:40:23 +0800
Subject: [PATCH] Fix memory invalid access due to using wrong conkey.
---
src/backend/commands/tablecmds.c | 2 +-
src/test/regress/expected/foreign_key.out | 14 +++++++++++++-
src/test/regress/sql/foreign_key.sql | 13 +++++++++++++
3 files changed, 27 insertions(+), 2 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index e14bc0c054..bf6e8ec59d 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -19543,7 +19543,7 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
Form_pg_attribute att;
att = TupleDescAttr(RelationGetDescr(partRel),
- attmap->attnums[conkey[i] - 1] - 1);
+ attmap->attnums[confkey[i] - 1] - 1);
fkconstraint->fk_attrs = lappend(fkconstraint->fk_attrs,
makeString(NameStr(att->attname)));
}
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index b73e7dced8..5bc36f7bb4 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -2997,7 +2997,7 @@ INSERT INTO fk_r_1 VALUES (2, 1, 2); -- should fail
ERROR: insert or update on table "fk_r_1" violates foreign key constraint "fk_r_p_id_p_jd_fkey"
DETAIL: Key (p_id, p_jd)=(1, 2) is not present in table "fk_p".
DELETE FROM fk_p; -- should fail
-ERROR: update or delete on table "fk_p_1_1" violates foreign key constraint "fk_r_1_p_id_p_jd_fkey1" on table "fk_r_1"
+ERROR: update or delete on table "fk_p_1_1" violates foreign key constraint "fk_r_1_id_p_id_fkey1" on table "fk_r_1"
DETAIL: Key (id, jd)=(1, 1) is still referenced from table "fk_r_1".
ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1);
ALTER TABLE fk_r ATTACH PARTITION fk_r_2 FOR VALUES IN (2);
@@ -3030,3 +3030,15 @@ SET client_min_messages TO warning;
DROP SCHEMA fkpart12 CASCADE;
RESET client_min_messages;
RESET search_path;
+CREATE SCHEMA fkpart13
+ CREATE TABLE fkt (a int, b int, PRIMARY KEY (a, b))
+ CREATE TABLE fkpt (a int, b int, FOREIGN KEY (a, b) REFERENCES fkt(a, b)) PARTITION BY LIST (a)
+ CREATE TABLE fktp1 (x int, a int, b int);
+SET search_path TO fkpart13;
+ALTER TABLE fktp1 DROP COLUMN x;
+ALTER TABLE fkpt ATTACH PARTITION fktp1 FOR VALUES IN (1);
+ALTER TABLE fkpt DETACH PARTITION fktp1;
+SET client_min_messages TO warning;
+DROP SCHEMA fkpart13 CASCADE;
+RESET client_min_messages;
+RESET search_path;
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index 9b2a6b6bff..386bf862f1 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -2143,3 +2143,16 @@ SET client_min_messages TO warning;
DROP SCHEMA fkpart12 CASCADE;
RESET client_min_messages;
RESET search_path;
+
+CREATE SCHEMA fkpart13
+ CREATE TABLE fkt (a int, b int, PRIMARY KEY (a, b))
+ CREATE TABLE fkpt (a int, b int, FOREIGN KEY (a, b) REFERENCES fkt(a, b)) PARTITION BY LIST (a)
+ CREATE TABLE fktp1 (x int, a int, b int);
+SET search_path TO fkpart13;
+ALTER TABLE fktp1 DROP COLUMN x;
+ALTER TABLE fkpt ATTACH PARTITION fktp1 FOR VALUES IN (1);
+ALTER TABLE fkpt DETACH PARTITION fktp1;
+SET client_min_messages TO warning;
+DROP SCHEMA fkpart13 CASCADE;
+RESET client_min_messages;
+RESET search_path;
--
2.25.1
On 2024-Oct-25, Tender Wang wrote:
Thanks for reporting. I can reproduce this memory invalid access on HEAD.
After debuging codes, I found the root cause.
In DetachPartitionFinalize(), below code:
att = TupleDescAttr(RelationGetDescr(partRel),
attmap->attnums[conkey[i] - 1] - 1);We should use confkey[i] -1 not conkey[i] - 1;
Wow, how embarrasing, now that's one _really_ stupid bug and it's
totally my own. Thanks for the analysis! I'll get this patched soon.
--
Álvaro Herrera Breisgau, Deutschland — https://www.EnterpriseDB.com/
Alvaro Herrera <alvherre@alvh.no-ip.org> 于2024年10月25日周五 16:30写道:
On 2024-Oct-25, Tender Wang wrote:
Thanks for reporting. I can reproduce this memory invalid access on HEAD.
After debuging codes, I found the root cause.
In DetachPartitionFinalize(), below code:
att = TupleDescAttr(RelationGetDescr(partRel),
attmap->attnums[conkey[i] - 1] - 1);We should use confkey[i] -1 not conkey[i] - 1;
Wow, how embarrasing, now that's one _really_ stupid bug and it's
totally my own. Thanks for the analysis! I'll get this patched soon.
Hi,
When I debug codes, I find that the way to access AttrMap almost uses
"attrmp_ptr->attnums[offset]."
The codes now usually don't check if the offset is out of bounds, which
seems unsafe.
Can we wrap an access function? For example:
inline AttrNumber(attrmap_ptr, offset)
{
Assert(offset >= 0 && offset < attrmap_ptr->maplen);
return attrmap_ptr->attnums[offset];
}
Or a Macro.
I'm not sure if it's worth doing this.
--
Thanks,
Tender Wang
Hello Alvaro and Tender Wang,
24.10.2024 17:00, Alexander Lakhin wrote:
Please look at a new anomaly introduced with 53af9491a. When running the
following script:
CREATE TABLE t (a int, b int, PRIMARY KEY (a, b));
CREATE TABLE pt (a int, b int, FOREIGN KEY (a, b) REFERENCES t(a, b))
PARTITION BY LIST (a);CREATE TABLE tp1 (x int, a int, b int);
ALTER TABLE tp1 DROP COLUMN x;ALTER TABLE pt ATTACH PARTITION tp1 FOR VALUES IN (1);
ALTER TABLE pt DETACH PARTITION tp1;
I've also discovered another anomaly with a similar setup, but it's not
related to 53af9491a.
CREATE TABLE t (a int, PRIMARY KEY (a));
INSERT INTO t VALUES (1);
CREATE TABLE pt (b int, a int) PARTITION BY RANGE (a);
ALTER TABLE pt DROP COLUMN b;
ALTER TABLE pt ADD FOREIGN KEY (a) REFERENCES t ON DELETE SET DEFAULT (a);
CREATE TABLE tp1 PARTITION OF pt FOR VALUES FROM (1) TO (2);
ALTER TABLE pt DETACH PARTITION tp1;
DELETE FROM t;
\d+ t
This script ends up with:
ERROR: invalid attribute number 2
ERROR: cache lookup failed for attribute 2 of relation 16398
(Perhaps it deserves a separate discussion.)
Best regards,
Alexander
On 2024-Oct-25, Alexander Lakhin wrote:
I've also discovered another anomaly with a similar setup, but it's not
related to 53af9491a.
Hmm, it may well be a preexisting problem, but I do think it involves
the same code. As far as I can tell, the value "2" here
This script ends up with:
ERROR: invalid attribute number 2
ERROR: cache lookup failed for attribute 2 of relation 16398
is coming from riinfo->confdelsetcols which was set up by
DetachPartitionFinalize during the last DETACH operation.
(Perhaps it deserves a separate discussion.)
No need for that.
A backtrace from the errfinish gives me this (apologies for the
wide terminal):
#0 errfinish (filename=0x5610d93e4510 "../../../../../pgsql/source/master/src/backend/parser/parse_relation.c", lineno=3618,
funcname=0x5610d93e5518 <__func__.5> "attnumAttName") at ../../../../../../pgsql/source/master/src/backend/utils/error/elog.c:475
#1 0x00005610d8d68bcc in attnumAttName (rd=rd@entry=0x7f69dd0c1a90, attid=2) at ../../../../../pgsql/source/master/src/backend/parser/parse_relation.c:3618
#2 0x00005610d924a8c5 in ri_set (trigdata=0x7ffe38d924c0, is_set_null=<optimized out>, tgkind=<optimized out>)
at ../../../../../../pgsql/source/master/src/backend/utils/adt/ri_triggers.c:1219
#3 0x00005610d8f897ea in ExecCallTriggerFunc (trigdata=trigdata@entry=0x7ffe38d924c0, tgindx=tgindx@entry=2, finfo=0x5611153f5768, finfo@entry=0x5611153f5708,
instr=instr@entry=0x0, per_tuple_context=per_tuple_context@entry=0x561115471c30) at ../../../../../pgsql/source/master/src/backend/commands/trigger.c:2362
#4 0x00005610d8f8b646 in AfterTriggerExecute (trig_tuple_slot2=0x0, trig_tuple_slot1=0x0, per_tuple_context=0x561115471c30, instr=0x0, finfo=0x5611153f5708,
trigdesc=0x5611153f53e8, dst_relInfo=<optimized out>, src_relInfo=<optimized out>, relInfo=0x5611153f51d8, event=0x56111546fd4c, estate=0x5611153f4d50)
at ../../../../../pgsql/source/master/src/backend/commands/trigger.c:4498
#5 afterTriggerInvokeEvents (events=events@entry=0x5611154292a0, firing_id=1, estate=estate@entry=0x5611153f4d50, delete_ok=delete_ok@entry=false)
at ../../../../../pgsql/source/master/src/backend/commands/trigger.c:4735
#6 0x00005610d8f8ff10 in AfterTriggerEndQuery (estate=estate@entry=0x5611153f4d50) at ../../../../../pgsql/source/master/src/backend/commands/trigger.c:5090
#7 0x00005610d8fb2457 in standard_ExecutorFinish (queryDesc=0x561115460810) at ../../../../../pgsql/source/master/src/backend/executor/execMain.c:442
#8 0x00005610d917df48 in ProcessQuery (plan=0x56111545ef50, sourceText=0x56111539cfb0 "DELETE FROM t;", params=0x0, queryEnv=0x0, dest=0x56111545f0b0,
qc=0x7ffe38d92830) at ../../../../../pgsql/source/master/src/backend/tcop/pquery.c:193
and then
(gdb) frame 2
#2 0x00005610d924a8c5 in ri_set (trigdata=0x7ffe38d924c0, is_set_null=<optimized out>, tgkind=<optimized out>)
at ../../../../../../pgsql/source/master/src/backend/utils/adt/ri_triggers.c:1219
1219 quoteOneName(attname, RIAttName(fk_rel, set_cols[i]));
(gdb) print *riinfo
$5 = {constraint_id = 16400, valid = true, constraint_root_id = 16400, oidHashValue = 3001019032, rootHashValue = 3001019032, conname = {
data = "pt_a_fkey", '\000' <repeats 54 times>}, pk_relid = 16384, fk_relid = 16397, confupdtype = 97 'a', confdeltype = 100 'd', ndelsetcols = 1,
confdelsetcols = {2, 0 <repeats 31 times>}, confmatchtype = 115 's', hasperiod = false, nkeys = 1, pk_attnums = {1, 0 <repeats 31 times>}, fk_attnums = {1,
0 <repeats 31 times>}, pf_eq_oprs = {96, 0 <repeats 31 times>}, pp_eq_oprs = {96, 0 <repeats 31 times>}, ff_eq_oprs = {96, 0 <repeats 31 times>},
period_contained_by_oper = 0, agged_period_contained_by_oper = 0, valid_link = {prev = 0x5611153f4c30, next = 0x5610d96b51b0 <ri_constraint_cache_valid_list>}}
--
Álvaro Herrera 48°01'N 7°57'E — https://www.EnterpriseDB.com/
"Digital and video cameras have this adjustment and film cameras don't for the
same reason dogs and cats lick themselves: because they can." (Ken Rockwell)
On 2024-Oct-25, Tender Wang wrote:
When I debug codes, I find that the way to access AttrMap almost uses
"attrmp_ptr->attnums[offset]."
The codes now usually don't check if the offset is out of bounds, which
seems unsafe.
Can we wrap an access function? For example:
inline AttrNumber(attrmap_ptr, offset)
{
Assert(offset >= 0 && offset < attrmap_ptr->maplen);
return attrmap_ptr->attnums[offset];
}
I don't see any reason not to do this, though it's not directly related
to the bugs in this thread. I encourage you to submit a patch, opening
a new thread.
--
Álvaro Herrera 48°01'N 7°57'E — https://www.EnterpriseDB.com/
On 2024-Oct-25, Alvaro Herrera wrote:
On 2024-Oct-25, Tender Wang wrote:
Thanks for reporting. I can reproduce this memory invalid access on HEAD.
After debuging codes, I found the root cause.
In DetachPartitionFinalize(), below code:
att = TupleDescAttr(RelationGetDescr(partRel),
attmap->attnums[conkey[i] - 1] - 1);We should use confkey[i] -1 not conkey[i] - 1;
Wow, how embarrasing, now that's one _really_ stupid bug and it's
totally my own. Thanks for the analysis! I'll get this patched soon.
Actually, now that I look at this again, I think this proposed fix is
wrong; conkey/confkey confusion is not what the problem is. Rather, the
problem is that we're applying a tuple conversion map when none should
be applied. So the fix is to remove "attmap" altogether. One thing
that didn't appear correct to me was that the patch was changing one
constraint name so that it appeared that the constrained columns were
"id, p_id" but that didn't make sense: they are "p_id, p_jd" instead.
Then I realized that you're wrong that it's the referenced side that
we're processing: what we're doing there is generate the fk_attrs list,
which is the list of constrained columns (not the list of referenced
columns, which is pk_attrs).
I also felt like modifying the fkpart12 test rather than adding a
separate fkpart13, so I did that.
So here's a v2 for this. (Commit message needs love still, but it's a
bit late here.)
--
Álvaro Herrera PostgreSQL Developer — https://www.EnterpriseDB.com/
"[PostgreSQL] is a great group; in my opinion it is THE best open source
development communities in existence anywhere." (Lamar Owen)
Attachments:
v2-0001-No-need-to-use-an-attrmap-when-detaching-a-foreig.patchtext/x-diff; charset=utf-8Download
From bb2aef5cbfd12bf47d49fdf58f303048f0c50339 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=81lvaro=20Herrera?= <alvherre@alvh.no-ip.org>
Date: Sat, 26 Oct 2024 23:44:58 +0200
Subject: [PATCH v2] No need to use an attrmap when detaching a foreign key
The reason is that the constraint being created is on the same relation
as the constraint that it spawns from.
---
src/backend/commands/tablecmds.c | 8 +++-----
src/test/regress/expected/foreign_key.out | 9 ++++++---
src/test/regress/sql/foreign_key.sql | 10 +++++++---
3 files changed, 16 insertions(+), 11 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index e14bc0c0548..acb7b002940 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -19502,7 +19502,6 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
Oid conffeqop[INDEX_MAX_KEYS];
int numfkdelsetcols;
AttrNumber confdelsetcols[INDEX_MAX_KEYS];
- AttrMap *attmap;
Relation refdRel;
DeconstructFkConstraintRow(contup,
@@ -19535,15 +19534,14 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
fkconstraint->old_pktable_oid = InvalidOid;
fkconstraint->location = -1;
- attmap = build_attrmap_by_name(RelationGetDescr(partRel),
- RelationGetDescr(rel),
- false);
+ /* set up colnames, used to generate the constraint name */
for (int i = 0; i < numfks; i++)
{
Form_pg_attribute att;
att = TupleDescAttr(RelationGetDescr(partRel),
- attmap->attnums[conkey[i] - 1] - 1);
+ conkey[i] - 1);
+
fkconstraint->fk_attrs = lappend(fkconstraint->fk_attrs,
makeString(NameStr(att->attname)));
}
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index b73e7dced8f..69994c98e32 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -2944,17 +2944,20 @@ CREATE SCHEMA fkpart12
CREATE TABLE fk_p ( id int, jd int, PRIMARY KEY(id, jd)) PARTITION BY list (id)
CREATE TABLE fk_p_1 PARTITION OF fk_p FOR VALUES IN (1) PARTITION BY list (jd)
CREATE TABLE fk_p_1_1 PARTITION OF fk_p_1 FOR VALUES IN (1)
- CREATE TABLE fk_p_1_2 PARTITION OF fk_p_1 FOR VALUES IN (2)
+ CREATE TABLE fk_p_1_2 (x int, y int, jd int NOT NULL, id int NOT NULL)
CREATE TABLE fk_p_2 PARTITION OF fk_p FOR VALUES IN (2) PARTITION BY list (jd)
CREATE TABLE fk_p_2_1 PARTITION OF fk_p_2 FOR VALUES IN (1)
CREATE TABLE fk_p_2_2 PARTITION OF fk_p_2 FOR VALUES IN (2)
- CREATE TABLE fk_r_1 ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL)
+ CREATE TABLE fk_r_1 ( p_jd int NOT NULL, x int, id int PRIMARY KEY, p_id int NOT NULL)
CREATE TABLE fk_r_2 ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL) PARTITION BY list (id)
CREATE TABLE fk_r_2_1 PARTITION OF fk_r_2 FOR VALUES IN (2, 1)
CREATE TABLE fk_r ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL,
FOREIGN KEY (p_id, p_jd) REFERENCES fk_p (id, jd)
) PARTITION BY list (id);
SET search_path TO fkpart12;
+ALTER TABLE fk_p_1_2 DROP COLUMN x, DROP COLUMN y;
+ALTER TABLE fk_p_1 ATTACH PARTITION fk_p_1_2 FOR VALUES IN (2);
+ALTER TABLE fk_r_1 DROP COLUMN x;
INSERT INTO fk_p VALUES (1, 1);
ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1);
ALTER TABLE fk_r ATTACH PARTITION fk_r_2 FOR VALUES IN (2);
@@ -2993,7 +2996,7 @@ Foreign-key constraints:
"fk_r_p_id_p_jd_fkey" FOREIGN KEY (p_id, p_jd) REFERENCES fk_p(id, jd)
Number of partitions: 1 (Use \d+ to list them.)
-INSERT INTO fk_r_1 VALUES (2, 1, 2); -- should fail
+INSERT INTO fk_r_1 (id, p_id, p_jd) VALUES (2, 1, 2); -- should fail
ERROR: insert or update on table "fk_r_1" violates foreign key constraint "fk_r_p_id_p_jd_fkey"
DETAIL: Key (p_id, p_jd)=(1, 2) is not present in table "fk_p".
DELETE FROM fk_p; -- should fail
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index 9b2a6b6bff7..2e710e419c2 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -2097,11 +2097,11 @@ CREATE SCHEMA fkpart12
CREATE TABLE fk_p ( id int, jd int, PRIMARY KEY(id, jd)) PARTITION BY list (id)
CREATE TABLE fk_p_1 PARTITION OF fk_p FOR VALUES IN (1) PARTITION BY list (jd)
CREATE TABLE fk_p_1_1 PARTITION OF fk_p_1 FOR VALUES IN (1)
- CREATE TABLE fk_p_1_2 PARTITION OF fk_p_1 FOR VALUES IN (2)
+ CREATE TABLE fk_p_1_2 (x int, y int, jd int NOT NULL, id int NOT NULL)
CREATE TABLE fk_p_2 PARTITION OF fk_p FOR VALUES IN (2) PARTITION BY list (jd)
CREATE TABLE fk_p_2_1 PARTITION OF fk_p_2 FOR VALUES IN (1)
CREATE TABLE fk_p_2_2 PARTITION OF fk_p_2 FOR VALUES IN (2)
- CREATE TABLE fk_r_1 ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL)
+ CREATE TABLE fk_r_1 ( p_jd int NOT NULL, x int, id int PRIMARY KEY, p_id int NOT NULL)
CREATE TABLE fk_r_2 ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL) PARTITION BY list (id)
CREATE TABLE fk_r_2_1 PARTITION OF fk_r_2 FOR VALUES IN (2, 1)
CREATE TABLE fk_r ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL,
@@ -2109,6 +2109,10 @@ CREATE SCHEMA fkpart12
) PARTITION BY list (id);
SET search_path TO fkpart12;
+ALTER TABLE fk_p_1_2 DROP COLUMN x, DROP COLUMN y;
+ALTER TABLE fk_p_1 ATTACH PARTITION fk_p_1_2 FOR VALUES IN (2);
+ALTER TABLE fk_r_1 DROP COLUMN x;
+
INSERT INTO fk_p VALUES (1, 1);
ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1);
@@ -2124,7 +2128,7 @@ ALTER TABLE fk_r DETACH PARTITION fk_r_2;
\d fk_r_2
-INSERT INTO fk_r_1 VALUES (2, 1, 2); -- should fail
+INSERT INTO fk_r_1 (id, p_id, p_jd) VALUES (2, 1, 2); -- should fail
DELETE FROM fk_p; -- should fail
ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1);
--
2.39.5
Alvaro Herrera <alvherre@alvh.no-ip.org> 于2024年10月27日周日 05:47写道:
On 2024-Oct-25, Alvaro Herrera wrote:
On 2024-Oct-25, Tender Wang wrote:
Thanks for reporting. I can reproduce this memory invalid access on
HEAD.
After debuging codes, I found the root cause.
In DetachPartitionFinalize(), below code:
att = TupleDescAttr(RelationGetDescr(partRel),
attmap->attnums[conkey[i] - 1] - 1);We should use confkey[i] -1 not conkey[i] - 1;
Wow, how embarrasing, now that's one _really_ stupid bug and it's
totally my own. Thanks for the analysis! I'll get this patched soon.Actually, now that I look at this again, I think this proposed fix is
wrong; conkey/confkey confusion is not what the problem is. Rather, the
problem is that we're applying a tuple conversion map when none should
be applied. So the fix is to remove "attmap" altogether. One thing
that didn't appear correct to me was that the patch was changing one
constraint name so that it appeared that the constrained columns were
"id, p_id" but that didn't make sense: they are "p_id, p_jd" instead.
Yeah, The constrained name change made me think about if the patch is
correct.
After your explanation, I have understood it.
Then I realized that you're wrong that it's the referenced side that
we're processing: what we're doing there is generate the fk_attrs list,
which is the list of constrained columns (not the list of referenced
columns, which is pk_attrs).I also felt like modifying the fkpart12 test rather than adding a
separate fkpart13, so I did that.So here's a v2 for this. (Commit message needs love still, but it's a
bit late here.)
The v2 LGTM.
BTW, while reviewing the v2 patch, I found the parentConTup in
foreach(cell, fks) block
didn't need it anymore. We can remove the related codes.
--
Thanks,
Tender Wang
Alvaro Herrera <alvherre@alvh.no-ip.org> 于2024年10月25日周五 23:14写道:
On 2024-Oct-25, Alexander Lakhin wrote:
I've also discovered another anomaly with a similar setup, but it's not
related to 53af9491a.Hmm, it may well be a preexisting problem, but I do think it involves
the same code. As far as I can tell, the value "2" hereThis script ends up with:
ERROR: invalid attribute number 2
ERROR: cache lookup failed for attribute 2 of relation 16398is coming from riinfo->confdelsetcols which was set up by
DetachPartitionFinalize during the last DETACH operation.
Hmm, actually, the confdelsetcols before detach and after detach is always
{2}, as below:
postgres=# select oid, conname, conrelid,conparentid,confdelsetcols from
pg_constraint where conrelid = 16397;
oid | conname | conrelid | conparentid | confdelsetcols
-------+-----------+----------+-------------+----------------
16400 | pt_a_fkey | 16397 | 16392 | {2}
(1 row)
postgres=# ALTER TABLE pt DETACH PARTITION tp1;
ALTER TABLE
postgres=# select oid, conname, conrelid,conparentid,confdelsetcols from
pg_constraint where conrelid = 16397;
oid | conname | conrelid | conparentid | confdelsetcols
-------+-----------+----------+-------------+----------------
16400 | pt_a_fkey | 16397 | 0 | {2}
(1 row)
Even though no detach, the confdelsetcols is {2} . But no error report.
Because the rel->rd_att->natts of pt is 2.
It will not go into tp1 because tp1 is a partition of pt. But after
detach, the rel->rd_att->natts of tp1 is 1,
so "ERROR: invalid attribute number 2" will report.
CREATE TABLE tp1... will ignore the dropped column of parent, so the natts
of tp1 is 1, but its parent is 2.
--
Thanks,
Tender Wang
On 2024-Oct-27, Tender Wang wrote:
BTW, while reviewing the v2 patch, I found the parentConTup in
foreach(cell, fks) block
didn't need it anymore. We can remove the related codes.
True. Done so in this v3.
I noticed another problem here: we're grabbing the wrong lock type on
the referenced rel (AccessShareLock) during detach. (What's more: we
release it afterwards, which is the wrong thing to do. We need to keep
such locks until end of transaction). I didn't try to construct a case
where this would be a problem, but if I change AccessShare to NoLock,
the assertion that says we don't hold _any_ lock on that relation fires,
which means that we're not taking any locks on those rels before this
point. So this lock strength choice is definitely wrong. I changed it
to ShareRowExclusive, which is what we're suppose to use when adding a
trigger. Another option might be to do find_all_inheritors() ahead of
time to grab all the necessary locks, but I didn't try to do that. I
also added an assertion in addFkRecurseReferenced to verify that we hold
that in all paths, and after this change it doesn't fire anymore with
the regression tests.
I have still not edited the commit message.
--
Álvaro Herrera PostgreSQL Developer — https://www.EnterpriseDB.com/
Attachments:
v3-0001-No-need-to-use-an-attrmap-when-detaching-a-foreig.patchtext/x-diff; charset=utf-8Download
From 3f18de0a263e507b79b1f1eb9a9a128cd6292779 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=81lvaro=20Herrera?= <alvherre@alvh.no-ip.org>
Date: Sat, 26 Oct 2024 23:44:58 +0200
Subject: [PATCH v3] No need to use an attrmap when detaching a foreign key
The reason is that the constraint being created is on the same relation
as the constraint that it spawns from.
---
src/backend/commands/tablecmds.c | 25 ++++++++---------------
src/test/regress/expected/foreign_key.out | 9 +++++---
src/test/regress/sql/foreign_key.sql | 10 ++++++---
3 files changed, 21 insertions(+), 23 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index e14bc0c0548..c993abc9792 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -10372,6 +10372,8 @@ addFkRecurseReferenced(Constraint *fkconstraint, Relation rel,
Oid deleteTriggerOid,
updateTriggerOid;
+ Assert(CheckRelationLockedByMe(pkrel, ShareRowExclusiveLock, true));
+
/*
* Create the action triggers that enforce the constraint.
*/
@@ -19436,8 +19438,7 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
foreach(cell, fks)
{
ForeignKeyCacheInfo *fk = lfirst(cell);
- HeapTuple contup,
- parentConTup;
+ HeapTuple contup;
Form_pg_constraint conform;
Oid insertTriggerOid,
updateTriggerOid;
@@ -19455,13 +19456,6 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
continue;
}
- Assert(OidIsValid(conform->conparentid));
- parentConTup = SearchSysCache1(CONSTROID,
- ObjectIdGetDatum(conform->conparentid));
- if (!HeapTupleIsValid(parentConTup))
- elog(ERROR, "cache lookup failed for constraint %u",
- conform->conparentid);
-
/*
* The constraint on this table must be marked no longer a child of
* the parent's constraint, as do its check triggers.
@@ -19502,7 +19496,6 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
Oid conffeqop[INDEX_MAX_KEYS];
int numfkdelsetcols;
AttrNumber confdelsetcols[INDEX_MAX_KEYS];
- AttrMap *attmap;
Relation refdRel;
DeconstructFkConstraintRow(contup,
@@ -19535,20 +19528,19 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
fkconstraint->old_pktable_oid = InvalidOid;
fkconstraint->location = -1;
- attmap = build_attrmap_by_name(RelationGetDescr(partRel),
- RelationGetDescr(rel),
- false);
+ /* set up colnames, used to generate the constraint name */
for (int i = 0; i < numfks; i++)
{
Form_pg_attribute att;
att = TupleDescAttr(RelationGetDescr(partRel),
- attmap->attnums[conkey[i] - 1] - 1);
+ conkey[i] - 1);
+
fkconstraint->fk_attrs = lappend(fkconstraint->fk_attrs,
makeString(NameStr(att->attname)));
}
- refdRel = table_open(fk->confrelid, AccessShareLock);
+ refdRel = table_open(fk->confrelid, ShareRowExclusiveLock);
addFkRecurseReferenced(fkconstraint, partRel,
refdRel,
@@ -19565,11 +19557,10 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
true,
InvalidOid, InvalidOid,
conform->conperiod);
- table_close(refdRel, AccessShareLock);
+ table_close(refdRel, NoLock); /* keep lock till end of xact */
}
ReleaseSysCache(contup);
- ReleaseSysCache(parentConTup);
}
list_free_deep(fks);
if (trigrel)
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index b73e7dced8f..69994c98e32 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -2944,17 +2944,20 @@ CREATE SCHEMA fkpart12
CREATE TABLE fk_p ( id int, jd int, PRIMARY KEY(id, jd)) PARTITION BY list (id)
CREATE TABLE fk_p_1 PARTITION OF fk_p FOR VALUES IN (1) PARTITION BY list (jd)
CREATE TABLE fk_p_1_1 PARTITION OF fk_p_1 FOR VALUES IN (1)
- CREATE TABLE fk_p_1_2 PARTITION OF fk_p_1 FOR VALUES IN (2)
+ CREATE TABLE fk_p_1_2 (x int, y int, jd int NOT NULL, id int NOT NULL)
CREATE TABLE fk_p_2 PARTITION OF fk_p FOR VALUES IN (2) PARTITION BY list (jd)
CREATE TABLE fk_p_2_1 PARTITION OF fk_p_2 FOR VALUES IN (1)
CREATE TABLE fk_p_2_2 PARTITION OF fk_p_2 FOR VALUES IN (2)
- CREATE TABLE fk_r_1 ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL)
+ CREATE TABLE fk_r_1 ( p_jd int NOT NULL, x int, id int PRIMARY KEY, p_id int NOT NULL)
CREATE TABLE fk_r_2 ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL) PARTITION BY list (id)
CREATE TABLE fk_r_2_1 PARTITION OF fk_r_2 FOR VALUES IN (2, 1)
CREATE TABLE fk_r ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL,
FOREIGN KEY (p_id, p_jd) REFERENCES fk_p (id, jd)
) PARTITION BY list (id);
SET search_path TO fkpart12;
+ALTER TABLE fk_p_1_2 DROP COLUMN x, DROP COLUMN y;
+ALTER TABLE fk_p_1 ATTACH PARTITION fk_p_1_2 FOR VALUES IN (2);
+ALTER TABLE fk_r_1 DROP COLUMN x;
INSERT INTO fk_p VALUES (1, 1);
ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1);
ALTER TABLE fk_r ATTACH PARTITION fk_r_2 FOR VALUES IN (2);
@@ -2993,7 +2996,7 @@ Foreign-key constraints:
"fk_r_p_id_p_jd_fkey" FOREIGN KEY (p_id, p_jd) REFERENCES fk_p(id, jd)
Number of partitions: 1 (Use \d+ to list them.)
-INSERT INTO fk_r_1 VALUES (2, 1, 2); -- should fail
+INSERT INTO fk_r_1 (id, p_id, p_jd) VALUES (2, 1, 2); -- should fail
ERROR: insert or update on table "fk_r_1" violates foreign key constraint "fk_r_p_id_p_jd_fkey"
DETAIL: Key (p_id, p_jd)=(1, 2) is not present in table "fk_p".
DELETE FROM fk_p; -- should fail
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index 9b2a6b6bff7..2e710e419c2 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -2097,11 +2097,11 @@ CREATE SCHEMA fkpart12
CREATE TABLE fk_p ( id int, jd int, PRIMARY KEY(id, jd)) PARTITION BY list (id)
CREATE TABLE fk_p_1 PARTITION OF fk_p FOR VALUES IN (1) PARTITION BY list (jd)
CREATE TABLE fk_p_1_1 PARTITION OF fk_p_1 FOR VALUES IN (1)
- CREATE TABLE fk_p_1_2 PARTITION OF fk_p_1 FOR VALUES IN (2)
+ CREATE TABLE fk_p_1_2 (x int, y int, jd int NOT NULL, id int NOT NULL)
CREATE TABLE fk_p_2 PARTITION OF fk_p FOR VALUES IN (2) PARTITION BY list (jd)
CREATE TABLE fk_p_2_1 PARTITION OF fk_p_2 FOR VALUES IN (1)
CREATE TABLE fk_p_2_2 PARTITION OF fk_p_2 FOR VALUES IN (2)
- CREATE TABLE fk_r_1 ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL)
+ CREATE TABLE fk_r_1 ( p_jd int NOT NULL, x int, id int PRIMARY KEY, p_id int NOT NULL)
CREATE TABLE fk_r_2 ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL) PARTITION BY list (id)
CREATE TABLE fk_r_2_1 PARTITION OF fk_r_2 FOR VALUES IN (2, 1)
CREATE TABLE fk_r ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL,
@@ -2109,6 +2109,10 @@ CREATE SCHEMA fkpart12
) PARTITION BY list (id);
SET search_path TO fkpart12;
+ALTER TABLE fk_p_1_2 DROP COLUMN x, DROP COLUMN y;
+ALTER TABLE fk_p_1 ATTACH PARTITION fk_p_1_2 FOR VALUES IN (2);
+ALTER TABLE fk_r_1 DROP COLUMN x;
+
INSERT INTO fk_p VALUES (1, 1);
ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1);
@@ -2124,7 +2128,7 @@ ALTER TABLE fk_r DETACH PARTITION fk_r_2;
\d fk_r_2
-INSERT INTO fk_r_1 VALUES (2, 1, 2); -- should fail
+INSERT INTO fk_r_1 (id, p_id, p_jd) VALUES (2, 1, 2); -- should fail
DELETE FROM fk_p; -- should fail
ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1);
--
2.39.5
Alvaro Herrera <alvherre@alvh.no-ip.org> 于2024年10月28日周一 17:16写道:
On 2024-Oct-27, Tender Wang wrote:
BTW, while reviewing the v2 patch, I found the parentConTup in
foreach(cell, fks) block
didn't need it anymore. We can remove the related codes.True. Done so in this v3.
I noticed another problem here: we're grabbing the wrong lock type on
the referenced rel (AccessShareLock) during detach. (What's more: we
release it afterwards, which is the wrong thing to do. We need to keep
such locks until end of transaction). I didn't try to construct a case
where this would be a problem, but if I change AccessShare to NoLock,
the assertion that says we don't hold _any_ lock on that relation fires,
which means that we're not taking any locks on those rels before this
point. So this lock strength choice is definitely wrong. I changed it
to ShareRowExclusive, which is what we're suppose to use when adding a
trigger.
In CloneFKReferencing(), the constrForm->confrelid uses the same lock type.
I think you're right. I don't find any other problem.
--
Thanks,
Tender Wang
I'm trying to write release notes for commits 53af9491a et al,
and it seems to me that we need to explain how to get out of
the mess that would be left behind by the old DETACH code.
There's no hint about that in the commit message :-(
Clearly, if you have now-inconsistent data, there's little
help for that but to manually fix the inconsistencies.
What I am worried about is how to get to a state where you
have correct catalog entries for the constraint.
Will ALTER TABLE DROP CONSTRAINT on the now stand-alone table
work to clean out the old catalog entries for the constraint?
I'm worried that it will either fail, or go through but remove
triggers on the referenced table that we still need for the
original partitioned table. If that doesn't work I think we had
better create a recipe for manually removing the detritus.
Once the old entries are gone it should be possible to do ALTER TABLE
ADD CONSTRAINT (with an updated server), and that would validate
your data. It's the DROP CONSTRAINT part that worries me.
regards, tom lane
On 2024-Nov-05, Tom Lane wrote:
I'm trying to write release notes for commits 53af9491a et al,
and it seems to me that we need to explain how to get out of
the mess that would be left behind by the old DETACH code.
There's no hint about that in the commit message :-(
Clearly, if you have now-inconsistent data, there's little
help for that but to manually fix the inconsistencies.
What I am worried about is how to get to a state where you
have correct catalog entries for the constraint.Will ALTER TABLE DROP CONSTRAINT on the now stand-alone table
work to clean out the old catalog entries for the constraint?
Yes -- as far as I can tell, a DROP CONSTRAINT of the offending
constraint is successful and leaves no unwanted detritus.
Perhaps one more task for me is to figure out a way to get a list of all
the constraints that are broken because of this ... let me see if I can
figure that out.
I'm worried that it will either fail, or go through but remove
triggers on the referenced table that we still need for the
original partitioned table. If that doesn't work I think we had
better create a recipe for manually removing the detritus.
As as far as I can see, it works and no triggers are spuriously removed.
Once the old entries are gone it should be possible to do ALTER TABLE
ADD CONSTRAINT (with an updated server), and that would validate
your data. It's the DROP CONSTRAINT part that worries me.
Yeah, that's correct: adding the constraint again after removing its
broken self detects that there are values violating the RI.
--
Álvaro Herrera 48°01'N 7°57'E — https://www.EnterpriseDB.com/
Thou shalt check the array bounds of all strings (indeed, all arrays), for
surely where thou typest "foo" someone someday shall type
"supercalifragilisticexpialidocious" (5th Commandment for C programmers)
On 2024-Nov-06, Alvaro Herrera wrote:
Perhaps one more task for me is to figure out a way to get a list of all
the constraints that are broken because of this ... let me see if I can
figure that out.
It's gotta be something like this,
SELECT conrelid::regclass AS "constrained table",
conname as constraint, confrelid::regclass AS "references"
FROM pg_constraint
WHERE contype = 'f' and conparentid = 0 AND
(SELECT count(*) FROM pg_constraint p2 WHERE conparentid = pg_constraint.oid) <>
(SELECT count(*)
FROM pg_inherits
WHERE inhparent = pg_constraint.conrelid OR inhparent = pg_constraint.confrelid);
Essentially, top-level constraints should have as many children
constraint as direct partitions each partitioned table has. Ideally
here you'd get an empty set, but you won't if the DETACH problem has
occurred. I'll test this further later or maybe tomorrow as time
allows.
A quick test rig for this is:
create table pk (a int primary key) partition by list (a);
create table pk1 partition of pk for values in (1);
create table pk2367 partition of pk for values in (2, 3, 6, 7) partition by list (a);
create table pk67 partition of pk2367 for values in (6, 7) partition by list (a);
create table pk2 partition of pk2367 for values in (2);
create table pk3 partition of pk2367 for values in (3);
create table pk6 partition of pk67 for values in (6);
create table pk7 partition of pk67 for values in (7);
create table pk45 partition of pk for values in (4, 5) partition by list (a);
create table pk4 partition of pk45 for values in (4);
create table pk5 partition of pk45 for values in (5);
create table fk (a int references pk) partition by list (a);
create table fk1 partition of fk for values in (1);
create table fk2367 partition of fk for values in (2, 3, 6, 7) partition by list (a);
create table fk67 partition of fk2367 for values in (6, 7) partition by list (a);
create table fk2 partition of fk2367 for values in (2);
create table fk3 partition of fk2367 for values in (3);
create table fk6 partition of fk67 for values in (6);
create table fk7 partition of fk67 for values in (7);
create table fk45 partition of fk for values in (4, 5) partition by list (a);
create table fk4 partition of fk45 for values in (4);
create table fk5 partition of fk45 for values in (5);
alter table fk detach partition fk2367;
Before the fix, you get
constrained table │ constraint │ references
───────────────────┼────────────┼────────────
fk2367 │ fk_a_fkey │ pk
(1 fila)
which means you need to
ALTER TABLE fk2367 DROP CONSTRAINT fk_a_fkey;
and then put it back. Maybe it'd be better to have the query emit the
commands to drop and reconstruct the FK?
--
Álvaro Herrera Breisgau, Deutschland — https://www.EnterpriseDB.com/
Alvaro Herrera <alvherre@alvh.no-ip.org> writes:
Perhaps one more task for me is to figure out a way to get a list of all
the constraints that are broken because of this ... let me see if I can
figure that out.
It's gotta be something like this,
SELECT conrelid::regclass AS "constrained table",
conname as constraint, confrelid::regclass AS "references"
FROM pg_constraint
WHERE contype = 'f' and conparentid = 0 AND
(SELECT count(*) FROM pg_constraint p2 WHERE conparentid = pg_constraint.oid) <>
(SELECT count(*)
FROM pg_inherits
WHERE inhparent = pg_constraint.conrelid OR inhparent = pg_constraint.confrelid);
Hmm ... interestingly, if I run this in HEAD's regression database,
I get
constrained table | constraint | references
-------------------+---------------+-------------
clstr_tst | clstr_tst_con | clstr_tst_s
(1 row)
Digging a bit deeper, the sub-select for conparentid finds no rows,
but the sub-select on pg_inherits finds
regression=# SELECT inhrelid::regclass, inhparent::regclass, inhseqno,inhdetachpending from pg_inherits WHERE inhparent = 'clstr_tst'::regclass or inhparent = 'clstr_tst_s'::regclass;
inhrelid | inhparent | inhseqno | inhdetachpending
---------------+-----------+----------+------------------
clstr_tst_inh | clstr_tst | 1 | f
(1 row)
So it looks like this query needs a guard to make it ignore
constraints on traditional-inheritance tables.
regards, tom lane
On 2024-Nov-08, Tom Lane wrote:
Hmm ... interestingly, if I run this in HEAD's regression database,
I getconstrained table | constraint | references
-------------------+---------------+-------------
clstr_tst | clstr_tst_con | clstr_tst_s
(1 row)
Eeek.
So it looks like this query needs a guard to make it ignore
constraints on traditional-inheritance tables.
Hmm, looks tricky, the only thing I found was to only consider rows in
pg_inherit if there's a corresponding one in pg_partitioned_table. This
should do it. I added the DROP/ADD commands. I also added some
pg_catalog schema quals, though that may be kinda useless. Anyway, this
reports empty in the regression database.
SELECT conrelid::pg_catalog.regclass AS "constrained table",
conname AS constraint,
confrelid::pg_catalog.regclass AS "references",
pg_catalog.format('ALTER TABLE %s DROP CONSTRAINT %I;', conrelid::regclass, conname),
pg_catalog.format('ALTER TABLE %s ADD CONSTRAINT %I %s;', conrelid::regclass, conname,
pg_catalog.pg_get_constraintdef(oid))
FROM pg_catalog.pg_constraint
WHERE contype = 'f' and conparentid = 0 AND
(SELECT count(*) FROM pg_catalog.pg_constraint p2 WHERE conparentid = pg_constraint.oid) <>
(SELECT count(*)
FROM pg_catalog.pg_inherits
WHERE EXISTS (SELECT 1 FROM pg_catalog.pg_partitioned_table WHERE partrelid = inhparent) AND
inhparent = pg_constraint.conrelid OR inhparent = pg_constraint.confrelid)
;
I would have loved to be able to add the constraint as NOT VALID
followed by a separate VALIDATE command, because if there are any RI
violations, the constraint would now be in place to prevent future ones.
However,
=# ALTER TABLE fk2367 ADD CONSTRAINT fk_a_fkey FOREIGN KEY (a) REFERENCES pk(a) NOT VALID;
ERROR: cannot add NOT VALID foreign key on partitioned table "fk2367" referencing relation "pk"
DETAIL: This feature is not yet supported on partitioned tables.
So it looks like we should suggest to save the output of the query,
execute each DROP followed by each ADD, and if the latter fails, fix the
violations and retry the ADD.
--
Álvaro Herrera PostgreSQL Developer — https://www.EnterpriseDB.com/
"Java is clearly an example of money oriented programming" (A. Stepanov)
Alvaro Herrera <alvherre@alvh.no-ip.org> writes:
So it looks like we should suggest to save the output of the query,
execute each DROP followed by each ADD, and if the latter fails, fix the
violations and retry the ADD.
Right. I'll make it so -- thanks for doing the legwork on
creating the query!
regards, tom lane