from __future__ import annotations import logging from collections.abc import Iterator from sqlalchemy import Engine, create_engine, inspect, text from sqlalchemy.orm import Session, sessionmaker from app.config import REPO_ROOT, get_settings from app.models import Base logger = logging.getLogger(__name__) _settings = get_settings() # Additive migrations for existing databases. ``create_all`` only creates missing # tables; it never alters existing ones, so new nullable/defaulted columns are # added here as (table, column, DDL type) tuples. _ADDITIVE_COLUMNS: list[tuple[str, str, str]] = [ ("strategy_configs", "name", "VARCHAR(80) DEFAULT ''"), ] def _normalize_url(url: str) -> tuple[str, dict]: connect_args: dict = {} if url.startswith("sqlite"): prefix, _, path = url.partition("sqlite:///") if path and not path.startswith("/"): url = f"{prefix}sqlite:///{REPO_ROOT / path}" connect_args = {"check_same_thread": False} return url, connect_args _url, _connect_args = _normalize_url(_settings.database_url) engine = create_engine(_url, connect_args=_connect_args, pool_pre_ping=True) SessionLocal = sessionmaker(bind=engine, autoflush=False, expire_on_commit=False) def _apply_additive_columns(target: Engine) -> None: inspector = inspect(target) existing_tables = set(inspector.get_table_names()) with target.begin() as connection: for table, column, ddl in _ADDITIVE_COLUMNS: if table not in existing_tables: continue columns = {col["name"] for col in inspector.get_columns(table)} if column in columns: continue logger.info("migrating: adding %s.%s", table, column) connection.execute(text(f"ALTER TABLE {table} ADD COLUMN {column} {ddl}")) def init_db() -> None: Base.metadata.create_all(bind=engine) _apply_additive_columns(engine) def get_session() -> Iterator[Session]: session = SessionLocal() try: yield session finally: session.close()