Skip to content

feat: detect and repair index definition drift during migrations - #134

Open
jerenkrantz wants to merge 2 commits into
mainfrom
jerenkrantz/index-mutation-detection
Open

feat: detect and repair index definition drift during migrations#134
jerenkrantz wants to merge 2 commits into
mainfrom
jerenkrantz/index-mutation-detection

Conversation

@jerenkrantz

@jerenkrantz jerenkrantz commented Feb 25, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Migrations() now detects when an existing index's definition has drifted from the expected state and automatically repairs it
  • Previously, only index existence was checked — if an index existed by name but had different columns, method, or predicate, the drift was silently ignored
  • Compares against PostgreSQL's canonical pg_indexes.indexdef as the source of truth, handling all index types (BTREE, GIN, BTREE_GIN, HNSW), UNIQUE indexes, WHERE predicates, override expressions, and partitioned tables (ON ONLY)

Migration strategy

Uses a create-then-drop approach to avoid any window without an index:

  1. CREATE INDEX CONCURRENTLY IF NOT EXISTS "<name>_new" ... — builds the replacement index (non-blocking)
  2. DROP INDEX CONCURRENTLY IF EXISTS "<name>" — removes the stale index (non-blocking)
  3. ALTER INDEX "<name>_new" RENAME TO "<name>" — instant catalog-only rename

CONCURRENTLY is used for non-partitioned tables (matching existing behavior via index2sql). This ensures queries always have at least one usable index during the transition.

Drift detection only triggers when index2expectedDef() produces a definition that differs from what pg_indexes.indexdef actually stores — unchanged indexes are skipped with zero overhead.

Test plan

  • Unit tests for pgQuoteIdent, pgNormalizeExpr, index2expectedDef, indexRename2sql covering all index types, partitioned tables, uppercase names, dropped/primary indexes
  • Integration test TestMigrationIndexMutation — creates schema, introduces drift, verifies the 3-step CREATE/DROP/RENAME migration, and confirms convergence
  • All existing tests pass (animals, food, zoo, city, pgdb/v1)

🤖 Generated with Claude Code

jerenkrantz and others added 2 commits February 25, 2026 12:45
Previously, Migrations() only checked whether an index existed by name.
If an index existed but its definition had changed (different columns,
method, predicate, etc.), the drift was silently ignored.

Now readIndexes() fetches the indexdef from pg_indexes and compares it
against the expected definition. When drift is detected, a DROP INDEX
followed by CREATE INDEX is emitted to bring the index back in sync.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Instead of DROP then CREATE (which leaves a window with no index),
use a three-step approach:
1. CREATE INDEX with a temporary name (_new suffix)
2. DROP INDEX the old one
3. ALTER INDEX RENAME the temp to the original name

This ensures there is always at least one usable index available
during the transition. The CONCURRENTLY keyword prevents blocking
concurrent queries during both the create and drop phases.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant