BUG #19817: CREATE UNIQUE INDEX on a partitioned table can silently attach an invalid, not-ready child index, le

From: PG Bug reporting form <noreply(at)postgresql(dot)org>
To: pgsql-bugs(at)lists(dot)postgresql(dot)org
Cc: 303677365(at)qq(dot)com
Subject: BUG #19817: CREATE UNIQUE INDEX on a partitioned table can silently attach an invalid, not-ready child index, le
Date: 2026-10-30 06:40:13
Message-ID: 19817-4e72209979d7bdd5@postgresql.org
Views: Whole Thread | Raw Message | Download mbox | Resend email
Thread:
Lists: pgsql-bugs

The following bug has been logged on the website:

Bug reference: 19817
Logged by: chunling qin
Email address: 303677365(at)qq(dot)com
PostgreSQL version: 18.6
Operating system: Linux (TencentOS 4.4, x86_64)
Description:

Hello,

When CREATE UNIQUE INDEX is run on a partitioned table, DefineIndex()
recurses to each partition and reuses an already-existing index it considers
"equivalent" (src/backend/commands/indexcmds.c, the recursion loop around
lines 1450-1495). That equivalence check only compares the index
definition;
it does not check indisvalid or indisready. As a result, an index left
invalid (and not ready) by a previously failed CREATE INDEX CONCURRENTLY is
silently attached as a child index, the parent index is created with
indisvalid=false, and the whole chain reports success -- while in fact the
UNIQUE constraint is not enforced at all.

Steps to reproduce (self-contained, no data needed):

-- a partition with a leftover invalid index from a failed CIC
CREATE TABLE b2_root (a int, b text) PARTITION BY RANGE (a);
CREATE TABLE b2_p1 PARTITION OF b2_root FOR VALUES FROM (0) TO (1000);

INSERT INTO b2_p1 VALUES (1,'x'), (1,'dup'); -- duplicate key
CREATE UNIQUE INDEX CONCURRENTLY b2_p1_bad ON b2_p1 (a); -- fails,
leaves
-- b2_p1_bad behind with indisvalid=f, indisready=f
DELETE FROM b2_p1 WHERE b = 'dup'; -- table is
clean now

-- the silent failure:
CREATE UNIQUE INDEX b2_root_ui ON b2_root (a); -- returns
-- CREATE INDEX, no warning

-- state after: the invalid leftover is attached as a child index
SELECT c.relname, i.indisvalid, i.indisready
FROM pg_index i JOIN pg_class c ON c.oid = i.indexrelid
WHERE c.relname IN ('b2_p1_bad','b2_root_ui');
-- b2_p1_bad | f | f (parent: b2_root_ui)
-- b2_root_ui | f | t

-- consequence a: uniqueness is not enforced, duplicates are accepted
INSERT INTO b2_p1 VALUES (1,'x'), (1,'x'); -- succeeds

-- consequence b: ON CONFLICT can no longer be used on this table
INSERT INTO b2_p1 VALUES (1,'x') ON CONFLICT (a) DO NOTHING;
-- ERROR: there is no unique or exclusion constraint matching
-- the ON CONFLICT specification

Expected: the recursion loop should skip (or hard-fail on) child indexes
that are not indisvalid/indisready, at minimum emit a WARNING. For
comparison, the ATTACH path already does a full check:
AttachPartitionEnsureIndexes() (src/backend/commands/tablecmds.c) refuses
in that situation, and the project has recently applied the same standard
to REPACK ("Fail REPACK in presence of invalid indexes", commits 0c5d6269614
and 78142821dfd).

Actual: CREATE UNIQUE INDEX reports success and silently attaches the
invalid leftover index; the unique constraint enforces nothing; no warning
of any kind is emitted.

Reproduced on:
- PostgreSQL 20devel (snapshot of 2026-10-27, x86_64-pc-linux-gnu)
- PostgreSQL 18.6 (REL_18_STABLE, self-built)

The behavior looks the same on both branches, so this appears to be
long-standing rather than a recent regression.

A draft fix location would be the recursion loop in DefineIndex()
(src/backend/commands/indexcmds.c:1450-1495): before reusing an existing
partition index, require indisvalid && indisready, mirroring
AttachPartitionEnsureIndexes().

Regards,
Chunling Qin

Browse pgsql-bugs by date

  From Date Subject
Next Message PG Bug reporting form 2026-10-30 06:49:16 BUG #19818: ATTACH PARTITION / CREATE INDEX accept a child index whose deferrable property differs from the pare
Previous Message Chris St Denis 2026-10-30 06:16:04 Re: crash in ltree_compare_distance() in 14.24