From b77dccaa16b78e4503e5c15604f4ce77b2fdacb4 Mon Sep 17 00:00:00 2001 From: Haibo Yan Date: Thu, 17 Sep 2026 19:49:54 -0700 Subject: Fix assertion failure in infer_arbiter_indexes() for GTTs. Global temporary tables make pg_index.indisready session-local: each backend has its own copy of an index's storage, and a copy that could not be built -- because the index was created by another session while this one already had data in the table -- is marked not ready for inserts, and not valid, in that session alone. The relcache applies that override, so idxRel->rd_index->indisready holds the session-effective value. infer_arbiter_indexes() asserted that the index backing a constraint named in ON CONFLICT ON CONSTRAINT is always ready for inserts. That holds for every other kind of relation, but not for a global temporary table, so INSERT ... ON CONFLICT ON CONSTRAINT could fail the assertion and terminate the backend in an assert-enabled build. Nothing beyond the assertion needs to change. The loop that follows already ignores indexes that are not ready for inserts, and the query then reports that no matching unique or exclusion constraint was found, which is the correct answer: such an index cannot serve as an arbiter until REINDEX has rebuilt it in this session. Add an isolation test for the whole cycle -- the constraint's index becoming not ready in the session that holds data, ON CONFLICT ON CONSTRAINT reporting no matching constraint, and arbiter inference working again after REINDEX. --- src/backend/optimizer/util/plancat.c | 13 +++++- src/test/isolation/expected/global-temp.out | 46 +++++++++++++++++++++ src/test/isolation/specs/global-temp.spec | 30 ++++++++++++++ 3 files changed, 87 insertions(+), 2 deletions(-) diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c index 1c6bb49ef00..e5b3ed0d9f9 100644 --- a/src/backend/optimizer/util/plancat.c +++ b/src/backend/optimizer/util/plancat.c @@ -920,8 +920,17 @@ infer_arbiter_indexes(PlannerInfo *root) if (indexOidFromConstraint == idxForm->indexrelid) { - /* Found it. */ - Assert(idxForm->indisready); + /* + * Found it. + * + * For a global temporary relation, indisready is + * session-local, so a constraint's index may be not ready for + * inserts in this session even though pg_index says that it + * is. The loop below skips such an index, and we then report + * that no matching constraint was found. + */ + Assert(idxForm->indisready || + RELATION_IS_GLOBAL_TEMP(idxRel)); /* * Set up inferElems and inferIndexExprs to match the diff --git a/src/test/isolation/expected/global-temp.out b/src/test/isolation/expected/global-temp.out index f349bbf2b08..1d9133d8210 100644 --- a/src/test/isolation/expected/global-temp.out +++ b/src/test/isolation/expected/global-temp.out @@ -356,6 +356,52 @@ step uniq_reidx2: REINDEX INDEX tmp2_un; ERROR: could not create unique index "tmp2_un" step drop1: DROP TABLE tmp2; +starting permutation: oc_create1 oc_ins1 oc_ins2 oc_uniq1 oc_con1 oc_open2 oc_info2 oc_conflict2 oc_reidx2 oc_info2 oc_conflict2 oc_sel2 oc_drop1 +step oc_create1: CREATE GLOBAL TEMP TABLE tmp3 (key int, val text); +step oc_ins1: INSERT INTO tmp3 VALUES (1, 's1'); +step oc_ins2: INSERT INTO tmp3 VALUES (2, 's2'); +step oc_uniq1: CREATE UNIQUE INDEX tmp3_un ON tmp3(val); +step oc_con1: ALTER TABLE tmp3 ADD CONSTRAINT tmp3_un UNIQUE USING INDEX tmp3_un; +step oc_open2: INSERT INTO tmp3 VALUES (3, 's2b'); +step oc_info2: + SELECT i.indisvalid AS global_valid, i.indisready AS global_ready, + t.indisvalid AS local_valid, t.indisready AS local_ready + FROM pg_index i, LATERAL pg_gtr_index_info(i.indexrelid) t + WHERE i.indexrelid = 'tmp3_un'::regclass; + +global_valid|global_ready|local_valid|local_ready +------------+------------+-----------+----------- +t |t |f |f +(1 row) + +step oc_conflict2: + INSERT INTO tmp3 VALUES (4, 's2') ON CONFLICT ON CONSTRAINT tmp3_un DO NOTHING; + +ERROR: there is no unique or exclusion constraint matching the ON CONFLICT specification +step oc_reidx2: REINDEX TABLE tmp3; +step oc_info2: + SELECT i.indisvalid AS global_valid, i.indisready AS global_ready, + t.indisvalid AS local_valid, t.indisready AS local_ready + FROM pg_index i, LATERAL pg_gtr_index_info(i.indexrelid) t + WHERE i.indexrelid = 'tmp3_un'::regclass; + +global_valid|global_ready|local_valid|local_ready +------------+------------+-----------+----------- +t |t |t |t +(1 row) + +step oc_conflict2: + INSERT INTO tmp3 VALUES (4, 's2') ON CONFLICT ON CONSTRAINT tmp3_un DO NOTHING; + +step oc_sel2: SELECT * FROM tmp3 ORDER BY key; +key|val +---+--- + 2|s2 + 3|s2b +(2 rows) + +step oc_drop1: DROP TABLE tmp3; + starting permutation: create1dr b1 b2 ins1_2 ins2_2 sel1_2 sel2_2 c1 c2 sel1_2 sel2_2 drop1 step create1dr: CREATE GLOBAL TEMP TABLE tmp2 (key int, val text) ON COMMIT DELETE ROWS; step b1: BEGIN; diff --git a/src/test/isolation/specs/global-temp.spec b/src/test/isolation/specs/global-temp.spec index 4ef65df1caf..18bdfd91419 100644 --- a/src/test/isolation/specs/global-temp.spec +++ b/src/test/isolation/specs/global-temp.spec @@ -56,6 +56,13 @@ step idx_info1 { WHERE i.indexrelid = 'tmp2_un'::regclass; } +# Test ON CONFLICT ON CONSTRAINT when the constraint's index is not ready +step oc_create1 { CREATE GLOBAL TEMP TABLE tmp3 (key int, val text); } +step oc_ins1 { INSERT INTO tmp3 VALUES (1, 's1'); } +step oc_uniq1 { CREATE UNIQUE INDEX tmp3_un ON tmp3(val); } +step oc_con1 { ALTER TABLE tmp3 ADD CONSTRAINT tmp3_un UNIQUE USING INDEX tmp3_un; } +step oc_drop1 { DROP TABLE tmp3; } + # Test concurrent ON COMMIT DELETE ROWS step create1dr { CREATE GLOBAL TEMP TABLE tmp2 (key int, val text) ON COMMIT DELETE ROWS; } step sel1_2 { SELECT * FROM tmp2; } @@ -123,6 +130,21 @@ step idx_info2 { WHERE i.indexrelid = 'tmp2_un'::regclass; } +# Test ON CONFLICT ON CONSTRAINT when the constraint's index is not ready +step oc_ins2 { INSERT INTO tmp3 VALUES (2, 's2'); } +step oc_open2 { INSERT INTO tmp3 VALUES (3, 's2b'); } +step oc_info2 { + SELECT i.indisvalid AS global_valid, i.indisready AS global_ready, + t.indisvalid AS local_valid, t.indisready AS local_ready + FROM pg_index i, LATERAL pg_gtr_index_info(i.indexrelid) t + WHERE i.indexrelid = 'tmp3_un'::regclass; +} +step oc_conflict2 { + INSERT INTO tmp3 VALUES (4, 's2') ON CONFLICT ON CONSTRAINT tmp3_un DO NOTHING; +} +step oc_reidx2 { REINDEX TABLE tmp3; } +step oc_sel2 { SELECT * FROM tmp3 ORDER BY key; } + # Test concurrent ON COMMIT DELETE ROWS step sel2_2 { SELECT * FROM tmp2; } @@ -182,6 +204,14 @@ permutation create1 ins1_2 ins2_2 uniq_idx1 permutation create1 ins1_2 ins2_2 uniq_idx1 idx_info1 idx_info2 ins2_2 idx_info2 uniq_reidx2 drop1 +# Test ON CONFLICT ON CONSTRAINT when the constraint's index was created by +# another session while we already had data in the table, and so is not ready +# for inserts locally. The executor must not use such an index, and the +# planner must not assume that a constraint's index is always ready. +permutation oc_create1 oc_ins1 oc_ins2 oc_uniq1 oc_con1 + oc_open2 oc_info2 oc_conflict2 + oc_reidx2 oc_info2 oc_conflict2 oc_sel2 oc_drop1 + # Test concurrent ON COMMIT DELETE ROWS permutation create1dr b1 b2 ins1_2 ins2_2 sel1_2 sel2_2 c1 c2 sel1_2 sel2_2 drop1 -- 2.54.0