diff --git a/guarddog_nexus/database.py b/guarddog_nexus/database.py index 95fa759..ebab461 100644 --- a/guarddog_nexus/database.py +++ b/guarddog_nexus/database.py @@ -1,9 +1,12 @@ """Async SQLite database setup via SQLAlchemy.""" + +from sqlalchemy import inspect, text from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine from sqlalchemy.orm import DeclarativeBase from guarddog_nexus.config import config +from guarddog_nexus.logging_setup import log DATABASE_URL = f"sqlite+aiosqlite:///{config.database_path}" @@ -15,11 +18,56 @@ class Base(DeclarativeBase): pass +async def _migrate(): + """Add any missing columns from model definitions to existing SQLite tables.""" + import guarddog_nexus.models # noqa: F401 + + async with _engine.connect() as conn: + for table in Base.metadata.sorted_tables: + # Get existing columns in DB + col_names = [] + try: + existing = await conn.run_sync( + lambda c: [col["name"] for col in inspect(c).get_columns(table.name)] + ) + col_names = existing + except Exception: + continue + + # Add missing model columns + for col in table.columns: + if col.name not in col_names: + col_type = col.type.compile(_engine.dialect) + nullable = "" if col.nullable else " NOT NULL" + default = "" + if col.default and col.default.arg is not None: + default_val = col.default.arg + if isinstance(default_val, str): + default = f" DEFAULT '{default_val}'" + else: + default = f" DEFAULT {default_val}" + if col.server_default: + # Skip — func.now() etc. not trivially stringable + pass + + sql = ( + f"ALTER TABLE {table.name} ADD COLUMN " + f"{col.name} {col_type}{nullable}{default}" + ) + log.info("Migration: %s", sql) + try: + await conn.execute(text(sql)) + await conn.commit() + except Exception as e: + log.warning("Migration skipped (may already exist): %s", e) + + async def init_db(): import guarddog_nexus.models # noqa: F401 async with _engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) + await _migrate() async def get_session() -> AsyncSession: