generated from john/python-template
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
"""One-time migration for V6.1 maintenance-run storage.
|
||||
|
||||
This migration aligns the ``maintenance_run`` table with the V6.1 model.
|
||||
|
||||
Steps:
|
||||
1. Create ``maintenance_run`` if absent.
|
||||
2. If present, add missing non-critical columns.
|
||||
3. Refuse unsafe patching when critical identity/state columns are missing.
|
||||
|
||||
Usage::
|
||||
|
||||
uv run python tools/migrate_v60_to_v61.py --dry-run
|
||||
uv run python tools/migrate_v60_to_v61.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from collections.abc import Sequence
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy import inspect as sqlalchemy_inspect
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.engine import Connection
|
||||
from sqlmodel import SQLModel
|
||||
|
||||
from transcription.config import Settings
|
||||
from transcription.config import get_settings
|
||||
from transcription.db import models as _models # noqa: F401 (registers metadata tables)
|
||||
from transcription.db.engine import get_database_url
|
||||
|
||||
TABLE_NAME = "maintenance_run"
|
||||
CRITICAL_COLUMNS = frozenset({"id", "job_type", "status"})
|
||||
|
||||
|
||||
def _sync_url(settings: Settings) -> str:
|
||||
return get_database_url(settings).replace("+aiosqlite", "").replace("+asyncpg", "").replace("+psycopg", "")
|
||||
|
||||
|
||||
def _existing_columns(connection: Connection) -> set[str]:
|
||||
inspector = sqlalchemy_inspect(connection)
|
||||
return {column["name"] for column in inspector.get_columns(TABLE_NAME)}
|
||||
|
||||
|
||||
def _add_missing_columns(connection: Connection, *, dry_run: bool) -> int:
|
||||
table = SQLModel.metadata.tables[TABLE_NAME]
|
||||
existing = _existing_columns(connection)
|
||||
missing = [column for column in table.columns if column.name not in existing]
|
||||
if not missing:
|
||||
print(" no missing columns")
|
||||
return 0
|
||||
|
||||
for column in missing:
|
||||
type_sql = column.type.compile(dialect=connection.dialect)
|
||||
statement = f'alter table "{TABLE_NAME}" add column "{column.name}" {type_sql}'
|
||||
print(f" add column {column.name} {type_sql}")
|
||||
if not dry_run:
|
||||
connection.execute(text(statement))
|
||||
return len(missing)
|
||||
|
||||
|
||||
def migrate_maintenance_run_schema(connection: Connection, *, dry_run: bool) -> None:
|
||||
inspector = sqlalchemy_inspect(connection)
|
||||
if TABLE_NAME not in set(inspector.get_table_names()):
|
||||
print(f" create table {TABLE_NAME}")
|
||||
if not dry_run:
|
||||
SQLModel.metadata.tables[TABLE_NAME].create(connection)
|
||||
return
|
||||
|
||||
existing = _existing_columns(connection)
|
||||
missing_critical = sorted(CRITICAL_COLUMNS - existing)
|
||||
if missing_critical:
|
||||
raise RuntimeError(
|
||||
"Existing maintenance_run table is missing critical columns "
|
||||
f"{missing_critical}; run export/import migration instead of patching in place."
|
||||
)
|
||||
|
||||
_add_missing_columns(connection, dry_run=dry_run)
|
||||
|
||||
|
||||
def migrate(*, settings: Settings, dry_run: bool) -> None:
|
||||
engine = create_engine(_sync_url(settings))
|
||||
try:
|
||||
with engine.begin() as connection:
|
||||
print("Step 1: align maintenance_run schema")
|
||||
migrate_maintenance_run_schema(connection, dry_run=dry_run)
|
||||
finally:
|
||||
engine.dispose()
|
||||
|
||||
if dry_run:
|
||||
print("\nDry run: nothing was written.")
|
||||
else:
|
||||
print("\nDone.")
|
||||
|
||||
|
||||
def main(argv: Sequence[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--dry-run", action="store_true", help="Report planned changes without writing")
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
settings = get_settings()
|
||||
print(f"Target: {_sync_url(settings)}")
|
||||
migrate(settings=settings, dry_run=args.dry_run)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user