Fix ALTER TABLE DETACH for inconsistent indexes
authorAlvaro Herrera <[email protected]>
Fri, 12 Jul 2024 10:54:01 +0000 (12:54 +0200)
committerAlvaro Herrera <[email protected]>
Fri, 12 Jul 2024 10:54:01 +0000 (12:54 +0200)
When a partitioned table has an index that doesn't support a constraint,
but a partition has an equivalent index that does, then a DETACH
operation would misbehave: a crash in assertion-enabled systems (because
we fail to find the constraint in the parent that we expect to), or a
broken coninhcount value (-1) in production systems (because we blindly
believe that we've successfully detached the parent).

While we should reject an ATTACH of a partition with such an index, we
have failed to do so in existing releases, so adding an error in stable
releases might break the (unlikely) existing applications that rely on
this behavior.  At this point I don't even want to reject them in
master, because it'd break pg_upgrade if such databases exist, and there
would be no easy way to fix existing databases without expensive index
rebuilds.

(Later on we could add ALTER TABLE ... ADD CONSTRAINT USING INDEX to
partitioned tables, which would allow the user to fix such patterns.  At
that point we could add more restrictions to prevent the problem from
its root.)

Also, add a test case that leaves one table in this condition, so that
we can verify that pg_upgrade continues to work if we later decide to
change the policy on the master branch.

Backpatch to all supported branches.

Co-authored-by: Tender Wang <[email protected]>
Reported-by: Alexander Lakhin <[email protected]>
Reviewed-by: Tender Wang <[email protected]>
Reviewed-by: Michael Paquier <[email protected]>
Discussion: https://postgr.es/m/18500-62948b6fe5522f56@postgresql.org

src/backend/commands/tablecmds.c
src/test/regress/expected/sanity_check.out
src/test/regress/input/constraints.source
src/test/regress/output/constraints.source

index c0c707ca03bd4522192cecf6713de936160c1d88..5c2e64d4868e555cbd31424a2f149d647a340ae0 100644 (file)
@@ -17066,22 +17066,31 @@ ATExecDetachPartition(Relation rel, RangeVar *name)
    foreach(cell, indexes)
    {
        Oid         idxid = lfirst_oid(cell);
+       Oid         parentidx;
        Relation    idx;
        Oid         constrOid;
+       Oid         parentConstrOid;
 
        if (!has_superclass(idxid))
            continue;
 
-       Assert((IndexGetRelation(get_partition_parent(idxid), false) ==
-               RelationGetRelid(rel)));
+       parentidx = get_partition_parent(idxid);
+       Assert(IndexGetRelation(parentidx, false) == RelationGetRelid(rel));
 
        idx = index_open(idxid, AccessExclusiveLock);
        IndexSetParentIndex(idx, InvalidOid);
 
-       /* If there's a constraint associated with the index, detach it too */
+       /*
+        * If there's a constraint associated with the index, detach it too.
+        * Careful: it is possible for a constraint index in a partition to be
+        * the child of a non-constraint index, so verify whether the parent
+        * index does actually have a constraint.
+        */
        constrOid = get_relation_idx_constraint_oid(RelationGetRelid(partRel),
                                                    idxid);
-       if (OidIsValid(constrOid))
+       parentConstrOid = get_relation_idx_constraint_oid(RelationGetRelid(rel),
+                                                         parentidx);
+       if (OidIsValid(parentConstrOid) && OidIsValid(constrOid))
            ConstraintSetParentConstraint(constrOid, InvalidOid, InvalidOid);
 
        index_close(idx, NoLock);
index deff49b07b58c6dfab5cae2df19a53d1c77c53b0..c1ebf5a37491f4d3c4549156ad5458db0ccbc1cc 100644 (file)
@@ -171,6 +171,8 @@ quad_poly_tbl|t
 radix_text_tbl|t
 ramp|f
 real_city|f
+regress_constr_partition1|t
+regress_constr_partitioned|t
 road|t
 shighway|t
 slow_emp4000|f
index d7996a5d8377dec8992df1cc90d85e3c5b5a7396..47f2bfa0b4f814a2784cfcf8f6ed6150eae09f6a 100644 (file)
@@ -429,6 +429,46 @@ ALTER TABLE parted_fk_naming ATTACH PARTITION parted_fk_naming_1 FOR VALUES IN (
 SELECT conname FROM pg_constraint WHERE conrelid = 'parted_fk_naming_1'::regclass AND contype = 'f';
 DROP TABLE parted_fk_naming;
 
+--
+-- Test various ways to create primary keys on partitions, linked to unique
+-- indexes (without constraints) on the partitioned table.  Ideally these should
+-- fail, but we don't dare change released behavior, so instead cope with it at
+-- DETACH time.
+CREATE TEMP TABLE t (a integer, b integer) PARTITION BY HASH (a, b);
+CREATE TEMP TABLE tp (a integer, b integer, PRIMARY KEY (a, b), UNIQUE (b, a));
+ALTER TABLE t ATTACH PARTITION tp FOR VALUES WITH (MODULUS 1, REMAINDER 0);
+CREATE UNIQUE INDEX t_a_idx ON t (a, b);
+CREATE UNIQUE INDEX t_b_idx ON t (b, a);
+ALTER INDEX t_a_idx ATTACH PARTITION tp_pkey;
+ALTER INDEX t_b_idx ATTACH PARTITION tp_b_a_key;
+ALTER TABLE t DETACH PARTITION tp;
+SELECT conname, conparentid, conislocal, coninhcount
+  FROM pg_constraint WHERE conname IN ('tp_pkey', 'tp_b_a_key');
+DROP TABLE t, tp;
+
+CREATE TEMP TABLE t (a integer) PARTITION BY LIST (a);
+CREATE TEMP TABLE tp (a integer PRIMARY KEY);
+CREATE UNIQUE INDEX t_a_idx ON t (a);
+ALTER TABLE t ATTACH PARTITION tp FOR VALUES IN (1);
+ALTER TABLE t DETACH PARTITION tp;
+DROP TABLE t, tp;
+
+CREATE TEMP TABLE t (a integer) PARTITION BY LIST (a);
+CREATE TEMP TABLE tp (a integer PRIMARY KEY);
+CREATE UNIQUE INDEX t_a_idx ON ONLY t (a);
+ALTER TABLE t ATTACH PARTITION tp FOR VALUES IN (1);
+ALTER TABLE t DETACH PARTITION tp;
+DROP TABLE t, tp;
+
+CREATE TABLE regress_constr_partitioned (a integer) PARTITION BY LIST (a);
+CREATE TABLE regress_constr_partition1 PARTITION OF regress_constr_partitioned FOR VALUES IN (1);
+ALTER TABLE regress_constr_partition1 ADD PRIMARY KEY (a);
+CREATE UNIQUE INDEX ON regress_constr_partitioned (a);
+BEGIN;
+ALTER TABLE regress_constr_partitioned DETACH PARTITION regress_constr_partition1;
+ROLLBACK;
+--  Leave this one in funny state for pg_upgrade testing
+
 -- test a HOT update that invalidates the conflicting tuple.
 -- the trigger should still fire and catch the violation
 
index 4a422442e5822c1e1ef12a5c61f165c3f12463fc..2b4a06f8160401b780ae90da8155e8e3b7fa215a 100644 (file)
@@ -598,6 +598,48 @@ SELECT conname FROM pg_constraint WHERE conrelid = 'parted_fk_naming_1'::regclas
 (1 row)
 
 DROP TABLE parted_fk_naming;
+--
+-- Test various ways to create primary keys on partitions, linked to unique
+-- indexes (without constraints) on the partitioned table.  Ideally these should
+-- fail, but we don't dare change released behavior, so instead cope with it at
+-- DETACH time.
+CREATE TEMP TABLE t (a integer, b integer) PARTITION BY HASH (a, b);
+CREATE TEMP TABLE tp (a integer, b integer, PRIMARY KEY (a, b), UNIQUE (b, a));
+ALTER TABLE t ATTACH PARTITION tp FOR VALUES WITH (MODULUS 1, REMAINDER 0);
+CREATE UNIQUE INDEX t_a_idx ON t (a, b);
+CREATE UNIQUE INDEX t_b_idx ON t (b, a);
+ALTER INDEX t_a_idx ATTACH PARTITION tp_pkey;
+ALTER INDEX t_b_idx ATTACH PARTITION tp_b_a_key;
+ALTER TABLE t DETACH PARTITION tp;
+SELECT conname, conparentid, conislocal, coninhcount
+  FROM pg_constraint WHERE conname IN ('tp_pkey', 'tp_b_a_key');
+  conname   | conparentid | conislocal | coninhcount 
+------------+-------------+------------+-------------
+ tp_pkey    |           0 | t          |           0
+ tp_b_a_key |           0 | t          |           0
+(2 rows)
+
+DROP TABLE t, tp;
+CREATE TEMP TABLE t (a integer) PARTITION BY LIST (a);
+CREATE TEMP TABLE tp (a integer PRIMARY KEY);
+CREATE UNIQUE INDEX t_a_idx ON t (a);
+ALTER TABLE t ATTACH PARTITION tp FOR VALUES IN (1);
+ALTER TABLE t DETACH PARTITION tp;
+DROP TABLE t, tp;
+CREATE TEMP TABLE t (a integer) PARTITION BY LIST (a);
+CREATE TEMP TABLE tp (a integer PRIMARY KEY);
+CREATE UNIQUE INDEX t_a_idx ON ONLY t (a);
+ALTER TABLE t ATTACH PARTITION tp FOR VALUES IN (1);
+ALTER TABLE t DETACH PARTITION tp;
+DROP TABLE t, tp;
+CREATE TABLE regress_constr_partitioned (a integer) PARTITION BY LIST (a);
+CREATE TABLE regress_constr_partition1 PARTITION OF regress_constr_partitioned FOR VALUES IN (1);
+ALTER TABLE regress_constr_partition1 ADD PRIMARY KEY (a);
+CREATE UNIQUE INDEX ON regress_constr_partitioned (a);
+BEGIN;
+ALTER TABLE regress_constr_partitioned DETACH PARTITION regress_constr_partition1;
+ROLLBACK;
+--  Leave this one in funny state for pg_upgrade testing
 -- test a HOT update that invalidates the conflicting tuple.
 -- the trigger should still fire and catch the violation
 BEGIN;