Skip to content

Commit d20194c

Browse files
alvherreioguixtenderwg
committed
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 f56f8f8) 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 f456634, 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
1 parent beab395 commit d20194c

File tree

3 files changed

+281
-26
lines changed

3 files changed

+281
-26
lines changed

src/backend/commands/tablecmds.c

Lines changed: 134 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -10102,6 +10102,82 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
1010210102
table_close(trigrel, RowExclusiveLock);
1010310103

1010410104
ConstraintSetParentConstraint(fk->conoid, parentConstrOid, partRelid);
10105+
10106+
/*
10107+
* If the referenced table is partitioned, then the partition we're
10108+
* attaching now has extra pg_constraint rows and action triggers that are
10109+
* no longer needed. Remove those.
10110+
*/
10111+
if (get_rel_relkind(fk->confrelid) == RELKIND_PARTITIONED_TABLE)
10112+
{
10113+
Relation pg_constraint = table_open(ConstraintRelationId, RowShareLock);
10114+
ObjectAddresses *objs;
10115+
HeapTuple consttup;
10116+
10117+
ScanKeyInit(&key,
10118+
Anum_pg_constraint_conrelid,
10119+
BTEqualStrategyNumber, F_OIDEQ,
10120+
ObjectIdGetDatum(fk->conrelid));
10121+
10122+
scan = systable_beginscan(pg_constraint,
10123+
ConstraintRelidTypidNameIndexId,
10124+
true, NULL, 1, &key);
10125+
objs = new_object_addresses();
10126+
while ((consttup = systable_getnext(scan)) != NULL)
10127+
{
10128+
Form_pg_constraint conform = (Form_pg_constraint) GETSTRUCT(consttup);
10129+
10130+
if (conform->conparentid != fk->conoid)
10131+
continue;
10132+
else
10133+
{
10134+
ObjectAddress addr;
10135+
SysScanDesc scan2;
10136+
ScanKeyData key2;
10137+
int n PG_USED_FOR_ASSERTS_ONLY;
10138+
10139+
ObjectAddressSet(addr, ConstraintRelationId, conform->oid);
10140+
add_exact_object_address(&addr, objs);
10141+
10142+
/*
10143+
* First we must delete the dependency records that bind
10144+
* the constraint records together.
10145+
*/
10146+
n = deleteDependencyRecordsForSpecific(ConstraintRelationId,
10147+
conform->oid,
10148+
DEPENDENCY_INTERNAL,
10149+
ConstraintRelationId,
10150+
fk->conoid);
10151+
Assert(n == 1); /* actually only one is expected */
10152+
10153+
/*
10154+
* Now search for the triggers for this constraint and set
10155+
* them up for deletion too
10156+
*/
10157+
ScanKeyInit(&key2,
10158+
Anum_pg_trigger_tgconstraint,
10159+
BTEqualStrategyNumber, F_OIDEQ,
10160+
ObjectIdGetDatum(conform->oid));
10161+
scan2 = systable_beginscan(trigrel, TriggerConstraintIndexId,
10162+
true, NULL, 1, &key2);
10163+
while ((trigtup = systable_getnext(scan2)) != NULL)
10164+
{
10165+
ObjectAddressSet(addr, TriggerRelationId,
10166+
((Form_pg_trigger) GETSTRUCT(trigtup))->oid);
10167+
add_exact_object_address(&addr, objs);
10168+
}
10169+
systable_endscan(scan2);
10170+
}
10171+
}
10172+
/* make the dependency deletions visible */
10173+
CommandCounterIncrement();
10174+
performMultipleDeletions(objs, DROP_RESTRICT,
10175+
PERFORM_DELETION_INTERNAL);
10176+
systable_endscan(scan);
10177+
10178+
table_close(pg_constraint, RowShareLock);
10179+
}
10180+
1010510181
CommandCounterIncrement();
1010610182
return true;
1010710183
}
@@ -17470,6 +17546,7 @@ ATExecDetachPartition(Relation rel, RangeVar *name)
1747017546
List *indexes;
1747117547
List *fks;
1747217548
ListCell *cell;
17549+
ObjectAddresses *dropobjs = NULL;
1747317550

1747417551
/*
1747517552
* We must lock the default partition, because detaching this partition
@@ -17567,16 +17644,15 @@ ATExecDetachPartition(Relation rel, RangeVar *name)
1756717644
DropClonedTriggersFromPartition(RelationGetRelid(partRel));
1756817645

1756917646
/*
17570-
* Detach any foreign keys that are inherited. This includes creating
17571-
* additional action triggers.
17647+
* Detach any foreign keys that are inherited -- or, if they reference
17648+
* partitioned tables, drop them.
1757217649
*/
1757317650
fks = copyObject(RelationGetFKeyList(partRel));
1757417651
foreach(cell, fks)
1757517652
{
1757617653
ForeignKeyCacheInfo *fk = lfirst(cell);
1757717654
HeapTuple contup;
1757817655
Form_pg_constraint conform;
17579-
Constraint *fkconstraint;
1758017656

1758117657
contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
1758217658
if (!HeapTupleIsValid(contup))
@@ -17591,39 +17667,71 @@ ATExecDetachPartition(Relation rel, RangeVar *name)
1759117667
continue;
1759217668
}
1759317669

17594-
/* unset conparentid and adjust conislocal, coninhcount, etc. */
17670+
/* Mark the constraint as independent */
1759517671
ConstraintSetParentConstraint(fk->conoid, InvalidOid, InvalidOid);
1759617672

1759717673
/*
17598-
* Make the action triggers on the referenced relation. When this was
17599-
* a partition the action triggers pointed to the parent rel (they
17600-
* still do), but now we need separate ones of our own.
17674+
* If the constraint references a partitioned table, just drop the
17675+
* constraint, because it's more work to preserve the constraint
17676+
* correctly.
17677+
*
17678+
* If it references a plain table, then we can create the action
17679+
* triggers and it'll be okay.
1760117680
*/
17602-
fkconstraint = makeNode(Constraint);
17603-
fkconstraint->contype = CONSTRAINT_FOREIGN;
17604-
fkconstraint->conname = pstrdup(NameStr(conform->conname));
17605-
fkconstraint->deferrable = conform->condeferrable;
17606-
fkconstraint->initdeferred = conform->condeferred;
17607-
fkconstraint->location = -1;
17608-
fkconstraint->pktable = NULL;
17609-
fkconstraint->fk_attrs = NIL;
17610-
fkconstraint->pk_attrs = NIL;
17611-
fkconstraint->fk_matchtype = conform->confmatchtype;
17612-
fkconstraint->fk_upd_action = conform->confupdtype;
17613-
fkconstraint->fk_del_action = conform->confdeltype;
17614-
fkconstraint->old_conpfeqop = NIL;
17615-
fkconstraint->old_pktable_oid = InvalidOid;
17616-
fkconstraint->skip_validation = false;
17617-
fkconstraint->initially_valid = true;
17681+
if (get_rel_relkind(fk->confrelid) == RELKIND_PARTITIONED_TABLE)
17682+
{
17683+
ObjectAddress constraddr;
1761817684

17619-
createForeignKeyActionTriggers(partRel, conform->confrelid,
17620-
fkconstraint, fk->conoid,
17621-
conform->conindid);
17685+
/* make the dependency deletions above visible */
17686+
CommandCounterIncrement();
17687+
17688+
/*
17689+
* Remember the constraint and its triggers for later deletion.
17690+
*/
17691+
if (dropobjs == NULL)
17692+
dropobjs = new_object_addresses();
17693+
ObjectAddressSet(constraddr, ConstraintRelationId, fk->conoid);
17694+
add_exact_object_address(&constraddr, dropobjs);
17695+
}
17696+
else
17697+
{
17698+
Constraint *fkconstraint;
17699+
17700+
/*
17701+
* Make the action triggers on the referenced relation. When this
17702+
* was a partition the action triggers pointed to the parent rel
17703+
* (they still do), but now we need separate ones of our own.
17704+
*/
17705+
fkconstraint = makeNode(Constraint);
17706+
fkconstraint->contype = CONSTRAINT_FOREIGN;
17707+
fkconstraint->conname = pstrdup(NameStr(conform->conname));
17708+
fkconstraint->deferrable = conform->condeferrable;
17709+
fkconstraint->initdeferred = conform->condeferred;
17710+
fkconstraint->location = -1;
17711+
fkconstraint->pktable = NULL;
17712+
fkconstraint->fk_attrs = NIL;
17713+
fkconstraint->pk_attrs = NIL;
17714+
fkconstraint->fk_matchtype = conform->confmatchtype;
17715+
fkconstraint->fk_upd_action = conform->confupdtype;
17716+
fkconstraint->fk_del_action = conform->confdeltype;
17717+
fkconstraint->old_conpfeqop = NIL;
17718+
fkconstraint->old_pktable_oid = InvalidOid;
17719+
fkconstraint->skip_validation = false;
17720+
fkconstraint->initially_valid = true;
17721+
17722+
createForeignKeyActionTriggers(partRel, conform->confrelid,
17723+
fkconstraint, fk->conoid,
17724+
conform->conindid);
17725+
}
1762217726

1762317727
ReleaseSysCache(contup);
1762417728
}
1762517729
list_free_deep(fks);
1762617730

17731+
/* If we collected any constraints for deletion, do so now. */
17732+
if (dropobjs != NULL)
17733+
performMultipleDeletions(dropobjs, DROP_CASCADE, 0);
17734+
1762717735
/*
1762817736
* Any sub-constraints that are in the referenced-side of a larger
1762917737
* constraint have to be removed. This partition is no longer part of the

src/test/regress/expected/foreign_key.out

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2647,3 +2647,94 @@ DROP SCHEMA fkpart9 CASCADE;
26472647
NOTICE: drop cascades to 2 other objects
26482648
DETAIL: drop cascades to table fkpart9.pk
26492649
drop cascades to table fkpart9.fk
2650+
-- When a table is attached as partition to a partitioned table that has
2651+
-- a foreign key to another partitioned table, it acquires a clone of the
2652+
-- FK. Upon detach, Postgres 14 and earlier remove the foreign key (newer
2653+
-- versions make it a standalone constraint.)
2654+
CREATE SCHEMA fkpart12
2655+
CREATE TABLE fk_p ( id int, jd int, PRIMARY KEY(id, jd)) PARTITION BY list (id)
2656+
CREATE TABLE fk_p_1 PARTITION OF fk_p FOR VALUES IN (1) PARTITION BY list (jd)
2657+
CREATE TABLE fk_p_1_1 PARTITION OF fk_p_1 FOR VALUES IN (1)
2658+
CREATE TABLE fk_p_1_2 PARTITION OF fk_p_1 FOR VALUES IN (2)
2659+
CREATE TABLE fk_p_2 PARTITION OF fk_p FOR VALUES IN (2) PARTITION BY list (jd)
2660+
CREATE TABLE fk_p_2_1 PARTITION OF fk_p_2 FOR VALUES IN (1)
2661+
CREATE TABLE fk_p_2_2 PARTITION OF fk_p_2 FOR VALUES IN (2)
2662+
CREATE TABLE fk_r_1 ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL)
2663+
CREATE TABLE fk_r_2 ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL) PARTITION BY list (id)
2664+
CREATE TABLE fk_r_2_1 PARTITION OF fk_r_2 FOR VALUES IN (2, 1)
2665+
CREATE TABLE fk_r ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL,
2666+
FOREIGN KEY (p_id, p_jd) REFERENCES fk_p (id, jd)
2667+
) PARTITION BY list (id);
2668+
SET search_path TO fkpart12;
2669+
INSERT INTO fk_p VALUES (1, 1);
2670+
ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1);
2671+
ALTER TABLE fk_r ATTACH PARTITION fk_r_2 FOR VALUES IN (2);
2672+
\d fk_r_2
2673+
Partitioned table "fkpart12.fk_r_2"
2674+
Column | Type | Collation | Nullable | Default
2675+
--------+---------+-----------+----------+---------
2676+
id | integer | | not null |
2677+
p_id | integer | | not null |
2678+
p_jd | integer | | not null |
2679+
Partition of: fk_r FOR VALUES IN (2)
2680+
Partition key: LIST (id)
2681+
Indexes:
2682+
"fk_r_2_pkey" PRIMARY KEY, btree (id)
2683+
Foreign-key constraints:
2684+
TABLE "fk_r" CONSTRAINT "fk_r_p_id_p_jd_fkey" FOREIGN KEY (p_id, p_jd) REFERENCES fk_p(id, jd)
2685+
Number of partitions: 1 (Use \d+ to list them.)
2686+
2687+
INSERT INTO fk_r VALUES (1, 1, 1);
2688+
INSERT INTO fk_r VALUES (2, 2, 1); -- fails
2689+
ERROR: insert or update on table "fk_r_2_1" violates foreign key constraint "fk_r_p_id_p_jd_fkey"
2690+
DETAIL: Key (p_id, p_jd)=(2, 1) is not present in table "fk_p".
2691+
ALTER TABLE fk_r DETACH PARTITION fk_r_1;
2692+
ALTER TABLE fk_r DETACH PARTITION fk_r_2;
2693+
\d fk_r_2
2694+
Partitioned table "fkpart12.fk_r_2"
2695+
Column | Type | Collation | Nullable | Default
2696+
--------+---------+-----------+----------+---------
2697+
id | integer | | not null |
2698+
p_id | integer | | not null |
2699+
p_jd | integer | | not null |
2700+
Partition key: LIST (id)
2701+
Indexes:
2702+
"fk_r_2_pkey" PRIMARY KEY, btree (id)
2703+
Number of partitions: 1 (Use \d+ to list them.)
2704+
2705+
INSERT INTO fk_r_1 VALUES (2, 1, 2); -- works: there's no FK anymore
2706+
DELETE FROM fk_p; -- works
2707+
ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1); -- fails
2708+
ERROR: partition constraint of relation "fk_r_1" is violated by some row
2709+
INSERT INTO fk_r_2 VALUES (2, 2, 2);
2710+
INSERT INTO fk_p VALUES (2, 2);
2711+
ALTER TABLE fk_r ATTACH PARTITION fk_r_2 FOR VALUES IN (2);
2712+
\d fk_r_2
2713+
Partitioned table "fkpart12.fk_r_2"
2714+
Column | Type | Collation | Nullable | Default
2715+
--------+---------+-----------+----------+---------
2716+
id | integer | | not null |
2717+
p_id | integer | | not null |
2718+
p_jd | integer | | not null |
2719+
Partition of: fk_r FOR VALUES IN (2)
2720+
Partition key: LIST (id)
2721+
Indexes:
2722+
"fk_r_2_pkey" PRIMARY KEY, btree (id)
2723+
Foreign-key constraints:
2724+
TABLE "fk_r" CONSTRAINT "fk_r_p_id_p_jd_fkey" FOREIGN KEY (p_id, p_jd) REFERENCES fk_p(id, jd)
2725+
Number of partitions: 1 (Use \d+ to list them.)
2726+
2727+
DELETE FROM fk_p; -- fails
2728+
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"
2729+
DETAIL: Key (id, jd)=(2, 2) is still referenced from table "fk_r".
2730+
-- these should all fail
2731+
ALTER TABLE fk_r_1 DROP CONSTRAINT fk_r_p_id_p_jd_fkey;
2732+
ERROR: constraint "fk_r_p_id_p_jd_fkey" of relation "fk_r_1" does not exist
2733+
ALTER TABLE fk_r DROP CONSTRAINT fk_r_p_id_p_jd_fkey1;
2734+
ERROR: cannot drop inherited constraint "fk_r_p_id_p_jd_fkey1" of relation "fk_r"
2735+
ALTER TABLE fk_r_2 DROP CONSTRAINT fk_r_p_id_p_jd_fkey;
2736+
ERROR: cannot drop inherited constraint "fk_r_p_id_p_jd_fkey" of relation "fk_r_2"
2737+
SET client_min_messages TO warning;
2738+
DROP SCHEMA fkpart12 CASCADE;
2739+
RESET client_min_messages;
2740+
RESET search_path;

src/test/regress/sql/foreign_key.sql

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1886,3 +1886,59 @@ DELETE FROM fkpart9.pk WHERE a=35;
18861886
SELECT * FROM fkpart9.pk;
18871887
SELECT * FROM fkpart9.fk;
18881888
DROP SCHEMA fkpart9 CASCADE;
1889+
1890+
-- When a table is attached as partition to a partitioned table that has
1891+
-- a foreign key to another partitioned table, it acquires a clone of the
1892+
-- FK. Upon detach, Postgres 14 and earlier remove the foreign key (newer
1893+
-- versions make it a standalone constraint.)
1894+
CREATE SCHEMA fkpart12
1895+
CREATE TABLE fk_p ( id int, jd int, PRIMARY KEY(id, jd)) PARTITION BY list (id)
1896+
CREATE TABLE fk_p_1 PARTITION OF fk_p FOR VALUES IN (1) PARTITION BY list (jd)
1897+
CREATE TABLE fk_p_1_1 PARTITION OF fk_p_1 FOR VALUES IN (1)
1898+
CREATE TABLE fk_p_1_2 PARTITION OF fk_p_1 FOR VALUES IN (2)
1899+
CREATE TABLE fk_p_2 PARTITION OF fk_p FOR VALUES IN (2) PARTITION BY list (jd)
1900+
CREATE TABLE fk_p_2_1 PARTITION OF fk_p_2 FOR VALUES IN (1)
1901+
CREATE TABLE fk_p_2_2 PARTITION OF fk_p_2 FOR VALUES IN (2)
1902+
CREATE TABLE fk_r_1 ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL)
1903+
CREATE TABLE fk_r_2 ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL) PARTITION BY list (id)
1904+
CREATE TABLE fk_r_2_1 PARTITION OF fk_r_2 FOR VALUES IN (2, 1)
1905+
CREATE TABLE fk_r ( id int PRIMARY KEY, p_id int NOT NULL, p_jd int NOT NULL,
1906+
FOREIGN KEY (p_id, p_jd) REFERENCES fk_p (id, jd)
1907+
) PARTITION BY list (id);
1908+
SET search_path TO fkpart12;
1909+
1910+
INSERT INTO fk_p VALUES (1, 1);
1911+
1912+
ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1);
1913+
ALTER TABLE fk_r ATTACH PARTITION fk_r_2 FOR VALUES IN (2);
1914+
1915+
\d fk_r_2
1916+
1917+
INSERT INTO fk_r VALUES (1, 1, 1);
1918+
INSERT INTO fk_r VALUES (2, 2, 1); -- fails
1919+
1920+
ALTER TABLE fk_r DETACH PARTITION fk_r_1;
1921+
ALTER TABLE fk_r DETACH PARTITION fk_r_2;
1922+
1923+
\d fk_r_2
1924+
1925+
INSERT INTO fk_r_1 VALUES (2, 1, 2); -- works: there's no FK anymore
1926+
DELETE FROM fk_p; -- works
1927+
1928+
ALTER TABLE fk_r ATTACH PARTITION fk_r_1 FOR VALUES IN (1); -- fails
1929+
1930+
INSERT INTO fk_r_2 VALUES (2, 2, 2);
1931+
INSERT INTO fk_p VALUES (2, 2);
1932+
ALTER TABLE fk_r ATTACH PARTITION fk_r_2 FOR VALUES IN (2);
1933+
\d fk_r_2
1934+
DELETE FROM fk_p; -- fails
1935+
1936+
-- these should all fail
1937+
ALTER TABLE fk_r_1 DROP CONSTRAINT fk_r_p_id_p_jd_fkey;
1938+
ALTER TABLE fk_r DROP CONSTRAINT fk_r_p_id_p_jd_fkey1;
1939+
ALTER TABLE fk_r_2 DROP CONSTRAINT fk_r_p_id_p_jd_fkey;
1940+
1941+
SET client_min_messages TO warning;
1942+
DROP SCHEMA fkpart12 CASCADE;
1943+
RESET client_min_messages;
1944+
RESET search_path;

0 commit comments

Comments
 (0)