"""Esecuzione controllata e EXPLAIN via REST (rpc `run_query`/`explain_query`). Produce gli stessi tipi del transport diretto (`ExecResult`, `PlanSummary`) e traduce `RestError` in `ExecutionError`, così le fasi 6/7 e la gestione errori della CLI non cambiano. Differenze rispetto al diretto: read-only e statement_timeout sono garantiti lato server; il client mantiene solo l'iniezione del LIMIT (per il rilevamento del troncamento in preview). """ import time from tht.execute import ExecResult, ExecutionError, PlanSummary, _inject_limit from tht.rest.client import RestError from tht.rest.explain import parse_text_plan def run_controlled_rest(client, sql: str, *, limit: int) -> ExecResult: final_sql, injected = _inject_limit(sql, limit) start = time.monotonic() try: rows_dicts = client.run_query(final_sql) except RestError as e: raise ExecutionError(str(e)) from e elapsed_ms = int((time.monotonic() - start) * 1000) columns = list(rows_dicts[0].keys()) if rows_dicts else [] rows = [tuple(r.get(c) for c in columns) for r in rows_dicts] truncated = injected and len(rows) > limit return ExecResult( columns=columns, rows=rows[:limit], execution_ms=elapsed_ms, truncated=truncated ) def explain_rest(client, sql: str) -> PlanSummary: try: lines = client.explain_query(sql) except RestError as e: raise ExecutionError(str(e)) from e return parse_text_plan(lines)