"""Alembic environment for second-brain — second_brain schema. Mirrors vault-mcp/alembic/env.py. Reads SECOND_BRAIN_DATABASE_URL (with HERBYLAB_DATABASE_URL as a fallback) so the same migration tree works across local dev, CI, and production. The connection bootstraps the `second_brain` schema and pins alembic's own version table to that schema; migration scripts themselves set search_path before issuing unqualified DDL. The bootstrap-then-commit pattern below is the fix for the "autobegin trap": running any further statement before context.begin_transaction() would autobegin a new transaction, causing alembic's begin_transaction() to nest as a savepoint that silently rolls back when the connection closes. Do not regress this. """ from __future__ import annotations import os from logging.config import fileConfig from dotenv import load_dotenv from sqlalchemy import engine_from_config, pool from alembic import context # Make the project's `src/` importable so `second_brain.models` resolves # without a `pip install -e .`. import sys from pathlib import Path _PROJECT_ROOT = Path(__file__).resolve().parent.parent sys.path.insert(0, str(_PROJECT_ROOT / "src")) from second_brain.models import Base, SCHEMA # noqa: E402 load_dotenv() config = context.config if config.config_file_name is not None: fileConfig(config.config_file_name) def _resolve_db_url() -> str | None: """Pick the first usable URL out of the two homelab conventions.""" for var in ("SECOND_BRAIN_DATABASE_URL", "HERBYLAB_DATABASE_URL"): v = os.environ.get(var) if v: return v return None _db_url = _resolve_db_url() if _db_url: config.set_main_option("sqlalchemy.url", _db_url) target_metadata = Base.metadata def run_migrations_offline() -> None: url = config.get_main_option("sqlalchemy.url") context.configure( url=url, target_metadata=target_metadata, literal_binds=True, dialect_opts={"paramstyle": "named"}, version_table_schema=SCHEMA, include_schemas=True, ) with context.begin_transaction(): context.run_migrations() def run_migrations_online() -> None: connectable = engine_from_config( config.get_section(config.config_ini_section, {}), prefix="sqlalchemy.", poolclass=pool.NullPool, ) with connectable.connect() as connection: # Bootstrap the schema before alembic looks for alembic_version. # The runtime role (lovebug) does NOT have CREATE on the petalbrain # database — bare `CREATE SCHEMA IF NOT EXISTS` would fail with # permission denied even though the schema already exists. So we # only attempt to create it when the schema is genuinely missing # (operator must have bootstrapped it once as postgres superuser: # `CREATE SCHEMA second_brain AUTHORIZATION lovebug`). # Commit explicitly so any DDL persists; otherwise the implicit # transaction is rolled back when the connection closes. Avoid # running any further statement here — it would autobegin a new # transaction and cause alembic's `begin_transaction()` to nest as # a savepoint, which silently rolls back when the connection closes. # The migration script sets search_path itself. row = connection.exec_driver_sql( "SELECT 1 FROM pg_namespace WHERE nspname = %s", (SCHEMA,), ).fetchone() if row is None: connection.exec_driver_sql(f"CREATE SCHEMA {SCHEMA}") connection.commit() context.configure( connection=connection, target_metadata=target_metadata, version_table_schema=SCHEMA, include_schemas=True, ) with context.begin_transaction(): context.run_migrations() if context.is_offline_mode(): run_migrations_offline() else: run_migrations_online()