Skip to content

fix(hasura): make 1741520400001 teams_name_unique migration idempotent#46

Merged
acmeguy merged 1 commit intomainfrom
fix/idempotent-teams-name-migration
Apr 21, 2026
Merged

fix(hasura): make 1741520400001 teams_name_unique migration idempotent#46
acmeguy merged 1 commit intomainfrom
fix/idempotent-teams-name-migration

Conversation

@acmeguy
Copy link
Copy Markdown

@acmeguy acmeguy commented Apr 21, 2026

Summary

Unblocks the ArgoCD hasura-migrations-job PostSync hook, which has been failing on every sync and blocks all future Synmetrix deploys — including the recently merged Model Management API (#45).

The migration was previously:

DROP INDEX IF EXISTS public.teams_name_unique;
ALTER TABLE public.teams ADD CONSTRAINT teams_name_unique UNIQUE (name);

This crashes on any environment where the constraint was already created (locally on retry, or in the intermediate state after a partial apply):

cannot drop index teams_name_unique because constraint
teams_name_unique on table teams requires it

DROP INDEX refuses to drop an index that backs a UNIQUE constraint — the constraint owns the index.

Fix

Replace the two blind statements with a DO block that branches on the current DB state and short-circuits when the desired end state already holds:

Starting state Action
(a) expression index teams_name_unique exists without a constraint DROP INDEX then ADD CONSTRAINT
(b) plain UNIQUE constraint teams_name_unique already present no-op, return
(c) neither exists ADD CONSTRAINT

Detected via pg_constraint + pg_indexes lookups rather than bare SQL statements.

Also guarded

The pre-existing UPDATE public.teams SET name = lower(trim(name)) … was itself failing with a duplicate-key violation on environments where two rows normalize to the same value ("Default team" + "default team" is a real case observed on the dev cluster and locally). The UPDATE now skips rows whose normalized form already exists in another row. The constraint is case-sensitive on the exact stored string, so the two rows coexist under the new constraint and the application (which always writes lower(trim(…))) only ever writes the normalized form going forward.

Test plan

  • Applied forward against local dev DB in state (b): success in <1 s, migrate status shows every migration Present/Present.
  • node --test on services/cubejs + services/actions: 475 / 479 pass (the 4 pre-existing unrelated failures unchanged).
  • scripts/lint-error-codes.mjs: green.

State (c) follows the direct ADD CONSTRAINT path — trivially correct.
State (a) reproduces the original forward intent — DROP INDEX + ADD CONSTRAINT — and was the path the old migration was already trying to run; it just needed to be gated so it doesn't run in state (b).

Not in scope: fixing the symmetric bug in down.sql (cannot recreate the expression index while duplicate normalized values exist). That path is only hit during intentional rollbacks and is not on the deploy critical path.

🤖 Generated with Claude Code

The migration was failing on every environment where the constraint had
already been created, with:

  cannot drop index teams_name_unique because constraint
  teams_name_unique on table teams requires it

The original script did a blind `DROP INDEX IF EXISTS` followed by
`ADD CONSTRAINT`, which breaks when the constraint backs the index (the
constraint owns the index, so DROP INDEX is rejected). This made the
ArgoCD `hasura-migrations-job` PostSync hook fail on every sync and
blocked feature 011 from deploying.

Replace the two statements with a DO block that branches on current state:

  (a) expression index exists alone          → DROP INDEX, ADD CONSTRAINT
  (b) constraint exists                      → no-op
  (c) neither exists                         → ADD CONSTRAINT

Also guard the pre-existing name-normalisation UPDATE against a duplicate
in cases where two rows normalise to the same value — the UPDATE itself
was blowing up on unique-violation before the new constraint path even ran.

Verified locally against state (b): migrate apply succeeds in <1 s and
`migrate status` shows every migration Present/Present. Case (a) is
reachable on fresh environments where the previous migration hadn't been
applied; the branch logic is a straight DROP INDEX + ADD CONSTRAINT there
which mirrors the original intent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@akshaykumar2505 akshaykumar2505 self-requested a review April 21, 2026 06:33
@acmeguy acmeguy merged commit 59e8b6b into main Apr 21, 2026
3 checks passed
@acmeguy acmeguy deleted the fix/idempotent-teams-name-migration branch April 21, 2026 06:33
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.

3 participants