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

Reply via email to