nvikou's picture
Upgrade CRAG: embeddings, SQL SQLite, pro UI, 60-eval, local data
98aa813 verified
Raw History Blame Contribute Delete
1.98 kB
"""Exécuteur SQL lecture seule sur SQLite (company.db)."""
from __future__ import annotations
import logging
import sqlite3
import time
from pathlib import Path
from app.core.config import get_settings
from app.sql.seed import ensure_company_db
from app.sql.sql_validator import MAX_ROWS, is_safe_sql
logger = logging.getLogger(__name__)
def get_db_path() -> Path:
settings = get_settings()
return Path(settings.sqlite_db_path)
def _connect() -> sqlite3.Connection:
db_path = ensure_company_db(get_db_path())
conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True, timeout=5.0)
conn.row_factory = sqlite3.Row
return conn
def execute_readonly(query: str, params: tuple | list | None = None) -> dict:
"""Exécute un SELECT validé, avec limite de lignes et journal d'audit."""
params = tuple(params or ())
started = time.perf_counter()
if not is_safe_sql(query):
return {
"error": "Requête SQL refusée par le validateur.",
"query": query,
"duration_ms": 0,
}
limited = query.strip().rstrip(";")
if "LIMIT" not in limited.upper():
limited = f"{limited} LIMIT {MAX_ROWS}"
try:
with _connect() as conn:
cur = conn.execute(limited, params)
rows = [dict(r) for r in cur.fetchall()]
except sqlite3.Error as exc:
logger.warning("Échec SQL: %s", exc)
return {
"error": str(exc),
"query": limited,
"params": list(params),
"duration_ms": int((time.perf_counter() - started) * 1000),
}
duration_ms = int((time.perf_counter() - started) * 1000)
logger.info(
"SQL audit query=%r params=%s rows=%d duration_ms=%d",
limited, params, len(rows), duration_ms,
)
return {
"query": limited,
"params": list(params),
"rows": rows,
"row_count": len(rows),
"duration_ms": duration_ms,
}