Files
Mutual_Fund/nl2sql/executor.py
T
2026-09-14 10:57:48 +08:00

99 lines
3.5 KiB
Python

"""执行已通过安全校验的只读 SQL。"""
from __future__ import annotations
import asyncio
import time
from sqlalchemy import text
from nl2sql.contracts import DataQueryResult
from nl2sql.masking import mask_rows
from nl2sql.rendering import render_markdown
from nl2sql.runtime import QueryRuntimeRegistry, query_runtime_registry
from nl2sql.runtime import kill_mysql_query
from nl2sql.sql_security import ValidatedSql
class QueryExecutionError(RuntimeError):
"""只读 SQL 执行失败。"""
async def execute_readonly_sql(
session,
validated_sql: ValidatedSql,
*,
query_id: str,
trace_id: str,
masks: dict[tuple[str, str], str] | None = None,
max_rows: int | None = None,
timeout_seconds: float | None = None,
parameters: dict[str, object] | None = None,
user_id: int = 0,
connection_id: int | None = None,
kill_query=None,
runtime_registry: QueryRuntimeRegistry | None = query_runtime_registry,
) -> DataQueryResult:
"""执行安全 SQL 并转换为统一查询结果,不接收未校验的原始 SQL。"""
started_at = time.perf_counter()
if runtime_registry is not None:
if connection_id is None and hasattr(session, "connection"):
try:
connection = await session.connection()
connection_result = await connection.execute(text("SELECT CONNECTION_ID()"))
connection_id = int(connection_result.scalar_one())
except Exception: # noqa: BLE001 连接号仅用于运维,不影响正常查询
connection_id = None
await runtime_registry.register(
query_id=query_id,
user_id=user_id,
sql=validated_sql.sql,
connection_id=connection_id,
)
try:
statement = text(validated_sql.sql)
execution = (
session.execute(statement, parameters)
if parameters
else session.execute(statement)
)
result = (
await asyncio.wait_for(execution, timeout_seconds)
if timeout_seconds is not None
else await execution
)
columns = [str(key) for key in result.keys()]
rows = [dict(row) for row in result.mappings().all()]
if masks:
rows = mask_rows(rows, masks)
truncated = max_rows is not None and len(rows) > max_rows
if truncated:
rows = rows[:max_rows]
except asyncio.TimeoutError as exc:
killer = kill_query or kill_mysql_query
if connection_id is not None:
try:
await killer(connection_id)
except Exception: # noqa: BLE001 中止失败仍需返回统一超时错误
pass
if runtime_registry is not None:
await runtime_registry.complete(query_id, status="timeout")
raise QueryExecutionError("查询超时") from exc
except Exception as exc: # noqa: BLE001 统一收敛数据库异常
if runtime_registry is not None:
await runtime_registry.complete(query_id, status="failed")
raise QueryExecutionError("查询执行失败") from exc
elapsed_ms = (time.perf_counter() - started_at) * 1000
if runtime_registry is not None:
await runtime_registry.complete(query_id)
return DataQueryResult(
query_id=query_id,
trace_id=trace_id,
columns=columns,
rows=rows,
row_count=len(rows),
truncated=bool(truncated),
markdown=render_markdown(columns, rows),
sql=validated_sql.sql,
elapsed_ms=elapsed_ms,
)