AgentEvalTool/backend/agenteval/storage/db/core.py
sinohqb ca208232c7 perf(db): WAL 模式 + 性能索引 + N+1 查询消除
- SQLite 启用 WAL,允许读写并发
- 新增 6 个索引(eval_runs.status/campaign_id、eval_results.run_id、
  turns.run_id、intelligent_evals.status、task_queue.assigned_at)
- 幂等 Alembic 迁移(列/索引存在性检查)
- domain.py 计数改 func.count 聚合,get_attention_reason 单次加载 sessions
- scenario list_all 批量加载 bindings(1+N → 2 查询)
- mark_orphans_failed 批量加载 campaigns(N → 1 IN 查询)
2026-08-24 23:17:51 +08:00

92 lines
2.6 KiB
Python

"""Engine, session plumbing, and shared column helpers.
DATA_DIR is resolved relative to this file; the db package lives one level
deeper than the old single-file module, hence the extra ``parent``.
"""
import json
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
import sqlalchemy as sa
from sqlalchemy.pool import StaticPool
from sqlmodel import Session, SQLModel, create_engine
DATA_DIR = Path(__file__).resolve().parent.parent.parent.parent.parent / "data"
DATA_DIR.mkdir(parents=True, exist_ok=True)
DATABASE_URL = f"sqlite:///{DATA_DIR / 'agenteval.db'}"
FILES_DIR = DATA_DIR / "files"
FILES_DIR.mkdir(parents=True, exist_ok=True)
engine = create_engine(
DATABASE_URL,
echo=False,
connect_args={"check_same_thread": False},
poolclass=StaticPool,
)
# WAL mode allows concurrent reads during writes — critical for the evaluation
# engine's high-frequency turn/result writes while the frontend polls.
with engine.connect() as _conn:
_conn.execute(sa.text("PRAGMA journal_mode=WAL"))
_conn.commit()
def utc_now() -> datetime:
return datetime.now(timezone.utc)
def as_utc(dt: datetime) -> datetime:
"""Attach UTC tzinfo to a naive datetime.
SQLite round-trips drop tzinfo; stored times are always UTC, so a naive
value read back is restored as UTC before any comparison with utc_now().
"""
return dt if dt.tzinfo is not None else dt.replace(tzinfo=timezone.utc)
def iso_utc(dt: datetime | None) -> str | None:
"""Serialize a datetime to ISO 8601 with UTC timezone suffix.
Guarantees the output always ends with 'Z' or '+00:00' so JavaScript's
Date.parse() interprets it correctly as UTC (no 8-hour local-time offset).
"""
if dt is None:
return None
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
return dt.isoformat(timespec="seconds").replace("+00:00", "Z")
def new_uuid() -> str:
return str(uuid.uuid4())
def _json_dumps(value: Any) -> str:
"""Serialize a JSON column value. ensure_ascii=False keeps CJK readable
in the stored text — the single serialization口径 for all JSON columns."""
return json.dumps(value, ensure_ascii=False)
def _json_loads(raw: str) -> Any:
return json.loads(raw)
def init_db() -> None:
SQLModel.metadata.create_all(engine)
def get_session() -> Session:
return Session(engine)
def get_session_context():
"""Context manager that creates and properly closes a database session."""
session = Session(engine)
try:
yield session
finally:
session.close()