On Tue, Aug 23, 2022 at 8:07 AM Jehan-Guillaume de Rorthais <j...@dalibo.com>
wrote:

> Hi all,
>
> I've been able to work on this issue and isolate where in the code the
> oddity
> is laying.
>
> During ATExecAttachPartition(), AttachPartitionEnsureIndexes() look for
> existing
> required index on the partition to attach. It creates missing index, or
> sets the
> parent's index when a matching one exists on the partition. Good.
>
> When a matching index is found, if the parent index enforce a constraint,
> the
> function look for the similar constraint in the partition-to-be, and set
> the
> constraint parent as well:
>
>         constraintOid =
> get_relation_idx_constraint_oid(RelationGetRelid(rel),
>                                                         idx);
>
>         [...]
>
>         /*
>          * If this index is being created in the parent because of a
>          * constraint, then the child needs to have a constraint also,
>          * so look for one.  If there is no such constraint, this
>          * index is no good, so keep looking.
>          */
>         if (OidIsValid(constraintOid))
>         {
>                 cldConstrOid = get_relation_idx_constraint_oid(
>                                         RelationGetRelid(attachrel),
>                                         cldIdxId);
>                 /* no dice */
>                 if (!OidIsValid(cldConstrOid))
>                         continue;
>          }
>          /* bingo. */
>          IndexSetParentIndex(attachrelIdxRels[i], idx);
>          if (OidIsValid(constraintOid))
>                 ConstraintSetParentConstraint(cldConstrOid, constraintOid,
>                                               RelationGetRelid(attachrel));
>
> However, it seems get_relation_idx_constraint_oid(), introduced in
> eb7ed3f3063,
> assume there could be only ONE constraint depending to an index. But in
> fact,
> multiple constraints can rely on the same index, eg.: the PK and a self
> referencing FK. In consequence, when looking for a constraint depending on
> an
> index for the given relation, either the FK or a PK can appears first
> depending
> on various conditions. It is then possible to trick it make a FK
> constraint a
> parent of a PK...
>
> In the following little scenario, when looking at the constraint linked to
> the PK unique index using the same index than
> get_relation_idx_constraint_oid
> use, this is the self-FK that is actually returned first by
> get_relation_idx_constraint_oid(), NOT the PK:
>
>   postgres=# DROP TABLE IF EXISTS parent, child1;
>
>     CREATE TABLE parent (
>         id bigint NOT NULL default 1,
>         no_part smallint NOT NULL,
>         id_abc bigint,
>         FOREIGN KEY (id_abc, no_part) REFERENCES parent(id, no_part)
>             ON UPDATE RESTRICT ON DELETE RESTRICT,
>         PRIMARY KEY (id, no_part)
>     )
>     PARTITION BY LIST (no_part);
>
>     CREATE TABLE child1 (
>         id bigint NOT NULL default 1,
>         no_part smallint NOT NULL,
>         id_abc bigint,
>         PRIMARY KEY (id, no_part),
>         CONSTRAINT child1 CHECK ((no_part = 1))
>     );
>
>     -- force an indexscan as get_relation_idx_constraint_oid() use the
> unique
>     -- index on (conrelid, contypid, conname) to scan pg_cosntraint
>     set enable_seqscan TO off;
>     set enable_bitmapscan TO off;
>
>     SELECT conname
>     FROM pg_constraint
>     WHERE conrelid = 'parent'::regclass       <=== parent
>       AND conindid = 'parent_pkey'::regclass; <=== PK index
>
>   DROP TABLE
>   CREATE TABLE
>   CREATE TABLE
>   SET
>   SET
>             conname
>   ----------------------------
>    parent_id_abc_no_part_fkey <==== WOOPS!
>    parent_pkey
>   (2 rows)
>
> In consequence, when attaching the partition, the PK of child1 is not
> marked as
> partition of the parent's PK, which is wrong. WORST, the PK of child1 is
> actually unexpectedly marked as a partition of the parent's **self-FK**:
>
>   postgres=# ALTER TABLE ONLY parent ATTACH PARTITION child1
>              FOR VALUES IN ('1');
>
>     SELECT oid, conname, conparentid, conrelid, confrelid
>     FROM pg_constraint
>     WHERE conrelid in ('parent'::regclass, 'child1'::regclass)
>     ORDER BY 1;
>
>   ALTER TABLE
>     oid  |           conname           | conparentid | conrelid |
> confrelid
>
> -------+-----------------------------+-------------+----------+-----------
>    16700 | parent_pkey                 |           0 |    16695 |         0
>    16701 | parent_id_abc_no_part_fkey  |           0 |    16695 |     16695
>    16706 | child1                      |           0 |    16702 |         0
>    16708 | **child1_pkey**             |   **16701** |    16702 |         0
>    16709 | parent_id_abc_no_part_fkey1 |       16701 |    16695 |     16702
>    16712 | parent_id_abc_no_part_fkey  |       16701 |    16702 |     16695
>   (6 rows)
>
> The expected result should probably be something like:
>
>     oid  |           conname           | conparentid | conrelid |
> confrelid
>
> -------+-----------------------------+-------------+----------+-----------
>    16700 | parent_pkey                 |           0 |    16695 |         0
>    ...
>    16708 | child1_pkey                 |       16700 |    16702 |         0
>
>
> I suppose this bug might exists in ATExecAttachPartitionIdx(),
> DetachPartitionFinalize() and DefineIndex() where there's similar code and
> logic
> using get_relation_idx_constraint_oid(). I didn't check for potential bugs
> there
> though.
>
> I'm not sure yet of how this bug should be fixed. Any comment?
>
> Regards,
>
> Hi,
In this case the confrelid field of FormData_pg_constraint for the first
constraint would carry a valid Oid.
Can we use this information and continue searching in
get_relation_idx_constraint_oid() until an entry with 0 confrelid is found ?
If there is no such (secondary) entry, we return the first entry.

Cheers

Reply via email to