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.
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);