Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Skip to content

Commit 923c135

Browse files
committed
Have ALTER CONSTRAINT recurse on partitioned tables
When ALTER TABLE .. ALTER CONSTRAINT changes deferrability properties changed in a partitioned table, we failed to propagate those changes correctly to partitions and to triggers. Repair by adding a recursion mechanism to affect all derived constraints and all derived triggers. (In particular, recurse to partitions even if their respective parents are already in the desired state: it is possible for the partitions to have been altered individually.) Because foreign keys involve tables in two sides, we cannot use the standard ALTER TABLE recursion mechanism, so we invent our own by following pg_constraint.conparentid down. When ALTER TABLE .. ALTER CONSTRAINT is invoked on the derived pg_constraint object that's automaticaly created in a partition as a result of a constraint added to its parent, raise an error instead of pretending to work and then failing to modify all the affected triggers. Before this commit such a command would be allowed but failed to affect all triggers, so it would silently misbehave. (Restoring dumps of existing databases is not affected, because pg_dump does not produce anything for such a derived constraint anyway.) Add some tests for the case. Backpatch to 11, where foreign key support was added to partitioned tables by commit 3de241d. (A related change is commit f56f8f8 in pg12 which added support for FKs *referencing* partitioned tables; this is what forces us to use an ad-hoc recursion mechanism for this.) Diagnosed by Tom Lane from bug report from Ron L Johnson. As of this writing, no reviews were offered. Discussion: https://postgr.es/m/75fe0761-a291-86a9-c8d8-4906da077469@gmail.com Discussion: https://postgr.es/m/3144850.1607369633@sss.pgh.pa.us
1 parent 91a6b38 commit 923c135

File tree

3 files changed

+311
-38
lines changed

3 files changed

+311
-38
lines changed

src/backend/commands/tablecmds.c

Lines changed: 158 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,9 @@ static void AlterSeqNamespaces(Relation classRel, Relation rel,
328328
LOCKMODE lockmode);
329329
static ObjectAddress ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd,
330330
bool recurse, bool recursing, LOCKMODE lockmode);
331+
static bool ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
332+
Relation rel, HeapTuple contuple, List **otherrelids,
333+
LOCKMODE lockmode);
331334
static ObjectAddress ATExecValidateConstraint(List **wqueue,
332335
Relation rel, char *constrName,
333336
bool recurse, bool recursing, LOCKMODE lockmode);
@@ -4292,6 +4295,7 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
42924295
break;
42934296
case AT_AlterConstraint: /* ALTER CONSTRAINT */
42944297
ATSimplePermissions(rel, ATT_TABLE);
4298+
/* Recursion occurs during execution phase */
42954299
pass = AT_PASS_MISC;
42964300
break;
42974301
case AT_ValidateConstraint: /* VALIDATE CONSTRAINT */
@@ -9718,28 +9722,29 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
97189722
* Update the attributes of a constraint.
97199723
*
97209724
* Currently only works for Foreign Key constraints.
9721-
* Foreign keys do not inherit, so we purposely ignore the
9722-
* recursion bit here, but we keep the API the same for when
9723-
* other constraint types are supported.
97249725
*
97259726
* If the constraint is modified, returns its address; otherwise, return
97269727
* InvalidObjectAddress.
97279728
*/
97289729
static ObjectAddress
9729-
ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd,
9730-
bool recurse, bool recursing, LOCKMODE lockmode)
9730+
ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
9731+
bool recursing, LOCKMODE lockmode)
97319732
{
97329733
Constraint *cmdcon;
97339734
Relation conrel;
9735+
Relation tgrel;
97349736
SysScanDesc scan;
97359737
ScanKeyData skey[3];
97369738
HeapTuple contuple;
97379739
Form_pg_constraint currcon;
97389740
ObjectAddress address;
9741+
List *otherrelids = NIL;
9742+
ListCell *lc;
97399743

97409744
cmdcon = castNode(Constraint, cmd->def);
97419745

97429746
conrel = table_open(ConstraintRelationId, RowExclusiveLock);
9747+
tgrel = table_open(TriggerRelationId, RowExclusiveLock);
97439748

97449749
/*
97459750
* Find and check the target constraint
@@ -9773,50 +9778,150 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd,
97739778
errmsg("constraint \"%s\" of relation \"%s\" is not a foreign key constraint",
97749779
cmdcon->conname, RelationGetRelationName(rel))));
97759780

9781+
/*
9782+
* If it's not the topmost constraint, raise an error.
9783+
*
9784+
* Altering a non-topmost constraint leaves some triggers untouched, since
9785+
* they are not directly connected to this constraint; also, pg_dump would
9786+
* ignore the deferrability status of the individual constraint, since it
9787+
* only dumps topmost constraints. Avoid these problems by refusing this
9788+
* operation and telling the user to alter the parent constraint instead.
9789+
*/
9790+
if (OidIsValid(currcon->conparentid))
9791+
{
9792+
HeapTuple tp;
9793+
Oid parent = currcon->conparentid;
9794+
char *ancestorname = NULL;
9795+
char *ancestortable = NULL;
9796+
9797+
/* Loop to find the topmost constraint */
9798+
while (HeapTupleIsValid(tp = SearchSysCache1(CONSTROID, ObjectIdGetDatum(parent))))
9799+
{
9800+
Form_pg_constraint contup = (Form_pg_constraint) GETSTRUCT(tp);
9801+
9802+
/* If no parent, this is the constraint we want */
9803+
if (!OidIsValid(contup->conparentid))
9804+
{
9805+
ancestorname = pstrdup(NameStr(contup->conname));
9806+
ancestortable = get_rel_name(contup->conrelid);
9807+
ReleaseSysCache(tp);
9808+
break;
9809+
}
9810+
9811+
parent = contup->conparentid;
9812+
ReleaseSysCache(tp);
9813+
}
9814+
9815+
ereport(ERROR,
9816+
(errmsg("cannot alter constraint \"%s\" on relation \"%s\"",
9817+
cmdcon->conname, RelationGetRelationName(rel)),
9818+
ancestorname && ancestortable ?
9819+
errdetail("Constraint \"%s\" is derived from constraint \"%s\" of relation \"%s\".",
9820+
cmdcon->conname, ancestorname, ancestortable) : 0,
9821+
errhint("You may alter the constraint it derives from, instead.")));
9822+
}
9823+
9824+
/*
9825+
* Do the actual catalog work. We can skip changing if already in the
9826+
* desired state, but not if a partitioned table: partitions need to be
9827+
* processed regardless, in case they had the constraint locally changed.
9828+
*/
9829+
address = InvalidObjectAddress;
9830+
if (currcon->condeferrable != cmdcon->deferrable ||
9831+
currcon->condeferred != cmdcon->initdeferred ||
9832+
rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
9833+
{
9834+
if (ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, rel, contuple,
9835+
&otherrelids, lockmode))
9836+
ObjectAddressSet(address, ConstraintRelationId, currcon->oid);
9837+
}
9838+
9839+
/*
9840+
* ATExecConstrRecurse already invalidated relcache for the relations
9841+
* having the constraint itself; here we also invalidate for relations
9842+
* that have any triggers that are part of the constraint.
9843+
*/
9844+
foreach(lc, otherrelids)
9845+
CacheInvalidateRelcacheByRelid(lfirst_oid(lc));
9846+
9847+
systable_endscan(scan);
9848+
9849+
table_close(tgrel, RowExclusiveLock);
9850+
table_close(conrel, RowExclusiveLock);
9851+
9852+
return address;
9853+
}
9854+
9855+
/*
9856+
* Recursive subroutine of ATExecAlterConstraint. Returns true if the
9857+
* constraint is altered.
9858+
*
9859+
* *otherrelids is appended OIDs of relations containing affected triggers.
9860+
*
9861+
* Note that we must recurse even when the values are correct, in case
9862+
* indirect descendants have had their constraints altered locally.
9863+
* (This could be avoided if we forbade altering constraints in partitions
9864+
* but existing releases don't do that.)
9865+
*/
9866+
static bool
9867+
ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
9868+
Relation rel, HeapTuple contuple, List **otherrelids,
9869+
LOCKMODE lockmode)
9870+
{
9871+
Form_pg_constraint currcon;
9872+
Oid conoid;
9873+
Oid refrelid;
9874+
bool changed = false;
9875+
9876+
currcon = (Form_pg_constraint) GETSTRUCT(contuple);
9877+
conoid = currcon->oid;
9878+
refrelid = currcon->confrelid;
9879+
9880+
/*
9881+
* Update pg_constraint with the flags from cmdcon.
9882+
*
9883+
* If called to modify a constraint that's already in the desired state,
9884+
* silently do nothing.
9885+
*/
97769886
if (currcon->condeferrable != cmdcon->deferrable ||
97779887
currcon->condeferred != cmdcon->initdeferred)
97789888
{
97799889
HeapTuple copyTuple;
9780-
HeapTuple tgtuple;
97819890
Form_pg_constraint copy_con;
9782-
List *otherrelids = NIL;
9891+
HeapTuple tgtuple;
97839892
ScanKeyData tgkey;
97849893
SysScanDesc tgscan;
9785-
Relation tgrel;
9786-
ListCell *lc;
97879894

9788-
/*
9789-
* Now update the catalog, while we have the door open.
9790-
*/
97919895
copyTuple = heap_copytuple(contuple);
97929896
copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
97939897
copy_con->condeferrable = cmdcon->deferrable;
97949898
copy_con->condeferred = cmdcon->initdeferred;
97959899
CatalogTupleUpdate(conrel, &copyTuple->t_self, copyTuple);
97969900

97979901
InvokeObjectPostAlterHook(ConstraintRelationId,
9798-
currcon->oid, 0);
9902+
conoid, 0);
97999903

98009904
heap_freetuple(copyTuple);
9905+
changed = true;
9906+
9907+
/* Make new constraint flags visible to others */
9908+
CacheInvalidateRelcache(rel);
98019909

98029910
/*
98039911
* Now we need to update the multiple entries in pg_trigger that
98049912
* implement the constraint.
98059913
*/
9806-
tgrel = table_open(TriggerRelationId, RowExclusiveLock);
9807-
98089914
ScanKeyInit(&tgkey,
98099915
Anum_pg_trigger_tgconstraint,
98109916
BTEqualStrategyNumber, F_OIDEQ,
9811-
ObjectIdGetDatum(currcon->oid));
9812-
9917+
ObjectIdGetDatum(conoid));
98139918
tgscan = systable_beginscan(tgrel, TriggerConstraintIndexId, true,
98149919
NULL, 1, &tgkey);
9815-
98169920
while (HeapTupleIsValid(tgtuple = systable_getnext(tgscan)))
98179921
{
98189922
Form_pg_trigger tgform = (Form_pg_trigger) GETSTRUCT(tgtuple);
98199923
Form_pg_trigger copy_tg;
9924+
HeapTuple copyTuple;
98209925

98219926
/*
98229927
* Remember OIDs of other relation(s) involved in FK constraint.
@@ -9825,8 +9930,8 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd,
98259930
* change, but let's be conservative.)
98269931
*/
98279932
if (tgform->tgrelid != RelationGetRelid(rel))
9828-
otherrelids = list_append_unique_oid(otherrelids,
9829-
tgform->tgrelid);
9933+
*otherrelids = list_append_unique_oid(*otherrelids,
9934+
tgform->tgrelid);
98309935

98319936
/*
98329937
* Update deferrability of RI_FKey_noaction_del,
@@ -9853,31 +9958,46 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd,
98539958
}
98549959

98559960
systable_endscan(tgscan);
9961+
}
98569962

9857-
table_close(tgrel, RowExclusiveLock);
9963+
/*
9964+
* If the table at either end of the constraint is partitioned, we need to
9965+
* recurse and handle every constraint that is a child of this one.
9966+
*
9967+
* (This assumes that the recurse flag is forcibly set for partitioned
9968+
* tables, and not set for legacy inheritance, though we don't check for
9969+
* that here.)
9970+
*/
9971+
if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
9972+
get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)
9973+
{
9974+
ScanKeyData pkey;
9975+
SysScanDesc pscan;
9976+
HeapTuple childtup;
98589977

9859-
/*
9860-
* Invalidate relcache so that others see the new attributes. We must
9861-
* inval both the named rel and any others having relevant triggers.
9862-
* (At present there should always be exactly one other rel, but
9863-
* there's no need to hard-wire such an assumption here.)
9864-
*/
9865-
CacheInvalidateRelcache(rel);
9866-
foreach(lc, otherrelids)
9978+
ScanKeyInit(&pkey,
9979+
Anum_pg_constraint_conparentid,
9980+
BTEqualStrategyNumber, F_OIDEQ,
9981+
ObjectIdGetDatum(conoid));
9982+
9983+
pscan = systable_beginscan(conrel, ConstraintParentIndexId,
9984+
true, NULL, 1, &pkey);
9985+
9986+
while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
98679987
{
9868-
CacheInvalidateRelcacheByRelid(lfirst_oid(lc));
9988+
Form_pg_constraint childcon = (Form_pg_constraint) GETSTRUCT(childtup);
9989+
Relation childrel;
9990+
9991+
childrel = table_open(childcon->conrelid, lockmode);
9992+
ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, childrel, childtup,
9993+
otherrelids, lockmode);
9994+
table_close(childrel, NoLock);
98699995
}
98709996

9871-
ObjectAddressSet(address, ConstraintRelationId, currcon->oid);
9997+
systable_endscan(pscan);
98729998
}
9873-
else
9874-
address = InvalidObjectAddress;
98759999

9876-
systable_endscan(scan);
9877-
9878-
table_close(conrel, RowExclusiveLock);
9879-
9880-
return address;
10000+
return changed;
988110001
}
988210002

988310003
/*

src/test/regress/expected/foreign_key.out

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2313,6 +2313,84 @@ SET CONSTRAINTS fk_a_fkey DEFERRED;
23132313
DELETE FROM pk WHERE a = 1;
23142314
DELETE FROM fk WHERE a = 1;
23152315
COMMIT; -- OK
2316+
-- Verify constraint deferrability when changed by ALTER
2317+
-- Partitioned table at referencing end
2318+
CREATE TABLE pt(f1 int, f2 int, f3 int, PRIMARY KEY(f1,f2));
2319+
CREATE TABLE ref(f1 int, f2 int, f3 int)
2320+
PARTITION BY list(f1);
2321+
CREATE TABLE ref1 PARTITION OF ref FOR VALUES IN (1);
2322+
CREATE TABLE ref2 PARTITION OF ref FOR VALUES in (2);
2323+
ALTER TABLE ref ADD FOREIGN KEY(f1,f2) REFERENCES pt;
2324+
ALTER TABLE ref ALTER CONSTRAINT ref_f1_f2_fkey
2325+
DEFERRABLE INITIALLY DEFERRED;
2326+
INSERT INTO pt VALUES(1,2,3);
2327+
INSERT INTO ref VALUES(1,2,3);
2328+
BEGIN;
2329+
DELETE FROM pt;
2330+
DELETE FROM ref;
2331+
ABORT;
2332+
DROP TABLE pt, ref;
2333+
-- Multi-level partitioning at referencing end
2334+
CREATE TABLE pt(f1 int, f2 int, f3 int, PRIMARY KEY(f1,f2));
2335+
CREATE TABLE ref(f1 int, f2 int, f3 int)
2336+
PARTITION BY list(f1);
2337+
CREATE TABLE ref1_2 PARTITION OF ref FOR VALUES IN (1, 2) PARTITION BY list (f2);
2338+
CREATE TABLE ref1 PARTITION OF ref1_2 FOR VALUES IN (1);
2339+
CREATE TABLE ref2 PARTITION OF ref1_2 FOR VALUES IN (2) PARTITION BY list (f2);
2340+
CREATE TABLE ref22 PARTITION OF ref2 FOR VALUES IN (2);
2341+
ALTER TABLE ref ADD FOREIGN KEY(f1,f2) REFERENCES pt;
2342+
INSERT INTO pt VALUES(1,2,3);
2343+
INSERT INTO ref VALUES(1,2,3);
2344+
ALTER TABLE ref22 ALTER CONSTRAINT ref_f1_f2_fkey
2345+
DEFERRABLE INITIALLY IMMEDIATE; -- fails
2346+
ERROR: cannot alter constraint "ref_f1_f2_fkey" on relation "ref22"
2347+
DETAIL: Constraint "ref_f1_f2_fkey" is derived from constraint "ref_f1_f2_fkey" of relation "ref".
2348+
HINT: You may alter the constraint it derives from, instead.
2349+
ALTER TABLE ref ALTER CONSTRAINT ref_f1_f2_fkey
2350+
DEFERRABLE INITIALLY DEFERRED;
2351+
BEGIN;
2352+
DELETE FROM pt;
2353+
DELETE FROM ref;
2354+
ABORT;
2355+
DROP TABLE pt, ref;
2356+
-- Partitioned table at referenced end
2357+
CREATE TABLE pt(f1 int, f2 int, f3 int, PRIMARY KEY(f1,f2))
2358+
PARTITION BY LIST(f1);
2359+
CREATE TABLE pt1 PARTITION OF pt FOR VALUES IN (1);
2360+
CREATE TABLE pt2 PARTITION OF pt FOR VALUES IN (2);
2361+
CREATE TABLE ref(f1 int, f2 int, f3 int);
2362+
ALTER TABLE ref ADD FOREIGN KEY(f1,f2) REFERENCES pt;
2363+
ALTER TABLE ref ALTER CONSTRAINT ref_f1_f2_fkey
2364+
DEFERRABLE INITIALLY DEFERRED;
2365+
INSERT INTO pt VALUES(1,2,3);
2366+
INSERT INTO ref VALUES(1,2,3);
2367+
BEGIN;
2368+
DELETE FROM pt;
2369+
DELETE FROM ref;
2370+
ABORT;
2371+
DROP TABLE pt, ref;
2372+
-- Multi-level partitioning at at referenced end
2373+
CREATE TABLE pt(f1 int, f2 int, f3 int, PRIMARY KEY(f1,f2))
2374+
PARTITION BY LIST(f1);
2375+
CREATE TABLE pt1_2 PARTITION OF pt FOR VALUES IN (1, 2) PARTITION BY LIST (f1);
2376+
CREATE TABLE pt1 PARTITION OF pt1_2 FOR VALUES IN (1);
2377+
CREATE TABLE pt2 PARTITION OF pt1_2 FOR VALUES IN (2);
2378+
CREATE TABLE ref(f1 int, f2 int, f3 int);
2379+
ALTER TABLE ref ADD FOREIGN KEY(f1,f2) REFERENCES pt;
2380+
ALTER TABLE ref ALTER CONSTRAINT ref_f1_f2_fkey1
2381+
DEFERRABLE INITIALLY DEFERRED; -- fails
2382+
ERROR: cannot alter constraint "ref_f1_f2_fkey1" on relation "ref"
2383+
DETAIL: Constraint "ref_f1_f2_fkey1" is derived from constraint "ref_f1_f2_fkey" of relation "ref".
2384+
HINT: You may alter the constraint it derives from, instead.
2385+
ALTER TABLE ref ALTER CONSTRAINT ref_f1_f2_fkey
2386+
DEFERRABLE INITIALLY DEFERRED;
2387+
INSERT INTO pt VALUES(1,2,3);
2388+
INSERT INTO ref VALUES(1,2,3);
2389+
BEGIN;
2390+
DELETE FROM pt;
2391+
DELETE FROM ref;
2392+
ABORT;
2393+
DROP TABLE pt, ref;
23162394
DROP SCHEMA fkpart9 CASCADE;
23172395
NOTICE: drop cascades to 2 other objects
23182396
DETAIL: drop cascades to table pk

0 commit comments

Comments
 (0)