fix(migrations): idempotent schema helpers so drift can't crash startup #13

Merged
elirtf merged 1 commit from feat/reliability-hardening into master 2026-06-22 20:38:42 +00:00
Owner

A database whose schema had drifted from the recorded migration history
crashed the entire app at boot: several migrations issued bare or
inconsistently-guarded ALTER TABLE ... ADD COLUMN statements, which raise
duplicate column name when the column is already present (restored backup,
out-of-band change, interrupted earlier run). The runner re-raises to avoid
serving a half-migrated schema, so one already-applied column took the whole
service down — surfacing only as an opaque gunicorn traceback.

  • Add app/migrations/_helpers.py with idempotent add_column, create_index,
    table_exists, and column_names. add_column no-ops when the column
    exists and defensively swallows SQLite's duplicate column name, but still
    raises loudly if the target table is missing (a real ordering bug).
  • Convert all 11 column-adding migrations to the helper. Fixes the unguarded
    009 (ALTER TABLE user ADD COLUMN api_token_hash) and replaces the
    over-broad contextlib.suppress(Exception) in 005/007 (which also hid
    unrelated errors) with the targeted helper.
  • Make run_migrations log the DB path and emit an actionable remediation on
    failure (which DB, which migration, how to recover) instead of a bare trace.
  • Add tests/test_migrations.py covering a fresh full-chain apply, idempotent
    re-run, and a drift case (pre-existing notes column) that reproduces the
    exact production failure.

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com

A database whose schema had drifted from the recorded migration history crashed the entire app at boot: several migrations issued bare or inconsistently-guarded `ALTER TABLE ... ADD COLUMN` statements, which raise `duplicate column name` when the column is already present (restored backup, out-of-band change, interrupted earlier run). The runner re-raises to avoid serving a half-migrated schema, so one already-applied column took the whole service down — surfacing only as an opaque gunicorn traceback. - Add app/migrations/_helpers.py with idempotent `add_column`, `create_index`, `table_exists`, and `column_names`. `add_column` no-ops when the column exists and defensively swallows SQLite's `duplicate column name`, but still raises loudly if the target table is missing (a real ordering bug). - Convert all 11 column-adding migrations to the helper. Fixes the unguarded 009 (`ALTER TABLE user ADD COLUMN api_token_hash`) and replaces the over-broad `contextlib.suppress(Exception)` in 005/007 (which also hid unrelated errors) with the targeted helper. - Make run_migrations log the DB path and emit an actionable remediation on failure (which DB, which migration, how to recover) instead of a bare trace. - Add tests/test_migrations.py covering a fresh full-chain apply, idempotent re-run, and a drift case (pre-existing `notes` column) that reproduces the exact production failure. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A database whose schema had drifted from the recorded migration history
crashed the entire app at boot: several migrations issued bare or
inconsistently-guarded `ALTER TABLE ... ADD COLUMN` statements, which raise
`duplicate column name` when the column is already present (restored backup,
out-of-band change, interrupted earlier run). The runner re-raises to avoid
serving a half-migrated schema, so one already-applied column took the whole
service down — surfacing only as an opaque gunicorn traceback.

- Add app/migrations/_helpers.py with idempotent `add_column`, `create_index`,
  `table_exists`, and `column_names`. `add_column` no-ops when the column
  exists and defensively swallows SQLite's `duplicate column name`, but still
  raises loudly if the target table is missing (a real ordering bug).
- Convert all 11 column-adding migrations to the helper. Fixes the unguarded
  009 (`ALTER TABLE user ADD COLUMN api_token_hash`) and replaces the
  over-broad `contextlib.suppress(Exception)` in 005/007 (which also hid
  unrelated errors) with the targeted helper.
- Make run_migrations log the DB path and emit an actionable remediation on
  failure (which DB, which migration, how to recover) instead of a bare trace.
- Add tests/test_migrations.py covering a fresh full-chain apply, idempotent
  re-run, and a drift case (pre-existing `notes` column) that reproduces the
  exact production failure.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
elirtf merged commit 2e1a7aeeff into master 2026-06-22 20:38:42 +00:00
elirtf deleted branch feat/reliability-hardening 2026-06-22 20:38:42 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
elirtf/opus!13
No description provided.