refactor(db): run Alembic at boot, retire ad-hoc _migrate_* helpers

initialize() now delegates to _apply_schema(): real boots run
'alembic upgrade head' (schema owned by the migration history); tests
(DECNET_TESTING=1) keep create_all, which is faster and needs no upgrade
path. MySQL wraps the upgrade in the existing GET_LOCK advisory lock so
concurrent uvicorn workers don't race on DDL.

Deletes the three _migrate_* crimes (attackers-table legacy drop +
GeoIP backfill, TEXT->MEDIUMTEXT widening) — all now handled by the
baseline migration and the _BIG_TEXT model variants. Drops the test
file that only exercised the deleted helpers; adds tests pinning the
alembic-vs-create_all gate and guarding that every model table is in
the migration head.
This commit is contained in:
2026-06-16 16:31:10 -04:00
parent ef4d67cbef
commit 372375194c
6 changed files with 157 additions and 358 deletions

View File

@@ -6,8 +6,7 @@ Contains all dialect-portable query code used by the SQLite and MySQL
backends. Dialect-specific behavior lives in subclasses:
* engine/session construction (``__init__``)
* ``_migrate_attackers_table`` (legacy schema check; DDL introspection
is not portable)
* ``_apply_schema`` (MySQL wraps the Alembic upgrade in an advisory lock)
* ``get_log_histogram`` (date-bucket expression differs per dialect)
"""
from __future__ import annotations
@@ -103,14 +102,27 @@ class SQLModelRepository(
# ------------------------------------------------------------ lifecycle
async def initialize(self) -> None:
"""Create tables if absent and seed the admin user."""
from sqlmodel import SQLModel
await self._migrate_attackers_table()
async with self.engine.begin() as conn:
await conn.run_sync(SQLModel.metadata.create_all)
"""Bring the schema up to date and seed the admin user."""
await self._apply_schema()
await self._ensure_admin_user()
await self._ensure_contract_user()
async def _apply_schema(self) -> None:
"""Create/upgrade tables.
Real boots run Alembic migrations — the schema is owned by the
versioned migration history. Test/ephemeral DBs (``DECNET_TESTING=1``)
skip Alembic and use ``create_all``: faster, and an in-memory/throwaway
DB never needs an upgrade path.
"""
from sqlmodel import SQLModel
if os.environ.get("DECNET_TESTING") == "1":
async with self.engine.begin() as conn:
await conn.run_sync(SQLModel.metadata.create_all)
return
from decnet.web.db.migrate import run_migrations
await run_migrations(self.engine)
async def reinitialize(self) -> None:
"""Re-create schema (for tests / reset flows). Does NOT drop existing tables."""
from sqlmodel import SQLModel
@@ -165,10 +177,6 @@ class SQLModelRepository(
))
await session.commit()
async def _migrate_attackers_table(self) -> None:
"""Legacy-schema cleanup. Override per dialect (DDL introspection is non-portable)."""
return None
async def get_deckies(self) -> List[dict]:
# The fleet inventory the UI/API sees is fleet_deckies — the
# engine-mirrored table written on EVERY deploy/teardown (CLI or web),