# [DEF:backend.src.core.database:Module] # # @TIER: STANDARD # @SEMANTICS: database, postgresql, sqlalchemy, session, persistence # @PURPOSE: Configures database connection and session management (PostgreSQL-first). # @LAYER: Core # @RELATION: DEPENDS_ON ->[sqlalchemy] # @RELATION: DEPENDS_ON ->[backend.src.models.mapping] # @RELATION: DEPENDS_ON ->[backend.src.core.auth.config] # # @INVARIANT: A single engine instance is used for the entire application. # [SECTION: IMPORTS] from sqlalchemy import create_engine, inspect, text from sqlalchemy.orm import sessionmaker from ..models.mapping import Base from ..models.connection import ConnectionConfig # Import models to ensure they're registered with Base from ..models import task as _task_models # noqa: F401 from ..models import auth as _auth_models # noqa: F401 from ..models import config as _config_models # noqa: F401 from ..models import llm as _llm_models # noqa: F401 from ..models import assistant as _assistant_models # noqa: F401 from ..models import profile as _profile_models # noqa: F401 from ..models import clean_release as _clean_release_models # noqa: F401 from ..models import connection as _connection_models # noqa: F401 from .logger import belief_scope, logger from .auth.config import auth_config import os from pathlib import Path # [/SECTION] # [DEF:BASE_DIR:Variable] # @TIER: TRIVIAL # @PURPOSE: Base directory for the backend. BASE_DIR = Path(__file__).resolve().parent.parent.parent # [/DEF:BASE_DIR:Variable] # [DEF:DATABASE_URL:Constant] # @TIER: TRIVIAL # @PURPOSE: URL for the main application database. DEFAULT_POSTGRES_URL = os.getenv( "POSTGRES_URL", "postgresql+psycopg2://postgres:postgres@localhost:5432/ss_tools", ) DATABASE_URL = os.getenv("DATABASE_URL", DEFAULT_POSTGRES_URL) # [/DEF:DATABASE_URL:Constant] # [DEF:TASKS_DATABASE_URL:Constant] # @TIER: TRIVIAL # @PURPOSE: URL for the tasks execution database. # Defaults to DATABASE_URL to keep task logs in the same PostgreSQL instance. TASKS_DATABASE_URL = os.getenv("TASKS_DATABASE_URL", DATABASE_URL) # [/DEF:TASKS_DATABASE_URL:Constant] # [DEF:AUTH_DATABASE_URL:Constant] # @TIER: TRIVIAL # @PURPOSE: URL for the authentication database. AUTH_DATABASE_URL = os.getenv("AUTH_DATABASE_URL", auth_config.AUTH_DATABASE_URL) # [/DEF:AUTH_DATABASE_URL:Constant] # [DEF:engine:Variable] # @TIER: TRIVIAL # @PURPOSE: SQLAlchemy engine for mappings database. # @SIDE_EFFECT: Creates database engine and manages connection pool. def _build_engine(db_url: str): with belief_scope("_build_engine"): if db_url.startswith("sqlite"): return create_engine(db_url, connect_args={"check_same_thread": False}) return create_engine(db_url, pool_pre_ping=True) engine = _build_engine(DATABASE_URL) # [/DEF:engine:Variable] # [DEF:tasks_engine:Variable] # @TIER: TRIVIAL # @PURPOSE: SQLAlchemy engine for tasks database. tasks_engine = _build_engine(TASKS_DATABASE_URL) # [/DEF:tasks_engine:Variable] # [DEF:auth_engine:Variable] # @TIER: TRIVIAL # @PURPOSE: SQLAlchemy engine for authentication database. auth_engine = _build_engine(AUTH_DATABASE_URL) # [/DEF:auth_engine:Variable] # [DEF:SessionLocal:Class] # @TIER: TRIVIAL # @PURPOSE: A session factory for the main mappings database. # @PRE: engine is initialized. SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) # [/DEF:SessionLocal:Class] # [DEF:TasksSessionLocal:Class] # @TIER: TRIVIAL # @PURPOSE: A session factory for the tasks execution database. # @PRE: tasks_engine is initialized. TasksSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=tasks_engine) # [/DEF:TasksSessionLocal:Class] # [DEF:AuthSessionLocal:Class] # @TIER: TRIVIAL # @PURPOSE: A session factory for the authentication database. # @PRE: auth_engine is initialized. AuthSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=auth_engine) # [/DEF:AuthSessionLocal:Class] # [DEF:_ensure_user_dashboard_preferences_columns:Function] # @TIER: STANDARD # @PURPOSE: Applies additive schema upgrades for user_dashboard_preferences table. # @PRE: bind_engine points to application database where profile table is stored. # @POST: Missing columns are added without data loss. def _ensure_user_dashboard_preferences_columns(bind_engine): with belief_scope("_ensure_user_dashboard_preferences_columns"): table_name = "user_dashboard_preferences" inspector = inspect(bind_engine) if table_name not in inspector.get_table_names(): return existing_columns = { str(column.get("name") or "").strip() for column in inspector.get_columns(table_name) } alter_statements = [] if "git_username" not in existing_columns: alter_statements.append( "ALTER TABLE user_dashboard_preferences ADD COLUMN git_username VARCHAR" ) if "git_email" not in existing_columns: alter_statements.append( "ALTER TABLE user_dashboard_preferences ADD COLUMN git_email VARCHAR" ) if "git_personal_access_token_encrypted" not in existing_columns: alter_statements.append( "ALTER TABLE user_dashboard_preferences " "ADD COLUMN git_personal_access_token_encrypted VARCHAR" ) if "start_page" not in existing_columns: alter_statements.append( "ALTER TABLE user_dashboard_preferences " "ADD COLUMN start_page VARCHAR NOT NULL DEFAULT 'dashboards'" ) if "auto_open_task_drawer" not in existing_columns: alter_statements.append( "ALTER TABLE user_dashboard_preferences " "ADD COLUMN auto_open_task_drawer BOOLEAN NOT NULL DEFAULT TRUE" ) if "dashboards_table_density" not in existing_columns: alter_statements.append( "ALTER TABLE user_dashboard_preferences " "ADD COLUMN dashboards_table_density VARCHAR NOT NULL DEFAULT 'comfortable'" ) if "show_only_slug_dashboards" not in existing_columns: alter_statements.append( "ALTER TABLE user_dashboard_preferences " "ADD COLUMN show_only_slug_dashboards BOOLEAN NOT NULL DEFAULT TRUE" ) if not alter_statements: return try: with bind_engine.begin() as connection: for statement in alter_statements: connection.execute(text(statement)) except Exception as migration_error: logger.warning( "[database][EXPLORE] Profile preference additive migration failed: %s", migration_error, ) # [/DEF:_ensure_user_dashboard_preferences_columns:Function] # [DEF:_ensure_user_dashboard_preferences_health_columns:Function] # @TIER: STANDARD # @PURPOSE: Applies additive schema upgrades for user_dashboard_preferences table (health fields). def _ensure_user_dashboard_preferences_health_columns(bind_engine): with belief_scope("_ensure_user_dashboard_preferences_health_columns"): table_name = "user_dashboard_preferences" inspector = inspect(bind_engine) if table_name not in inspector.get_table_names(): return existing_columns = { str(column.get("name") or "").strip() for column in inspector.get_columns(table_name) } alter_statements = [] if "telegram_id" not in existing_columns: alter_statements.append( "ALTER TABLE user_dashboard_preferences ADD COLUMN telegram_id VARCHAR" ) if "email_address" not in existing_columns: alter_statements.append( "ALTER TABLE user_dashboard_preferences ADD COLUMN email_address VARCHAR" ) if "notify_on_fail" not in existing_columns: alter_statements.append( "ALTER TABLE user_dashboard_preferences ADD COLUMN notify_on_fail BOOLEAN NOT NULL DEFAULT TRUE" ) if not alter_statements: return try: with bind_engine.begin() as connection: for statement in alter_statements: connection.execute(text(statement)) except Exception as migration_error: logger.warning( "[database][EXPLORE] Profile health preference additive migration failed: %s", migration_error, ) # [/DEF:_ensure_user_dashboard_preferences_health_columns:Function] # [DEF:_ensure_llm_validation_results_columns:Function] # @TIER: STANDARD # @PURPOSE: Applies additive schema upgrades for llm_validation_results table. def _ensure_llm_validation_results_columns(bind_engine): with belief_scope("_ensure_llm_validation_results_columns"): table_name = "llm_validation_results" inspector = inspect(bind_engine) if table_name not in inspector.get_table_names(): return existing_columns = { str(column.get("name") or "").strip() for column in inspector.get_columns(table_name) } alter_statements = [] if "task_id" not in existing_columns: alter_statements.append( "ALTER TABLE llm_validation_results ADD COLUMN task_id VARCHAR" ) if "environment_id" not in existing_columns: alter_statements.append( "ALTER TABLE llm_validation_results ADD COLUMN environment_id VARCHAR" ) if not alter_statements: return try: with bind_engine.begin() as connection: for statement in alter_statements: connection.execute(text(statement)) except Exception as migration_error: logger.warning( "[database][EXPLORE] ValidationRecord additive migration failed: %s", migration_error, ) # [/DEF:_ensure_llm_validation_results_columns:Function] # [DEF:_ensure_git_server_configs_columns:Function] # @TIER: STANDARD # @PURPOSE: Applies additive schema upgrades for git_server_configs table. # @PRE: bind_engine points to application database. # @POST: Missing columns are added without data loss. def _ensure_git_server_configs_columns(bind_engine): with belief_scope("_ensure_git_server_configs_columns"): table_name = "git_server_configs" inspector = inspect(bind_engine) if table_name not in inspector.get_table_names(): return existing_columns = { str(column.get("name") or "").strip() for column in inspector.get_columns(table_name) } alter_statements = [] if "default_branch" not in existing_columns: alter_statements.append( "ALTER TABLE git_server_configs ADD COLUMN default_branch VARCHAR NOT NULL DEFAULT 'main'" ) if not alter_statements: return try: with bind_engine.begin() as connection: for statement in alter_statements: connection.execute(text(statement)) except Exception as migration_error: logger.warning( "[database][EXPLORE] GitServerConfig preference additive migration failed: %s", migration_error, ) # [/DEF:_ensure_git_server_configs_columns:Function] # [DEF:ensure_connection_configs_table:Function] # @TIER: STANDARD # @PURPOSE: Ensures the external connection registry table exists in the main database. # @PRE: bind_engine points to the application database. # @POST: connection_configs table exists without dropping existing data. def ensure_connection_configs_table(bind_engine): with belief_scope("ensure_connection_configs_table"): try: ConnectionConfig.__table__.create(bind=bind_engine, checkfirst=True) except Exception as migration_error: logger.warning( "[database][EXPLORE] ConnectionConfig table ensure failed: %s", migration_error, ) raise # [/DEF:ensure_connection_configs_table:Function] # [DEF:init_db:Function] # @TIER: STANDARD # @PURPOSE: Initializes the database by creating all tables. # @PRE: engine, tasks_engine and auth_engine are initialized. # @POST: Database tables created in all databases. # @SIDE_EFFECT: Creates physical database files if they don't exist. def init_db(): with belief_scope("init_db"): Base.metadata.create_all(bind=engine) Base.metadata.create_all(bind=tasks_engine) Base.metadata.create_all(bind=auth_engine) _ensure_user_dashboard_preferences_columns(engine) _ensure_llm_validation_results_columns(engine) _ensure_user_dashboard_preferences_health_columns(engine) _ensure_git_server_configs_columns(engine) ensure_connection_configs_table(engine) # [/DEF:init_db:Function] # [DEF:get_db:Function] # @TIER: STANDARD # @PURPOSE: Dependency for getting a database session. # @PRE: SessionLocal is initialized. # @POST: Session is closed after use. # @RETURN: Generator[Session, None, None] def get_db(): with belief_scope("get_db"): db = SessionLocal() try: yield db finally: db.close() # [/DEF:get_db:Function] # [DEF:get_tasks_db:Function] # @TIER: STANDARD # @PURPOSE: Dependency for getting a tasks database session. # @PRE: TasksSessionLocal is initialized. # @POST: Session is closed after use. # @RETURN: Generator[Session, None, None] def get_tasks_db(): with belief_scope("get_tasks_db"): db = TasksSessionLocal() try: yield db finally: db.close() # [/DEF:get_tasks_db:Function] # [DEF:get_auth_db:Function] # @TIER: STANDARD # @PURPOSE: Dependency for getting an authentication database session. # @PRE: AuthSessionLocal is initialized. # @POST: Session is closed after use. # @DATA_CONTRACT: None -> Output[sqlalchemy.orm.Session] # @RETURN: Generator[Session, None, None] def get_auth_db(): with belief_scope("get_auth_db"): db = AuthSessionLocal() try: yield db finally: db.close() # [/DEF:get_auth_db:Function] # [/DEF:backend.src.core.database:Module]