From 24c67c48c09c5631dec021d944d5ae076ee1ff18 Mon Sep 17 00:00:00 2001 From: Haibo Yan Date: Thu, 17 Sep 2026 20:00:40 -0700 Subject: Report not-ready GTT arbiter indexes correctly. For a partitioned global temporary table, arbiter inference and arbiter checking can legitimately disagree. The planner looks at the partitioned index, which has no session-local storage and therefore reports the pg_index values, so it looks ready for inserts; the executor has to use the index on the leaf partition, whose session-local state may say that it is not ready, because it was created by another session while this one already had data in the table. ExecCheckIndexConstraints() correctly declines to check such an index, but that left checkedIndex false with a non-empty arbiter list, reaching an elog() that is meant to catch planner/executor disagreement that cannot happen. Users of a partitioned global temporary table therefore saw an internal error, with nothing to tell them that REINDEX was the remedy. Record it explicitly when an index that is one of the requested arbiters is skipped because it is not ready for inserts, and report that case as a user error, with a hint naming the table to reindex. To be able to recognise it, test arbiter membership before readiness; both tests are plain skips, so the set of indexes examined does not change. Everything else that can leave no arbiter checked -- an arbiter that is missing, or not unique and not exclusion-capable, or otherwise not what the planner chose -- still reaches the existing elog(), since those remain can't-happen cases. Add an isolation test for the partitioned case, covering both the error while the leaf index is not ready and correct ON CONFLICT DO NOTHING and DO UPDATE behaviour after REINDEX. --- src/backend/executor/execIndexing.c | 44 +++++++++++++-- src/test/isolation/expected/global-temp.out | 61 +++++++++++++++++++++ src/test/isolation/specs/global-temp.spec | 39 +++++++++++++ 3 files changed, 140 insertions(+), 4 deletions(-) diff --git a/src/backend/executor/execIndexing.c b/src/backend/executor/execIndexing.c index d0a714a42f2..e14b37ca4b6 100644 --- a/src/backend/executor/execIndexing.c +++ b/src/backend/executor/execIndexing.c @@ -554,6 +554,7 @@ ExecCheckIndexConstraints(ResultRelInfo *resultRelInfo, TupleTableSlot *slot, bool isnull[INDEX_MAX_KEYS]; ItemPointerData invalidItemPtr; bool checkedIndex = false; + bool skippedNotReadyArbiter = false; ItemPointerSetInvalid(conflictTid); ItemPointerSetInvalid(&invalidItemPtr); @@ -593,16 +594,27 @@ ExecCheckIndexConstraints(ResultRelInfo *resultRelInfo, TupleTableSlot *slot, if (!indexInfo->ii_Unique && !indexInfo->ii_ExclusionOps) continue; - /* If the index is marked as read-only, ignore it */ - if (!indexInfo->ii_ReadyForInserts) - continue; - /* When specific arbiter indexes requested, only examine them */ if (arbiterIndexes != NIL && !list_member_oid(arbiterIndexes, indexRelation->rd_index->indexrelid)) continue; + /* + * If the index is marked as read-only, ignore it. + * + * Having got this far, we know that the index is one of the requested + * arbiter indexes, if any were requested, so remember that we skipped + * a requested arbiter for this reason. That lets us tell this case + * apart, below, from the can't-happen cases. + */ + if (!indexInfo->ii_ReadyForInserts) + { + if (arbiterIndexes != NIL) + skippedNotReadyArbiter = true; + continue; + } + if (!indexRelation->rd_index->indimmediate) ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), @@ -654,7 +666,31 @@ ExecCheckIndexConstraints(ResultRelInfo *resultRelInfo, TupleTableSlot *slot, } if (arbiterIndexes != NIL && !checkedIndex) + { + /* + * If we had to skip a requested arbiter index because it is not ready + * for inserts, report that, since the user can act on it. This is + * reachable for a global temporary relation, where indisready is + * session-local: the planner infers arbiters from a partitioned + * index, which has no session-local storage and so looks ready, while + * the executor uses the index on the leaf partition, which may have + * been marked not ready in this session because it was created by + * another session while we already had data in the table. + * + * Any other way of getting here means the planner and the executor + * disagree about the arbiter indexes, which is a can't-happen case. + */ + if (RELATION_IS_GLOBAL_TEMP(heapRelation) && skippedNotReadyArbiter) + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("cannot use ON CONFLICT with global temporary table \"%s\"", + RelationGetRelationName(heapRelation)), + errdetail("The arbiter index is not ready for inserts in this session."), + errhint("Use REINDEX TABLE \"%s\" to rebuild its indexes in this session.", + RelationGetRelationName(heapRelation))); + elog(ERROR, "unexpected failure to find arbiter index"); + } return true; } diff --git a/src/test/isolation/expected/global-temp.out b/src/test/isolation/expected/global-temp.out index f349bbf2b08..872f4dbe9e0 100644 --- a/src/test/isolation/expected/global-temp.out +++ b/src/test/isolation/expected/global-temp.out @@ -1056,6 +1056,67 @@ key|val|seq (1 row) +starting permutation: pc_create1 pc_ins1 pc_ins2 pc_uniq1 pc_open2 pc_info2 pc_nothing2 pc_update2 pc_reidx2 pc_info2 pc_nothing2 pc_update2 pc_sel2 pc_drop1 +step pc_create1: + CREATE GLOBAL TEMP TABLE tmp4 (key int, val text) PARTITION BY RANGE (key); + CREATE GLOBAL TEMP TABLE tmp4_p1 PARTITION OF tmp4 FOR VALUES FROM (1) TO (100); + +step pc_ins1: INSERT INTO tmp4 VALUES (1, 's1'); +step pc_ins2: INSERT INTO tmp4 VALUES (2, 's2'); +step pc_uniq1: CREATE UNIQUE INDEX tmp4_un ON tmp4(key); +step pc_open2: INSERT INTO tmp4 VALUES (3, 's2b'); +step pc_info2: + SELECT c.relname, c.relkind, t.indisvalid AS local_valid, + t.indisready AS local_ready + FROM pg_class c, LATERAL pg_gtr_index_info(c.oid) t + WHERE c.relkind IN ('i', 'I') AND c.relname LIKE 'tmp4%' + ORDER BY c.relname; + +relname |relkind|local_valid|local_ready +---------------+-------+-----------+----------- +tmp4_p1_key_idx|i |f |f +tmp4_un |I | | +(2 rows) + +step pc_nothing2: + INSERT INTO tmp4 VALUES (2, 's2 new') ON CONFLICT (key) DO NOTHING; + +ERROR: cannot use ON CONFLICT with global temporary table "tmp4_p1" +step pc_update2: + INSERT INTO tmp4 VALUES (3, 's2 upd') ON CONFLICT (key) + DO UPDATE SET val = excluded.val; + +ERROR: cannot use ON CONFLICT with global temporary table "tmp4_p1" +step pc_reidx2: REINDEX TABLE tmp4_p1; +step pc_info2: + SELECT c.relname, c.relkind, t.indisvalid AS local_valid, + t.indisready AS local_ready + FROM pg_class c, LATERAL pg_gtr_index_info(c.oid) t + WHERE c.relkind IN ('i', 'I') AND c.relname LIKE 'tmp4%' + ORDER BY c.relname; + +relname |relkind|local_valid|local_ready +---------------+-------+-----------+----------- +tmp4_p1_key_idx|i |t |t +tmp4_un |I | | +(2 rows) + +step pc_nothing2: + INSERT INTO tmp4 VALUES (2, 's2 new') ON CONFLICT (key) DO NOTHING; + +step pc_update2: + INSERT INTO tmp4 VALUES (3, 's2 upd') ON CONFLICT (key) + DO UPDATE SET val = excluded.val; + +step pc_sel2: SELECT * FROM tmp4 ORDER BY key; +key|val +---+------ + 2|s2 + 3|s2 upd +(2 rows) + +step pc_drop1: DROP TABLE tmp4; + starting permutation: drop_tblspace list_tblspaces step drop_tblspace: DROP TABLESPACE regress_isolation_tablespace; step list_tblspaces: SELECT spcname FROM pg_tablespace ORDER BY 1; diff --git a/src/test/isolation/specs/global-temp.spec b/src/test/isolation/specs/global-temp.spec index 4ef65df1caf..745959d9448 100644 --- a/src/test/isolation/specs/global-temp.spec +++ b/src/test/isolation/specs/global-temp.spec @@ -97,6 +97,15 @@ step sel1_idx { SELECT * FROM tmp WHERE val = 's1'; } +# Test ON CONFLICT on a partitioned GTT whose leaf index is not ready +step pc_create1 { + CREATE GLOBAL TEMP TABLE tmp4 (key int, val text) PARTITION BY RANGE (key); + CREATE GLOBAL TEMP TABLE tmp4_p1 PARTITION OF tmp4 FOR VALUES FROM (1) TO (100); +} +step pc_ins1 { INSERT INTO tmp4 VALUES (1, 's1'); } +step pc_uniq1 { CREATE UNIQUE INDEX tmp4_un ON tmp4(key); } +step pc_drop1 { DROP TABLE tmp4; } + session s2 # Transaction control step b2 { BEGIN; } @@ -154,6 +163,26 @@ step sel2_idx { step reidx2 { REINDEX INDEX tmp_val_idx; } step analyze2 { ANALYZE tmp; } +# Test ON CONFLICT on a partitioned GTT whose leaf index is not ready +step pc_ins2 { INSERT INTO tmp4 VALUES (2, 's2'); } +step pc_open2 { INSERT INTO tmp4 VALUES (3, 's2b'); } +step pc_info2 { + SELECT c.relname, c.relkind, t.indisvalid AS local_valid, + t.indisready AS local_ready + FROM pg_class c, LATERAL pg_gtr_index_info(c.oid) t + WHERE c.relkind IN ('i', 'I') AND c.relname LIKE 'tmp4%' + ORDER BY c.relname; +} +step pc_nothing2 { + INSERT INTO tmp4 VALUES (2, 's2 new') ON CONFLICT (key) DO NOTHING; +} +step pc_update2 { + INSERT INTO tmp4 VALUES (3, 's2 upd') ON CONFLICT (key) + DO UPDATE SET val = excluded.val; +} +step pc_reidx2 { REINDEX TABLE tmp4_p1; } +step pc_sel2 { SELECT * FROM tmp4 ORDER BY key; } + # Create test tablespace for remaining tests permutation create_tblspace list_tblspaces @@ -212,5 +241,15 @@ permutation ins1 ins2 idx1 sel1_idx sel2_idx permutation ins1 ins2 idx1 sel1_idx sel2_idx reidx2 sel2_idx permutation ins1 ins2 idx1 sel1_idx sel2_idx analyze2 sel2_idx reidx2 sel2_idx +# Test ON CONFLICT on a partitioned GTT whose leaf index is not ready for +# inserts in this session. Arbiter inference works from the partitioned +# index, which has no session-local storage and so looks ready, but the +# executor has to use the index on the leaf partition, which does not. That +# must be reported as a user error, not as an internal one, and REINDEX must +# make ON CONFLICT work again. +permutation pc_create1 pc_ins1 pc_ins2 pc_uniq1 pc_open2 pc_info2 + pc_nothing2 pc_update2 + pc_reidx2 pc_info2 pc_nothing2 pc_update2 pc_sel2 pc_drop1 + # Tidy up permutation drop_tblspace list_tblspaces -- 2.54.0