Skip to content

Commit a25b98d

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 4fd7c79 commit a25b98d

File tree

3 files changed

+240
-36
lines changed

3 files changed

+240
-36
lines changed

src/backend/commands/tablecmds.c

Lines changed: 154 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,9 @@ static void AlterSeqNamespaces(Relation classRel, Relation rel,
322322
LOCKMODE lockmode);
323323
static ObjectAddress ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd,
324324
bool recurse, bool recursing, LOCKMODE lockmode);
325+
static bool ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel,
326+
Relation tgrel, Relation rel, HeapTuple contuple,
327+
List **otherrelids, LOCKMODE lockmode);
325328
static ObjectAddress ATExecValidateConstraint(List **wqueue, Relation rel,
326329
char *constrName, bool recurse, bool recursing,
327330
LOCKMODE lockmode);
@@ -3953,6 +3956,7 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
39533956
break;
39543957
case AT_AlterConstraint: /* ALTER CONSTRAINT */
39553958
ATSimplePermissions(rel, ATT_TABLE);
3959+
/* Recursion occurs during execution phase */
39563960
pass = AT_PASS_MISC;
39573961
break;
39583962
case AT_ValidateConstraint: /* VALIDATE CONSTRAINT */
@@ -8311,28 +8315,29 @@ CloneFkReferencing(Relation pg_constraint, Relation parentRel,
83118315
* Update the attributes of a constraint.
83128316
*
83138317
* Currently only works for Foreign Key constraints.
8314-
* Foreign keys do not inherit, so we purposely ignore the
8315-
* recursion bit here, but we keep the API the same for when
8316-
* other constraint types are supported.
83178318
*
83188319
* If the constraint is modified, returns its address; otherwise, return
83198320
* InvalidObjectAddress.
83208321
*/
83218322
static ObjectAddress
8322-
ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd,
8323-
bool recurse, bool recursing, LOCKMODE lockmode)
8323+
ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
8324+
bool recursing, LOCKMODE lockmode)
83248325
{
83258326
Constraint *cmdcon;
83268327
Relation conrel;
8328+
Relation tgrel;
83278329
SysScanDesc scan;
83288330
ScanKeyData skey[3];
83298331
HeapTuple contuple;
83308332
Form_pg_constraint currcon;
83318333
ObjectAddress address;
8334+
List *otherrelids = NIL;
8335+
ListCell *lc;
83328336

83338337
cmdcon = castNode(Constraint, cmd->def);
83348338

83358339
conrel = heap_open(ConstraintRelationId, RowExclusiveLock);
8340+
tgrel = heap_open(TriggerRelationId, RowExclusiveLock);
83368341

83378342
/*
83388343
* Find and check the target constraint
@@ -8366,17 +8371,118 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd,
83668371
errmsg("constraint \"%s\" of relation \"%s\" is not a foreign key constraint",
83678372
cmdcon->conname, RelationGetRelationName(rel))));
83688373

8374+
/*
8375+
* If it's not the topmost constraint, raise an error.
8376+
*
8377+
* Altering a non-topmost constraint leaves some triggers untouched, since
8378+
* they are not directly connected to this constraint; also, pg_dump would
8379+
* ignore the deferrability status of the individual constraint, since it
8380+
* only dumps topmost constraints. Avoid these problems by refusing this
8381+
* operation and telling the user to alter the parent constraint instead.
8382+
*/
8383+
if (OidIsValid(currcon->conparentid))
8384+
{
8385+
HeapTuple tp;
8386+
Oid parent = currcon->conparentid;
8387+
char *ancestorname = NULL;
8388+
char *ancestortable = NULL;
8389+
8390+
/* Loop to find the topmost constraint */
8391+
while (HeapTupleIsValid(tp = SearchSysCache1(CONSTROID, ObjectIdGetDatum(parent))))
8392+
{
8393+
Form_pg_constraint contup = (Form_pg_constraint) GETSTRUCT(tp);
8394+
8395+
/* If no parent, this is the constraint we want */
8396+
if (!OidIsValid(contup->conparentid))
8397+
{
8398+
ancestorname = pstrdup(NameStr(contup->conname));
8399+
ancestortable = get_rel_name(contup->conrelid);
8400+
ReleaseSysCache(tp);
8401+
break;
8402+
}
8403+
8404+
parent = contup->conparentid;
8405+
ReleaseSysCache(tp);
8406+
}
8407+
8408+
ereport(ERROR,
8409+
(errmsg("cannot alter constraint \"%s\" on relation \"%s\"",
8410+
cmdcon->conname, RelationGetRelationName(rel)),
8411+
ancestorname && ancestortable ?
8412+
errdetail("Constraint \"%s\" is derived from constraint \"%s\" of relation \"%s\".",
8413+
cmdcon->conname, ancestorname, ancestortable) : 0,
8414+
errhint("You may alter the constraint it derives from, instead.")));
8415+
}
8416+
8417+
/*
8418+
* Do the actual catalog work. We can skip changing if already in the
8419+
* desired state, but not if a partitioned table: partitions need to be
8420+
* processed regardless, in case they've had it locally changed.
8421+
*/
8422+
address = InvalidObjectAddress;
8423+
if (currcon->condeferrable != cmdcon->deferrable ||
8424+
currcon->condeferred != cmdcon->initdeferred ||
8425+
rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
8426+
{
8427+
if (ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, rel, contuple,
8428+
&otherrelids, lockmode))
8429+
ObjectAddressSet(address, ConstraintRelationId,
8430+
HeapTupleGetOid(contuple));
8431+
}
8432+
8433+
/*
8434+
* ATExecConstrRecurse already invalidated relcache for the relations
8435+
* having the constraint itself; here we also invalidate for relations
8436+
* that have any triggers that implement the constraint.
8437+
*/
8438+
foreach(lc, otherrelids)
8439+
CacheInvalidateRelcacheByRelid(lfirst_oid(lc));
8440+
8441+
systable_endscan(scan);
8442+
8443+
heap_close(conrel, RowExclusiveLock);
8444+
heap_close(tgrel, RowExclusiveLock);
8445+
8446+
return address;
8447+
}
8448+
8449+
/*
8450+
* Recursive subroutine of ATExecAlterConstraint. Returns true if the
8451+
* constraint is altered.
8452+
*
8453+
* *otherrelids is appended OIDs of relations containing affected triggers.
8454+
*
8455+
* Note that we must recurse even when the values are correct, in case
8456+
* indirect descendants have had their constraints altered locally.
8457+
* (This could be avoided if we forbade altering constraints in partitions
8458+
* but existing releases don't do that.)
8459+
*/
8460+
static bool
8461+
ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
8462+
Relation rel, HeapTuple contuple, List **otherrelids,
8463+
LOCKMODE lockmode)
8464+
{
8465+
Form_pg_constraint currcon;
8466+
Oid conoid;
8467+
bool changed = false;
8468+
8469+
currcon = (Form_pg_constraint) GETSTRUCT(contuple);
8470+
conoid = HeapTupleGetOid(contuple);
8471+
8472+
/*
8473+
* Update pg_constraint with the flags from cmdcon.
8474+
*
8475+
* If called to modify a constraint that's already in the desired state,
8476+
* silently do nothing.
8477+
*/
83698478
if (currcon->condeferrable != cmdcon->deferrable ||
83708479
currcon->condeferred != cmdcon->initdeferred)
83718480
{
83728481
HeapTuple copyTuple;
8373-
HeapTuple tgtuple;
83748482
Form_pg_constraint copy_con;
8375-
List *otherrelids = NIL;
8483+
HeapTuple tgtuple;
83768484
ScanKeyData tgkey;
83778485
SysScanDesc tgscan;
8378-
Relation tgrel;
8379-
ListCell *lc;
83808486

83818487
/*
83828488
* Now update the catalog, while we have the door open.
@@ -8391,25 +8497,26 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd,
83918497
HeapTupleGetOid(contuple), 0);
83928498

83938499
heap_freetuple(copyTuple);
8500+
changed = true;
8501+
8502+
/* Make new constraint flags visible to others */
8503+
CacheInvalidateRelcache(rel);
83948504

83958505
/*
83968506
* Now we need to update the multiple entries in pg_trigger that
83978507
* implement the constraint.
83988508
*/
8399-
tgrel = heap_open(TriggerRelationId, RowExclusiveLock);
8400-
84018509
ScanKeyInit(&tgkey,
84028510
Anum_pg_trigger_tgconstraint,
84038511
BTEqualStrategyNumber, F_OIDEQ,
84048512
ObjectIdGetDatum(HeapTupleGetOid(contuple)));
8405-
84068513
tgscan = systable_beginscan(tgrel, TriggerConstraintIndexId, true,
84078514
NULL, 1, &tgkey);
8408-
84098515
while (HeapTupleIsValid(tgtuple = systable_getnext(tgscan)))
84108516
{
84118517
Form_pg_trigger tgform = (Form_pg_trigger) GETSTRUCT(tgtuple);
84128518
Form_pg_trigger copy_tg;
8519+
HeapTuple copyTuple;
84138520

84148521
/*
84158522
* Remember OIDs of other relation(s) involved in FK constraint.
@@ -8418,8 +8525,8 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd,
84188525
* change, but let's be conservative.)
84198526
*/
84208527
if (tgform->tgrelid != RelationGetRelid(rel))
8421-
otherrelids = list_append_unique_oid(otherrelids,
8422-
tgform->tgrelid);
8528+
*otherrelids = list_append_unique_oid(*otherrelids,
8529+
tgform->tgrelid);
84238530

84248531
/*
84258532
* Update deferrability of RI_FKey_noaction_del,
@@ -8447,32 +8554,45 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd,
84478554
}
84488555

84498556
systable_endscan(tgscan);
8557+
}
84508558

8451-
heap_close(tgrel, RowExclusiveLock);
8559+
/*
8560+
* If the referencing table is partitioned, we need to recurse and handle
8561+
* every constraint that is a child of this one.
8562+
*
8563+
* (This assumes that the recurse flag is forcibly set for partitioned
8564+
* tables, and not set for legacy inheritance, though we don't check for
8565+
* that here.)
8566+
*/
8567+
if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
8568+
{
8569+
ScanKeyData pkey;
8570+
SysScanDesc pscan;
8571+
HeapTuple childtup;
84528572

8453-
/*
8454-
* Invalidate relcache so that others see the new attributes. We must
8455-
* inval both the named rel and any others having relevant triggers.
8456-
* (At present there should always be exactly one other rel, but
8457-
* there's no need to hard-wire such an assumption here.)
8458-
*/
8459-
CacheInvalidateRelcache(rel);
8460-
foreach(lc, otherrelids)
8573+
ScanKeyInit(&pkey,
8574+
Anum_pg_constraint_conparentid,
8575+
BTEqualStrategyNumber, F_OIDEQ,
8576+
ObjectIdGetDatum(conoid));
8577+
8578+
pscan = systable_beginscan(conrel, ConstraintParentIndexId,
8579+
true, NULL, 1, &pkey);
8580+
8581+
while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
84618582
{
8462-
CacheInvalidateRelcacheByRelid(lfirst_oid(lc));
8583+
Form_pg_constraint childcon = (Form_pg_constraint) GETSTRUCT(childtup);
8584+
Relation childrel;
8585+
8586+
childrel = heap_open(childcon->conrelid, lockmode);
8587+
ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, childrel, childtup,
8588+
otherrelids, lockmode);
8589+
heap_close(childrel, NoLock);
84638590
}
84648591

8465-
ObjectAddressSet(address, ConstraintRelationId,
8466-
HeapTupleGetOid(contuple));
8592+
systable_endscan(pscan);
84678593
}
8468-
else
8469-
address = InvalidObjectAddress;
84708594

8471-
systable_endscan(scan);
8472-
8473-
heap_close(conrel, RowExclusiveLock);
8474-
8475-
return address;
8595+
return changed;
84768596
}
84778597

84788598
/*

src/test/regress/expected/foreign_key.out

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1954,7 +1954,50 @@ INSERT INTO fkpart8.tbl2 VALUES(1);
19541954
ALTER TABLE fkpart8.tbl2 DROP CONSTRAINT tbl2_f1_fkey;
19551955
ERROR: cannot ALTER TABLE "tbl2_p1" because it has pending trigger events
19561956
COMMIT;
1957+
CREATE SCHEMA fkpart9;
1958+
SET search_path TO fkpart9;
1959+
-- Verify constraint deferrability when changed by ALTER
1960+
-- Partitioned table at referencing end
1961+
CREATE TABLE pt(f1 int, f2 int, f3 int, PRIMARY KEY(f1,f2));
1962+
CREATE TABLE ref(f1 int, f2 int, f3 int)
1963+
PARTITION BY list(f1);
1964+
CREATE TABLE ref1 PARTITION OF ref FOR VALUES IN (1);
1965+
CREATE TABLE ref2 PARTITION OF ref FOR VALUES in (2);
1966+
ALTER TABLE ref ADD FOREIGN KEY(f1,f2) REFERENCES pt;
1967+
ALTER TABLE ref ALTER CONSTRAINT ref_f1_fkey
1968+
DEFERRABLE INITIALLY DEFERRED;
1969+
INSERT INTO pt VALUES(1,2,3);
1970+
INSERT INTO ref VALUES(1,2,3);
1971+
BEGIN;
1972+
DELETE FROM pt;
1973+
DELETE FROM ref;
1974+
ABORT;
1975+
DROP TABLE pt, ref;
1976+
-- Multi-level partitioning at referencing end
1977+
CREATE TABLE pt(f1 int, f2 int, f3 int, PRIMARY KEY(f1,f2));
1978+
CREATE TABLE ref(f1 int, f2 int, f3 int)
1979+
PARTITION BY list(f1);
1980+
CREATE TABLE ref1_2 PARTITION OF ref FOR VALUES IN (1, 2) PARTITION BY list (f2);
1981+
CREATE TABLE ref1 PARTITION OF ref1_2 FOR VALUES IN (1);
1982+
CREATE TABLE ref2 PARTITION OF ref1_2 FOR VALUES IN (2) PARTITION BY list (f2);
1983+
CREATE TABLE ref22 PARTITION OF ref2 FOR VALUES IN (2);
1984+
ALTER TABLE ref ADD FOREIGN KEY(f1,f2) REFERENCES pt;
1985+
INSERT INTO pt VALUES(1,2,3);
1986+
INSERT INTO ref VALUES(1,2,3);
1987+
ALTER TABLE ref22 ALTER CONSTRAINT ref_f1_fkey
1988+
DEFERRABLE INITIALLY IMMEDIATE; -- fails
1989+
ERROR: cannot alter constraint "ref_f1_fkey" on relation "ref22"
1990+
DETAIL: Constraint "ref_f1_fkey" is derived from constraint "ref_f1_fkey" of relation "ref".
1991+
HINT: You may alter the constraint it derives from, instead.
1992+
ALTER TABLE ref ALTER CONSTRAINT ref_f1_fkey
1993+
DEFERRABLE INITIALLY DEFERRED;
1994+
BEGIN;
1995+
DELETE FROM pt;
1996+
DELETE FROM ref;
1997+
ABORT;
1998+
DROP TABLE pt, ref;
1999+
RESET search_path;
19572000
\set VERBOSITY terse \\ -- suppress cascade details
1958-
drop schema fkpart0, fkpart1, fkpart2, fkpart3, fkpart8 cascade;
2001+
drop schema fkpart0, fkpart1, fkpart2, fkpart3, fkpart8, fkpart9 cascade;
19592002
NOTICE: drop cascades to 12 other objects
19602003
\set VERBOSITY default

src/test/regress/sql/foreign_key.sql

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1409,6 +1409,47 @@ INSERT INTO fkpart8.tbl2 VALUES(1);
14091409
ALTER TABLE fkpart8.tbl2 DROP CONSTRAINT tbl2_f1_fkey;
14101410
COMMIT;
14111411

1412+
CREATE SCHEMA fkpart9;
1413+
SET search_path TO fkpart9;
1414+
-- Verify constraint deferrability when changed by ALTER
1415+
-- Partitioned table at referencing end
1416+
CREATE TABLE pt(f1 int, f2 int, f3 int, PRIMARY KEY(f1,f2));
1417+
CREATE TABLE ref(f1 int, f2 int, f3 int)
1418+
PARTITION BY list(f1);
1419+
CREATE TABLE ref1 PARTITION OF ref FOR VALUES IN (1);
1420+
CREATE TABLE ref2 PARTITION OF ref FOR VALUES in (2);
1421+
ALTER TABLE ref ADD FOREIGN KEY(f1,f2) REFERENCES pt;
1422+
ALTER TABLE ref ALTER CONSTRAINT ref_f1_fkey
1423+
DEFERRABLE INITIALLY DEFERRED;
1424+
INSERT INTO pt VALUES(1,2,3);
1425+
INSERT INTO ref VALUES(1,2,3);
1426+
BEGIN;
1427+
DELETE FROM pt;
1428+
DELETE FROM ref;
1429+
ABORT;
1430+
DROP TABLE pt, ref;
1431+
-- Multi-level partitioning at referencing end
1432+
CREATE TABLE pt(f1 int, f2 int, f3 int, PRIMARY KEY(f1,f2));
1433+
CREATE TABLE ref(f1 int, f2 int, f3 int)
1434+
PARTITION BY list(f1);
1435+
CREATE TABLE ref1_2 PARTITION OF ref FOR VALUES IN (1, 2) PARTITION BY list (f2);
1436+
CREATE TABLE ref1 PARTITION OF ref1_2 FOR VALUES IN (1);
1437+
CREATE TABLE ref2 PARTITION OF ref1_2 FOR VALUES IN (2) PARTITION BY list (f2);
1438+
CREATE TABLE ref22 PARTITION OF ref2 FOR VALUES IN (2);
1439+
ALTER TABLE ref ADD FOREIGN KEY(f1,f2) REFERENCES pt;
1440+
INSERT INTO pt VALUES(1,2,3);
1441+
INSERT INTO ref VALUES(1,2,3);
1442+
ALTER TABLE ref22 ALTER CONSTRAINT ref_f1_fkey
1443+
DEFERRABLE INITIALLY IMMEDIATE; -- fails
1444+
ALTER TABLE ref ALTER CONSTRAINT ref_f1_fkey
1445+
DEFERRABLE INITIALLY DEFERRED;
1446+
BEGIN;
1447+
DELETE FROM pt;
1448+
DELETE FROM ref;
1449+
ABORT;
1450+
DROP TABLE pt, ref;
1451+
RESET search_path;
1452+
14121453
\set VERBOSITY terse \\ -- suppress cascade details
1413-
drop schema fkpart0, fkpart1, fkpart2, fkpart3, fkpart8 cascade;
1454+
drop schema fkpart0, fkpart1, fkpart2, fkpart3, fkpart8, fkpart9 cascade;
14141455
\set VERBOSITY default

0 commit comments

Comments
 (0)