Summary
squeeze.squeeze_table() fails on any table that has an exclusion constraint:
ERROR: exclusion constraint record missing for rel ind_1
The error is raised by PostgreSQL core (RelationGetExclusionInfo in src/backend/utils/cache/relcache.c) while pg_squeeze opens the transient table's indexes, because the transient copy of the exclusion-constraint index is created with pg_index.indisexclusion = true but no matching pg_constraint row exists on the transient table.
The failure is clean — the worker's transaction aborts before any storage swap, the source table is untouched, and no replication slot or transient table is left behind — but it makes any table carrying an EXCLUDE constraint unsqueezable.
Environment
- pg_squeeze 1.9 (
CREATE EXTENSION on Crunchy Bridge), PostgreSQL 18.4 (aarch64)
- The code path is unchanged at tag
REL1_9_4
Reproduction
CREATE EXTENSION pg_squeeze;
CREATE TABLE excl_min (
id int PRIMARY KEY,
r int4range NOT NULL,
EXCLUDE USING gist (r WITH &&)
);
INSERT INTO excl_min SELECT g, int4range(g * 10, g * 10 + 5) FROM generate_series(1, 1000) g;
SELECT squeeze.squeeze_table('public', 'excl_min');
-- ERROR: exclusion constraint record missing for rel ind_1
Also reproduces with a DEFERRABLE INITIALLY DEFERRED multi-column btree_gist exclusion constraint on a partition leaf — the shape doesn't matter, any exclusion constraint triggers it.
Analysis
build_transient_indexes() (pg_squeeze.c) builds each transient index from BuildIndexInfo(source_index). For an exclusion-constraint index the resulting IndexInfo carries ii_ExclusionOps/Procs/Strats.
index_create() sets pg_index.indisexclusion = (indexInfo->ii_ExclusionOps != NULL), so the transient index is flagged as an exclusion index.
create_transient_table() intentionally creates no constraints on the transient table ("each data change must be committed in the source table before we see it").
get_index_insert_state() (concurrent.c) then calls ExecOpenIndices(), which calls BuildIndexInfo() on the transient index. Because indisexclusion is true, core calls RelationGetExclusionInfo(), which scans pg_constraint for a row with conindid = <transient index> — there is none, so it errors out.
As far as I can tell this has never worked: commit 9759990 ("Do not reset ii_ExclusionOps.", 2018) removed code that nulled ii_ExclusionOps in get_index_insert_state(), but that nulling ran on ri_IndexRelationInfo after ExecOpenIndices() had returned, i.e. after the point where BuildIndexInfo() already errors — so the pre-2018 code would have failed the same way.
Suggested fix
Clear ii_ExclusionOps/Procs/Strats on the IndexInfo before calling index_create() in build_transient_indexes(), so the transient index is a plain index (indisexclusion = false):
- The physical index structure is identical (it depends only on columns/opclasses), so the storage swap remains valid — and the source index's
pg_index row, which keeps its catalog identity through the swap, retains indisexclusion = true with its constraint intact.
- Skipping exclusion checks while applying concurrent changes to the transient table matches the extension's existing rationale (the source relation has already enforced them at commit time — the same argument commit 9759990's message makes, and the reason the
recheck list from ExecInsertIndexTuples() is already discarded).
Happy to send a PR along these lines if that approach sounds right.
Summary
squeeze.squeeze_table()fails on any table that has an exclusion constraint:The error is raised by PostgreSQL core (
RelationGetExclusionInfoinsrc/backend/utils/cache/relcache.c) while pg_squeeze opens the transient table's indexes, because the transient copy of the exclusion-constraint index is created withpg_index.indisexclusion = truebut no matchingpg_constraintrow exists on the transient table.The failure is clean — the worker's transaction aborts before any storage swap, the source table is untouched, and no replication slot or transient table is left behind — but it makes any table carrying an
EXCLUDEconstraint unsqueezable.Environment
CREATE EXTENSIONon Crunchy Bridge), PostgreSQL 18.4 (aarch64)REL1_9_4Reproduction
Also reproduces with a
DEFERRABLE INITIALLY DEFERREDmulti-columnbtree_gistexclusion constraint on a partition leaf — the shape doesn't matter, any exclusion constraint triggers it.Analysis
build_transient_indexes()(pg_squeeze.c) builds each transient index fromBuildIndexInfo(source_index). For an exclusion-constraint index the resultingIndexInfocarriesii_ExclusionOps/Procs/Strats.index_create()setspg_index.indisexclusion = (indexInfo->ii_ExclusionOps != NULL), so the transient index is flagged as an exclusion index.create_transient_table()intentionally creates no constraints on the transient table ("each data change must be committed in the source table before we see it").get_index_insert_state()(concurrent.c) then callsExecOpenIndices(), which callsBuildIndexInfo()on the transient index. Becauseindisexclusionis true, core callsRelationGetExclusionInfo(), which scanspg_constraintfor a row withconindid = <transient index>— there is none, so it errors out.As far as I can tell this has never worked: commit 9759990 ("Do not reset ii_ExclusionOps.", 2018) removed code that nulled
ii_ExclusionOpsinget_index_insert_state(), but that nulling ran onri_IndexRelationInfoafterExecOpenIndices()had returned, i.e. after the point whereBuildIndexInfo()already errors — so the pre-2018 code would have failed the same way.Suggested fix
Clear
ii_ExclusionOps/Procs/Stratson theIndexInfobefore callingindex_create()inbuild_transient_indexes(), so the transient index is a plain index (indisexclusion = false):pg_indexrow, which keeps its catalog identity through the swap, retainsindisexclusion = truewith its constraint intact.rechecklist fromExecInsertIndexTuples()is already discarded).Happy to send a PR along these lines if that approach sounds right.