Skip to content

squeeze_table() fails on tables with exclusion constraints: "exclusion constraint record missing for rel ind_N" #94

Description

@alexspeller

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

  1. 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.
  2. index_create() sets pg_index.indisexclusion = (indexInfo->ii_ExclusionOps != NULL), so the transient index is flagged as an exclusion index.
  3. 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").
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions