Files
mo a11621b21f Add Command Center v2: DQ/RAG integration, S3 browser, Jupyter, GPU matrix.
Mirror mo/atc-GPU layout with config/, docs/, scripts/ for Gitea deploy.
2026-06-25 00:28:23 +00:00

165 lines
6.8 KiB
Python

"""Database engine, session factory, and startup migrations."""
from __future__ import annotations
import os
import sqlite3
from typing import Any
from sqlalchemy import create_engine, inspect, text
from sqlalchemy.orm import sessionmaker
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:////data/atc-agents.db")
SQLITE_FALLBACK_PATH = os.getenv("SQLITE_FALLBACK_PATH", "/data/atc-agents.db")
def make_engine(url: str = DATABASE_URL):
kwargs: dict[str, Any] = {"pool_pre_ping": True}
if url.startswith("sqlite"):
kwargs["connect_args"] = {"check_same_thread": False}
return create_engine(url, **kwargs)
engine = make_engine()
SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False)
def migrate_approval_columns(db_engine: Any = engine) -> None:
"""Add approval columns on legacy SQLite/Postgres schemas."""
insp = inspect(db_engine)
if "approvals" not in insp.get_table_names():
return
existing = {c["name"] for c in insp.get_columns("approvals")}
dialect = db_engine.dialect.name
if dialect == "postgresql":
alters = [
("action_type", "VARCHAR(64)", "generic.mutate"),
("target", "TEXT", ""),
("payload", "TEXT", "{}"),
("decided_by", "VARCHAR(64)", None),
("decide_note", "TEXT", None),
("decided_at", "TIMESTAMP WITH TIME ZONE", None),
("priority", "VARCHAR(16)", "normal"),
]
with db_engine.begin() as conn:
for col, typ, default in alters:
if col in existing:
continue
if default is None:
conn.execute(text(f"ALTER TABLE approvals ADD COLUMN IF NOT EXISTS {col} {typ}"))
elif default == "":
conn.execute(text(f"ALTER TABLE approvals ADD COLUMN IF NOT EXISTS {col} {typ} DEFAULT ''"))
else:
conn.execute(
text(f"ALTER TABLE approvals ADD COLUMN IF NOT EXISTS {col} {typ} DEFAULT '{default}'")
)
return
alters = [
("action_type", "VARCHAR(64)", "'generic.mutate'"),
("target", "TEXT", "''"),
("payload", "TEXT", "'{}'"),
("decided_by", "VARCHAR(64)", "NULL"),
("decide_note", "TEXT", "NULL"),
("decided_at", "DATETIME", "NULL"),
("priority", "VARCHAR(16)", "'normal'"),
]
with db_engine.begin() as conn:
for col, typ, default in alters:
if col not in existing:
conn.execute(text(f"ALTER TABLE approvals ADD COLUMN {col} {typ} DEFAULT {default}"))
def migrate_sqlite_to_postgres(db_engine: Any = engine) -> dict[str, int]:
"""One-time copy from legacy SQLite volume into Postgres when Postgres is empty."""
if not DATABASE_URL.startswith("postgresql"):
return {}
if not os.path.isfile(SQLITE_FALLBACK_PATH):
return {}
insp = inspect(db_engine)
tables = set(insp.get_table_names())
if "approvals" not in tables or "feed" not in tables:
return {}
with db_engine.connect() as conn:
approval_count = conn.execute(text("SELECT COUNT(*) FROM approvals")).scalar() or 0
feed_count = conn.execute(text("SELECT COUNT(*) FROM feed")).scalar() or 0
if approval_count or feed_count:
return {"approvals": 0, "feed": 0, "skipped": 1}
copied = {"approvals": 0, "feed": 0}
src = sqlite3.connect(SQLITE_FALLBACK_PATH)
src.row_factory = sqlite3.Row
try:
with db_engine.begin() as conn:
for row in src.execute("SELECT * FROM feed"):
conn.execute(
text(
"INSERT INTO feed (id, ts, agent_id, level, message) "
"VALUES (:id, :ts, :agent_id, :level, :message) ON CONFLICT (id) DO NOTHING"
),
dict(row),
)
copied["feed"] += 1
for row in src.execute("SELECT * FROM approvals"):
conn.execute(
text(
"INSERT INTO approvals (id, ts, agent_id, action, reason, status, "
"action_type, target, payload, decided_by, decide_note, decided_at, priority) "
"VALUES (:id, :ts, :agent_id, :action, :reason, :status, "
":action_type, :target, :payload, :decided_by, :decide_note, :decided_at, :priority) "
"ON CONFLICT (id) DO NOTHING"
),
{
"id": row["id"],
"ts": row["ts"],
"agent_id": row["agent_id"],
"action": row["action"],
"reason": row["reason"],
"status": row["status"],
"action_type": row["action_type"] if "action_type" in row.keys() else "generic.mutate",
"target": row["target"] if "target" in row.keys() else "",
"payload": row["payload"] if "payload" in row.keys() else "{}",
"decided_by": row["decided_by"] if "decided_by" in row.keys() else None,
"decide_note": row["decide_note"] if "decide_note" in row.keys() else None,
"decided_at": row["decided_at"] if "decided_at" in row.keys() else None,
"priority": row["priority"] if "priority" in row.keys() else "normal",
},
)
copied["approvals"] += 1
finally:
src.close()
return copied
def init_database(Base: type) -> dict[str, Any]:
"""Create tables, run migrations, optionally import legacy SQLite data."""
Base.metadata.create_all(engine)
migrate_approval_columns(engine)
migrated = migrate_sqlite_to_postgres(engine)
return {"engine": engine.dialect.name, "migrated_from_sqlite": migrated}
def db_health() -> dict[str, Any]:
try:
with engine.connect() as conn:
conn.execute(text("SELECT 1"))
insp = inspect(engine)
tables = insp.get_table_names()
stats = {}
if "approvals" in tables:
with engine.connect() as conn:
stats["approvals"] = conn.execute(text("SELECT COUNT(*) FROM approvals")).scalar()
stats["approvals_pending"] = conn.execute(
text("SELECT COUNT(*) FROM approvals WHERE status = 'pending'")
).scalar()
if "feed" in tables:
with engine.connect() as conn:
stats["feed"] = conn.execute(text("SELECT COUNT(*) FROM feed")).scalar()
return {"ok": True, "dialect": engine.dialect.name, "tables": tables, **stats}
except Exception as exc:
return {"ok": False, "dialect": engine.dialect.name, "error": str(exc)}