Re: [BUG] parenting a PK constraint to a self-FK one (Was: Self FK oddity when attaching a partition) - Mailing list pgsql-hackers

From Zhihong Yu
Subject Re: [BUG] parenting a PK constraint to a self-FK one (Was: Self FK oddity when attaching a partition)
Date
Msg-id CALNJ-vTL7y8EykDcJ8_fk5rcjqF8jCWoYw_uqjpAiB6NDtPMKg@mail.gmail.com
Whole thread Raw
In response to [BUG] parenting a PK constraint to a self-FK one (Was: Self FK oddity when attaching a partition)  (Jehan-Guillaume de Rorthais <jgdr@dalibo.com>)
List pgsql-hackers


On Tue, Aug 23, 2022 at 8:07 AM Jehan-Guillaume de Rorthais <jgdr@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

pgsql-hackers by date:

Previous
From: Andres Freund
Date:
Subject: Re: SQL/JSON features for v15
Next
From: Pavel Stehule
Date:
Subject: Re: SQL/JSON features for v15