fix(database): auto-reset orphaned Alembic revisions

This commit is contained in:
2026-08-25 16:06:14 +03:00
parent 4b9619e229
commit 8a3bba139e
2 changed files with 23 additions and 9 deletions

View File

@@ -7,6 +7,9 @@ from pathlib import Path
import sys
from alembic.config import Config
from alembic.script import ScriptDirectory
from alembic.script.revision import ResolutionError
from alembic.util.exc import CommandError
from sqlalchemy import create_engine, inspect, text
from alembic import command
@@ -20,16 +23,22 @@ def _enabled(value: str | None) -> bool:
return str(value or "").strip().lower() in {"1", "true", "yes", "y"}
def _maybe_reset(connection) -> None:
def _revision_is_known(config: Config, revision: str) -> bool:
try:
return ScriptDirectory.from_config(config).get_revision(revision) is not None
except (CommandError, ResolutionError):
return False
def _maybe_reset(connection, config: Config) -> None:
reset_enabled = _enabled(os.getenv("RESET_DATABASE_SCHEMA"))
sys.stderr.write(
f"[database] RESET_DATABASE_SCHEMA={'enabled' if reset_enabled else 'disabled'}\n"
)
if not reset_enabled:
return
if connection.dialect.name != "postgresql":
raise RuntimeError("RESET_DATABASE_SCHEMA is supported only for PostgreSQL")
if reset_enabled:
raise RuntimeError("RESET_DATABASE_SCHEMA is supported only for PostgreSQL")
return
tables = set(inspect(connection).get_table_names())
revision = None
@@ -42,6 +51,9 @@ def _maybe_reset(connection) -> None:
sys.stderr.write("[database] Baseline already applied; one-shot reset skipped\n")
connection.commit()
return
orphaned_revision = bool(revision) and not _revision_is_known(config, revision)
if not reset_enabled and not orphaned_revision:
return
if tables and revision is None:
raise RuntimeError(
"Database schema is non-empty but has no Alembic revision; "
@@ -52,7 +64,8 @@ def _maybe_reset(connection) -> None:
connection.execute(text("CREATE SCHEMA public"))
connection.commit()
sys.stderr.write(
f"[database] Legacy Alembic revision {revision or '<empty>'} reset successfully\n"
f"[database] {'Orphaned' if orphaned_revision else 'Legacy'} Alembic revision "
f"{revision or '<empty>'} reset successfully\n"
)
@@ -60,14 +73,14 @@ def prepare_database() -> None:
engine = create_engine(database_url(), pool_pre_ping=True)
try:
with engine.connect() as connection:
config = Config(str(Path(__file__).resolve().parents[2] / "alembic.ini"))
locked = connection.dialect.name == "postgresql"
if locked:
connection.execute(text("SELECT pg_advisory_lock(:lock_id)"), {"lock_id": _MIGRATION_LOCK_ID})
connection.commit()
try:
_maybe_reset(connection)
_maybe_reset(connection, config)
connection.commit()
config = Config(str(Path(__file__).resolve().parents[2] / "alembic.ini"))
config.attributes["connection"] = connection
command.upgrade(config, "head")
finally:

View File

@@ -74,7 +74,8 @@ def test_prepare_database_resets_any_legacy_revision(
connection.execute(text("CREATE TABLE legacy_probe (id INTEGER PRIMARY KEY)"))
monkeypatch.setenv("DATABASE_URL", migration_database_url)
monkeypatch.setenv("RESET_DATABASE_SCHEMA", "true")
# Orphaned revisions are reset independently of stale Compose defaults.
monkeypatch.setenv("RESET_DATABASE_SCHEMA", "false")
monkeypatch.delenv("RESET_DATABASE_FROM_REVISION", raising=False)
monkeypatch.delenv("RESET_DATABASE_NAME", raising=False)