fix: make migration 020 idempotent to unblock crash-looping app #185

Merged
forgejo_admin merged 1 commit from 184-fix-migration-020-idempotent into main 2026-03-27 06:25:09 +00:00

Summary

Migration 020 (add custom_notes to players) was applied to the production DB but never stamped in alembic_version. On redeploy, Alembic re-runs 020 and hits "column already exists," crash-looping the pod. This fix wraps the add_column/drop_column calls in an information_schema.columns existence check so the migration is safe to re-run regardless of prior DB state.

Changes

  • alembic/versions/020_add_custom_notes_to_player.py: Added _column_exists() helper that queries information_schema.columns. Wrapped upgrade() in a "column does not exist" guard and downgrade() in a "column exists" guard.

Test Plan

  • ruff format and ruff check pass
  • pytest tests/ -x passes (568/568)
  • Alembic upgrade runs cleanly on fresh DB (deploy pipeline)
  • Alembic upgrade runs cleanly on DB with 020 pre-applied (the current prod state)
  • Pod starts without crash-loop after deploy

Review Checklist

  • Passed automated review-fix loop
  • No secrets committed
  • No unnecessary file changes
  • Commit messages are descriptive
  • forgejo_admin/basketball-api #184 -- the Forgejo issue this PR fixes
  • project-westside-basketball -- the project this work belongs to
  • Closes #184
## Summary Migration 020 (add `custom_notes` to `players`) was applied to the production DB but never stamped in `alembic_version`. On redeploy, Alembic re-runs 020 and hits "column already exists," crash-looping the pod. This fix wraps the `add_column`/`drop_column` calls in an `information_schema.columns` existence check so the migration is safe to re-run regardless of prior DB state. ## Changes - `alembic/versions/020_add_custom_notes_to_player.py`: Added `_column_exists()` helper that queries `information_schema.columns`. Wrapped `upgrade()` in a "column does not exist" guard and `downgrade()` in a "column exists" guard. ## Test Plan - [x] `ruff format` and `ruff check` pass - [x] `pytest tests/ -x` passes (568/568) - [ ] Alembic upgrade runs cleanly on fresh DB (deploy pipeline) - [ ] Alembic upgrade runs cleanly on DB with 020 pre-applied (the current prod state) - [ ] Pod starts without crash-loop after deploy ## Review Checklist - [x] Passed automated review-fix loop - [x] No secrets committed - [x] No unnecessary file changes - [x] Commit messages are descriptive ## Related Notes - `forgejo_admin/basketball-api #184` -- the Forgejo issue this PR fixes - `project-westside-basketball` -- the project this work belongs to - Closes #184
fix: make migration 020 idempotent to unblock crash-looping app
All checks were successful
ci/woodpecker/pr/woodpecker Pipeline was successful
446cb0df29
Migration 020 (add custom_notes to players) was applied to the DB but
never stamped in alembic_version. On redeploy, Alembic re-runs 020 and
hits "column already exists," crash-looping the pod.

Wrap both upgrade and downgrade in an information_schema check so the
migration is safe to re-run regardless of prior DB state.

Refs: #184

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author
Owner

QA Review -- PR #185

Diff Summary

Single file change: alembic/versions/020_add_custom_notes_to_player.py (+17/-2). Adds a _column_exists() helper querying information_schema.columns, then guards both upgrade() and downgrade() with existence checks.

Findings

Correctness

  • Parameterized query via sa.text() with :table / :column bind params -- safe against injection.
  • upgrade() skips add_column if column already exists -- handles the "applied but not stamped" production state.
  • downgrade() skips drop_column if column is already gone -- symmetric idempotency.
  • Helper uses op.get_bind() which is the standard Alembic pattern for raw SQL during migrations.

Scope

  • Only migration 020 modified. No other files touched. Minimal, focused fix.

Nit (non-blocking)

  • The information_schema.columns query does not filter by table_schema. In a multi-schema DB, a players.custom_notes column in another schema could cause a false positive. Not a concern for this single-schema codebase, but worth noting if schema isolation is ever added.

Tests

  • 568/568 tests pass. Ruff format and check clean.

SOP Compliance

  • PR body has all required sections (Summary, Changes, Test Plan, Review Checklist, Related Notes).
  • Closes #184 present.
  • Commit message references Refs: #184.

VERDICT: APPROVE

Clean, minimal fix that directly addresses the crash-loop. No blockers.

## QA Review -- PR #185 ### Diff Summary Single file change: `alembic/versions/020_add_custom_notes_to_player.py` (+17/-2). Adds a `_column_exists()` helper querying `information_schema.columns`, then guards both `upgrade()` and `downgrade()` with existence checks. ### Findings **Correctness** - Parameterized query via `sa.text()` with `:table` / `:column` bind params -- safe against injection. - `upgrade()` skips `add_column` if column already exists -- handles the "applied but not stamped" production state. - `downgrade()` skips `drop_column` if column is already gone -- symmetric idempotency. - Helper uses `op.get_bind()` which is the standard Alembic pattern for raw SQL during migrations. **Scope** - Only migration 020 modified. No other files touched. Minimal, focused fix. **Nit (non-blocking)** - The `information_schema.columns` query does not filter by `table_schema`. In a multi-schema DB, a `players.custom_notes` column in another schema could cause a false positive. Not a concern for this single-schema codebase, but worth noting if schema isolation is ever added. **Tests** - 568/568 tests pass. Ruff format and check clean. **SOP Compliance** - PR body has all required sections (Summary, Changes, Test Plan, Review Checklist, Related Notes). - `Closes #184` present. - Commit message references `Refs: #184`. ### VERDICT: APPROVE Clean, minimal fix that directly addresses the crash-loop. No blockers.
forgejo_admin deleted branch 184-fix-migration-020-idempotent 2026-03-27 06:25:09 +00:00
Sign in to join this conversation.
No description provided.