Skip to content

Commit 4ae09c5

Browse files
alvherretenderwg
andcommitted
Fix ALTER TABLE DETACH for inconsistent indexes
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 <tndrwang@gmail.com> Reported-by: Alexander Lakhin <exclusion@gmail.com> Reviewed-by: Tender Wang <tndrwang@gmail.com> Reviewed-by: Michael Paquier <michael@paquier.xyz> Discussion: https://postgr.es/m/18500-62948b6fe5522f56@postgresql.org
1 parent 6e2552d commit 4ae09c5

File tree

3 files changed

+95
-4
lines changed

3 files changed

+95
-4
lines changed

src/backend/commands/tablecmds.c

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18782,22 +18782,31 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
1878218782
foreach(cell, indexes)
1878318783
{
1878418784
Oid idxid = lfirst_oid(cell);
18785+
Oid parentidx;
1878518786
Relation idx;
1878618787
Oid constrOid;
18788+
Oid parentConstrOid;
1878718789

1878818790
if (!has_superclass(idxid))
1878918791
continue;
1879018792

18791-
Assert((IndexGetRelation(get_partition_parent(idxid, false), false) ==
18792-
RelationGetRelid(rel)));
18793+
parentidx = get_partition_parent(idxid, false);
18794+
Assert((IndexGetRelation(parentidx, false) == RelationGetRelid(rel)));
1879318795

1879418796
idx = index_open(idxid, AccessExclusiveLock);
1879518797
IndexSetParentIndex(idx, InvalidOid);
1879618798

18797-
/* If there's a constraint associated with the index, detach it too */
18799+
/*
18800+
* If there's a constraint associated with the index, detach it too.
18801+
* Careful: it is possible for a constraint index in a partition to be
18802+
* the child of a non-constraint index, so verify whether the parent
18803+
* index does actually have a constraint.
18804+
*/
1879818805
constrOid = get_relation_idx_constraint_oid(RelationGetRelid(partRel),
1879918806
idxid);
18800-
if (OidIsValid(constrOid))
18807+
parentConstrOid = get_relation_idx_constraint_oid(RelationGetRelid(rel),
18808+
parentidx);
18809+
if (OidIsValid(parentConstrOid) && OidIsValid(constrOid))
1880118810
ConstraintSetParentConstraint(constrOid, InvalidOid, InvalidOid);
1880218811

1880318812
index_close(idx, NoLock);

src/test/regress/expected/constraints.out

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -626,6 +626,48 @@ SELECT conname FROM pg_constraint WHERE conrelid = 'parted_fk_naming_1'::regclas
626626
(1 row)
627627

628628
DROP TABLE parted_fk_naming;
629+
--
630+
-- Test various ways to create primary keys on partitions, linked to unique
631+
-- indexes (without constraints) on the partitioned table. Ideally these should
632+
-- fail, but we don't dare change released behavior, so instead cope with it at
633+
-- DETACH time.
634+
CREATE TEMP TABLE t (a integer, b integer) PARTITION BY HASH (a, b);
635+
CREATE TEMP TABLE tp (a integer, b integer, PRIMARY KEY (a, b), UNIQUE (b, a));
636+
ALTER TABLE t ATTACH PARTITION tp FOR VALUES WITH (MODULUS 1, REMAINDER 0);
637+
CREATE UNIQUE INDEX t_a_idx ON t (a, b);
638+
CREATE UNIQUE INDEX t_b_idx ON t (b, a);
639+
ALTER INDEX t_a_idx ATTACH PARTITION tp_pkey;
640+
ALTER INDEX t_b_idx ATTACH PARTITION tp_b_a_key;
641+
SELECT conname, conparentid, conislocal, coninhcount
642+
FROM pg_constraint WHERE conname IN ('tp_pkey', 'tp_b_a_key');
643+
conname | conparentid | conislocal | coninhcount
644+
------------+-------------+------------+-------------
645+
tp_pkey | 0 | t | 0
646+
tp_b_a_key | 0 | t | 0
647+
(2 rows)
648+
649+
ALTER TABLE t DETACH PARTITION tp;
650+
DROP TABLE t, tp;
651+
CREATE TEMP TABLE t (a integer) PARTITION BY LIST (a);
652+
CREATE TEMP TABLE tp (a integer PRIMARY KEY);
653+
CREATE UNIQUE INDEX t_a_idx ON t (a);
654+
ALTER TABLE t ATTACH PARTITION tp FOR VALUES IN (1);
655+
ALTER TABLE t DETACH PARTITION tp;
656+
DROP TABLE t, tp;
657+
CREATE TEMP TABLE t (a integer) PARTITION BY LIST (a);
658+
CREATE TEMP TABLE tp (a integer PRIMARY KEY);
659+
CREATE UNIQUE INDEX t_a_idx ON ONLY t (a);
660+
ALTER TABLE t ATTACH PARTITION tp FOR VALUES IN (1);
661+
ALTER TABLE t DETACH PARTITION tp;
662+
DROP TABLE t, tp;
663+
CREATE TABLE regress_constr_partitioned (a integer) PARTITION BY LIST (a);
664+
CREATE TABLE regress_constr_partition1 PARTITION OF regress_constr_partitioned FOR VALUES IN (1);
665+
ALTER TABLE regress_constr_partition1 ADD PRIMARY KEY (a);
666+
CREATE UNIQUE INDEX ON regress_constr_partitioned (a);
667+
BEGIN;
668+
ALTER TABLE regress_constr_partitioned DETACH PARTITION regress_constr_partition1;
669+
ROLLBACK;
670+
-- Leave this one in funny state for pg_upgrade testing
629671
-- test a HOT update that invalidates the conflicting tuple.
630672
-- the trigger should still fire and catch the violation
631673
BEGIN;

src/test/regress/sql/constraints.sql

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,46 @@ ALTER TABLE parted_fk_naming ATTACH PARTITION parted_fk_naming_1 FOR VALUES IN (
449449
SELECT conname FROM pg_constraint WHERE conrelid = 'parted_fk_naming_1'::regclass AND contype = 'f';
450450
DROP TABLE parted_fk_naming;
451451

452+
--
453+
-- Test various ways to create primary keys on partitions, linked to unique
454+
-- indexes (without constraints) on the partitioned table. Ideally these should
455+
-- fail, but we don't dare change released behavior, so instead cope with it at
456+
-- DETACH time.
457+
CREATE TEMP TABLE t (a integer, b integer) PARTITION BY HASH (a, b);
458+
CREATE TEMP TABLE tp (a integer, b integer, PRIMARY KEY (a, b), UNIQUE (b, a));
459+
ALTER TABLE t ATTACH PARTITION tp FOR VALUES WITH (MODULUS 1, REMAINDER 0);
460+
CREATE UNIQUE INDEX t_a_idx ON t (a, b);
461+
CREATE UNIQUE INDEX t_b_idx ON t (b, a);
462+
ALTER INDEX t_a_idx ATTACH PARTITION tp_pkey;
463+
ALTER INDEX t_b_idx ATTACH PARTITION tp_b_a_key;
464+
SELECT conname, conparentid, conislocal, coninhcount
465+
FROM pg_constraint WHERE conname IN ('tp_pkey', 'tp_b_a_key');
466+
ALTER TABLE t DETACH PARTITION tp;
467+
DROP TABLE t, tp;
468+
469+
CREATE TEMP TABLE t (a integer) PARTITION BY LIST (a);
470+
CREATE TEMP TABLE tp (a integer PRIMARY KEY);
471+
CREATE UNIQUE INDEX t_a_idx ON t (a);
472+
ALTER TABLE t ATTACH PARTITION tp FOR VALUES IN (1);
473+
ALTER TABLE t DETACH PARTITION tp;
474+
DROP TABLE t, tp;
475+
476+
CREATE TEMP TABLE t (a integer) PARTITION BY LIST (a);
477+
CREATE TEMP TABLE tp (a integer PRIMARY KEY);
478+
CREATE UNIQUE INDEX t_a_idx ON ONLY t (a);
479+
ALTER TABLE t ATTACH PARTITION tp FOR VALUES IN (1);
480+
ALTER TABLE t DETACH PARTITION tp;
481+
DROP TABLE t, tp;
482+
483+
CREATE TABLE regress_constr_partitioned (a integer) PARTITION BY LIST (a);
484+
CREATE TABLE regress_constr_partition1 PARTITION OF regress_constr_partitioned FOR VALUES IN (1);
485+
ALTER TABLE regress_constr_partition1 ADD PRIMARY KEY (a);
486+
CREATE UNIQUE INDEX ON regress_constr_partitioned (a);
487+
BEGIN;
488+
ALTER TABLE regress_constr_partitioned DETACH PARTITION regress_constr_partition1;
489+
ROLLBACK;
490+
-- Leave this one in funny state for pg_upgrade testing
491+
452492
-- test a HOT update that invalidates the conflicting tuple.
453493
-- the trigger should still fire and catch the violation
454494

0 commit comments

Comments
 (0)