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

Commit abce26c

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 72fabd4 commit abce26c

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
@@ -325,6 +325,9 @@ static void AlterSeqNamespaces(Relation classRel, Relation rel,
325325
LOCKMODE lockmode);
326326
static ObjectAddress ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd,
327327
bool recurse, bool recursing, LOCKMODE lockmode);
328+
static bool ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
329+
Relation rel, HeapTuple contuple, List **otherrelids,
330+
LOCKMODE lockmode);
328331
static ObjectAddress ATExecValidateConstraint(List **wqueue,
329332
Relation rel, char *constrName,
330333
bool recurse, bool recursing, LOCKMODE lockmode);
@@ -4212,6 +4215,7 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
42124215
break;
42134216
case AT_AlterConstraint: /* ALTER CONSTRAINT */
42144217
ATSimplePermissions(rel, ATT_TABLE);
4218+
/* Recursion occurs during execution phase */
42154219
pass = AT_PASS_MISC;
42164220
break;
42174221
case AT_ValidateConstraint: /* VALIDATE CONSTRAINT */
@@ -9268,28 +9272,29 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
92689272
* Update the attributes of a constraint.
92699273
*
92709274
* Currently only works for Foreign Key constraints.
9271-
* Foreign keys do not inherit, so we purposely ignore the
9272-
* recursion bit here, but we keep the API the same for when
9273-
* other constraint types are supported.
92749275
*
92759276
* If the constraint is modified, returns its address; otherwise, return
92769277
* InvalidObjectAddress.
92779278
*/
92789279
static ObjectAddress
9279-
ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd,
9280-
bool recurse, bool recursing, LOCKMODE lockmode)
9280+
ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
9281+
bool recursing, LOCKMODE lockmode)
92819282
{
92829283
Constraint *cmdcon;
92839284
Relation conrel;
9285+
Relation tgrel;
92849286
SysScanDesc scan;
92859287
ScanKeyData skey[3];
92869288
HeapTuple contuple;
92879289
Form_pg_constraint currcon;
92889290
ObjectAddress address;
9291+
List *otherrelids = NIL;
9292+
ListCell *lc;
92899293

92909294
cmdcon = castNode(Constraint, cmd->def);
92919295

92929296
conrel = table_open(ConstraintRelationId, RowExclusiveLock);
9297+
tgrel = table_open(TriggerRelationId, RowExclusiveLock);
92939298

92949299
/*
92959300
* Find and check the target constraint
@@ -9323,50 +9328,150 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd,
93239328
errmsg("constraint \"%s\" of relation \"%s\" is not a foreign key constraint",
93249329
cmdcon->conname, RelationGetRelationName(rel))));
93259330

9331+
/*
9332+
* If it's not the topmost constraint, raise an error.
9333+
*
9334+
* Altering a non-topmost constraint leaves some triggers untouched, since
9335+
* they are not directly connected to this constraint; also, pg_dump would
9336+
* ignore the deferrability status of the individual constraint, since it
9337+
* only dumps topmost constraints. Avoid these problems by refusing this
9338+
* operation and telling the user to alter the parent constraint instead.
9339+
*/
9340+
if (OidIsValid(currcon->conparentid))
9341+
{
9342+
HeapTuple tp;
9343+
Oid parent = currcon->conparentid;
9344+
char *ancestorname = NULL;
9345+
char *ancestortable = NULL;
9346+
9347+
/* Loop to find the topmost constraint */
9348+
while (HeapTupleIsValid(tp = SearchSysCache1(CONSTROID, ObjectIdGetDatum(parent))))
9349+
{
9350+
Form_pg_constraint contup = (Form_pg_constraint) GETSTRUCT(tp);
9351+
9352+
/* If no parent, this is the constraint we want */
9353+
if (!OidIsValid(contup->conparentid))
9354+
{
9355+
ancestorname = pstrdup(NameStr(contup->conname));
9356+
ancestortable = get_rel_name(contup->conrelid);
9357+
ReleaseSysCache(tp);
9358+
break;
9359+
}
9360+
9361+
parent = contup->conparentid;
9362+
ReleaseSysCache(tp);
9363+
}
9364+
9365+
ereport(ERROR,
9366+
(errmsg("cannot alter constraint \"%s\" on relation \"%s\"",
9367+
cmdcon->conname, RelationGetRelationName(rel)),
9368+
ancestorname && ancestortable ?
9369+
errdetail("Constraint \"%s\" is derived from constraint \"%s\" of relation \"%s\".",
9370+
cmdcon->conname, ancestorname, ancestortable) : 0,
9371+
errhint("You may alter the constraint it derives from, instead.")));
9372+
}
9373+
9374+
/*
9375+
* Do the actual catalog work. We can skip changing if already in the
9376+
* desired state, but not if a partitioned table: partitions need to be
9377+
* processed regardless, in case they had the constraint locally changed.
9378+
*/
9379+
address = InvalidObjectAddress;
9380+
if (currcon->condeferrable != cmdcon->deferrable ||
9381+
currcon->condeferred != cmdcon->initdeferred ||
9382+
rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
9383+
{
9384+
if (ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, rel, contuple,
9385+
&otherrelids, lockmode))
9386+
ObjectAddressSet(address, ConstraintRelationId, currcon->oid);
9387+
}
9388+
9389+
/*
9390+
* ATExecConstrRecurse already invalidated relcache for the relations
9391+
* having the constraint itself; here we also invalidate for relations
9392+
* that have any triggers that are part of the constraint.
9393+
*/
9394+
foreach(lc, otherrelids)
9395+
CacheInvalidateRelcacheByRelid(lfirst_oid(lc));
9396+
9397+
systable_endscan(scan);
9398+
9399+
table_close(tgrel, RowExclusiveLock);
9400+
table_close(conrel, RowExclusiveLock);
9401+
9402+
return address;
9403+
}
9404+
9405+
/*
9406+
* Recursive subroutine of ATExecAlterConstraint. Returns true if the
9407+
* constraint is altered.
9408+
*
9409+
* *otherrelids is appended OIDs of relations containing affected triggers.
9410+
*
9411+
* Note that we must recurse even when the values are correct, in case
9412+
* indirect descendants have had their constraints altered locally.
9413+
* (This could be avoided if we forbade altering constraints in partitions
9414+
* but existing releases don't do that.)
9415+
*/
9416+
static bool
9417+
ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
9418+
Relation rel, HeapTuple contuple, List **otherrelids,
9419+
LOCKMODE lockmode)
9420+
{
9421+
Form_pg_constraint currcon;
9422+
Oid conoid;
9423+
Oid refrelid;
9424+
bool changed = false;
9425+
9426+
currcon = (Form_pg_constraint) GETSTRUCT(contuple);
9427+
conoid = currcon->oid;
9428+
refrelid = currcon->confrelid;
9429+
9430+
/*
9431+
* Update pg_constraint with the flags from cmdcon.
9432+
*
9433+
* If called to modify a constraint that's already in the desired state,
9434+
* silently do nothing.
9435+
*/
93269436
if (currcon->condeferrable != cmdcon->deferrable ||
93279437
currcon->condeferred != cmdcon->initdeferred)
93289438
{
93299439
HeapTuple copyTuple;
9330-
HeapTuple tgtuple;
93319440
Form_pg_constraint copy_con;
9332-
List *otherrelids = NIL;
9441+
HeapTuple tgtuple;
93339442
ScanKeyData tgkey;
93349443
SysScanDesc tgscan;
9335-
Relation tgrel;
9336-
ListCell *lc;
93379444

9338-
/*
9339-
* Now update the catalog, while we have the door open.
9340-
*/
93419445
copyTuple = heap_copytuple(contuple);
93429446
copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
93439447
copy_con->condeferrable = cmdcon->deferrable;
93449448
copy_con->condeferred = cmdcon->initdeferred;
93459449
CatalogTupleUpdate(conrel, &copyTuple->t_self, copyTuple);
93469450

93479451
InvokeObjectPostAlterHook(ConstraintRelationId,
9348-
currcon->oid, 0);
9452+
conoid, 0);
93499453

93509454
heap_freetuple(copyTuple);
9455+
changed = true;
9456+
9457+
/* Make new constraint flags visible to others */
9458+
CacheInvalidateRelcache(rel);
93519459

93529460
/*
93539461
* Now we need to update the multiple entries in pg_trigger that
93549462
* implement the constraint.
93559463
*/
9356-
tgrel = table_open(TriggerRelationId, RowExclusiveLock);
9357-
93589464
ScanKeyInit(&tgkey,
93599465
Anum_pg_trigger_tgconstraint,
93609466
BTEqualStrategyNumber, F_OIDEQ,
9361-
ObjectIdGetDatum(currcon->oid));
9362-
9467+
ObjectIdGetDatum(conoid));
93639468
tgscan = systable_beginscan(tgrel, TriggerConstraintIndexId, true,
93649469
NULL, 1, &tgkey);
9365-
93669470
while (HeapTupleIsValid(tgtuple = systable_getnext(tgscan)))
93679471
{
93689472
Form_pg_trigger tgform = (Form_pg_trigger) GETSTRUCT(tgtuple);
93699473
Form_pg_trigger copy_tg;
9474+
HeapTuple copyTuple;
93709475

93719476
/*
93729477
* Remember OIDs of other relation(s) involved in FK constraint.
@@ -9375,8 +9480,8 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd,
93759480
* change, but let's be conservative.)
93769481
*/
93779482
if (tgform->tgrelid != RelationGetRelid(rel))
9378-
otherrelids = list_append_unique_oid(otherrelids,
9379-
tgform->tgrelid);
9483+
*otherrelids = list_append_unique_oid(*otherrelids,
9484+
tgform->tgrelid);
93809485

93819486
/*
93829487
* Update deferrability of RI_FKey_noaction_del,
@@ -9403,31 +9508,46 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd,
94039508
}
94049509

94059510
systable_endscan(tgscan);
9511+
}
94069512

9407-
table_close(tgrel, RowExclusiveLock);
9513+
/*
9514+
* If the table at either end of the constraint is partitioned, we need to
9515+
* recurse and handle every constraint that is a child of this one.
9516+
*
9517+
* (This assumes that the recurse flag is forcibly set for partitioned
9518+
* tables, and not set for legacy inheritance, though we don't check for
9519+
* that here.)
9520+
*/
9521+
if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
9522+
get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)
9523+
{
9524+
ScanKeyData pkey;
9525+
SysScanDesc pscan;
9526+
HeapTuple childtup;
94089527

9409-
/*
9410-
* Invalidate relcache so that others see the new attributes. We must
9411-
* inval both the named rel and any others having relevant triggers.
9412-
* (At present there should always be exactly one other rel, but
9413-
* there's no need to hard-wire such an assumption here.)
9414-
*/
9415-
CacheInvalidateRelcache(rel);
9416-
foreach(lc, otherrelids)
9528+
ScanKeyInit(&pkey,
9529+
Anum_pg_constraint_conparentid,
9530+
BTEqualStrategyNumber, F_OIDEQ,
9531+
ObjectIdGetDatum(conoid));
9532+
9533+
pscan = systable_beginscan(conrel, ConstraintParentIndexId,
9534+
true, NULL, 1, &pkey);
9535+
9536+
while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
94179537
{
9418-
CacheInvalidateRelcacheByRelid(lfirst_oid(lc));
9538+
Form_pg_constraint childcon = (Form_pg_constraint) GETSTRUCT(childtup);
9539+
Relation childrel;
9540+
9541+
childrel = table_open(childcon->conrelid, lockmode);
9542+
ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, childrel, childtup,
9543+
otherrelids, lockmode);
9544+
table_close(childrel, NoLock);
94199545
}
94209546

9421-
ObjectAddressSet(address, ConstraintRelationId, currcon->oid);
9547+
systable_endscan(pscan);
94229548
}
9423-
else
9424-
address = InvalidObjectAddress;
94259549

9426-
systable_endscan(scan);
9427-
9428-
table_close(conrel, RowExclusiveLock);
9429-
9430-
return address;
9550+
return changed;
94319551
}
94329552

94339553
/*

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)