| """Tiny execution-accuracy eval: gold NL/SQL pairs scored by result-set match |
| (order-insensitive), not string similarity.""" |
| from __future__ import annotations |
|
|
| from dataclasses import dataclass |
| from typing import Callable, Optional |
|
|
| from .db import QueryResult, execute |
| from .guardrails import validate |
|
|
| |
| GOLD = [ |
| {"q": "How many customers are there?", |
| "sql": "SELECT count(*) AS n FROM customers"}, |
| {"q": "List all distinct product categories.", |
| "sql": "SELECT DISTINCT category FROM products ORDER BY category"}, |
| {"q": "How many orders have the status completed?", |
| "sql": "SELECT count(*) AS n FROM orders WHERE status = 'completed'"}, |
| {"q": "Which product has the highest price? Return its name.", |
| "sql": "SELECT name FROM products ORDER BY price DESC LIMIT 1"}, |
| {"q": "List the names of customers from the USA.", |
| "sql": "SELECT name FROM customers WHERE country = 'USA' ORDER BY name"}, |
| {"q": "How many products are in the Electronics category?", |
| "sql": "SELECT count(*) AS n FROM products WHERE category = 'Electronics'"}, |
| {"q": "What is the total revenue (quantity times unit_price) across all order items?", |
| "sql": "SELECT sum(quantity * unit_price) AS revenue FROM order_items"}, |
| {"q": "How many orders did each customer place? Return customer name and the count.", |
| "sql": "SELECT c.name, count(*) AS orders FROM customers c " |
| "JOIN orders o ON o.customer_id = c.customer_id GROUP BY c.name ORDER BY c.name"}, |
| ] |
|
|
|
|
| def _normalize(qr: QueryResult): |
| return sorted(tuple(str(x) for x in row) for row in qr.rows) |
|
|
|
|
| def execution_match(con, pred_sql: str, gold_sql: str, max_rows: int = 1000) -> bool: |
| """True if both queries return the same result set (order-insensitive).""" |
| try: |
| pred = execute(con, pred_sql, max_rows) |
| gold = execute(con, gold_sql, max_rows) |
| except Exception: |
| return False |
| return _normalize(pred) == _normalize(gold) |
|
|
|
|
| @dataclass |
| class EvalItem: |
| question: str |
| gold_sql: str |
| pred_sql: str |
| ok: bool |
| blocked: bool |
| reason: str |
|
|
|
|
| def run_eval( |
| con, |
| generate_fn: Callable[[str], str], |
| allowed: set[str], |
| max_rows: int = 1000, |
| ) -> dict: |
| """Run the gold set through generate -> guardrail -> execute -> match. |
| |
| ``generate_fn(question) -> sql`` (the LLM, or a stub for offline runs).""" |
| items: list[EvalItem] = [] |
| for g in GOLD: |
| pred = generate_fn(g["q"]) |
| gr = validate(pred, allowed, max_rows) |
| if not gr.ok: |
| items.append(EvalItem(g["q"], g["sql"], pred, False, True, gr.reason)) |
| continue |
| ok = execution_match(con, gr.safe_sql, g["sql"], max_rows) |
| items.append(EvalItem(g["q"], g["sql"], gr.safe_sql, ok, False, "ok" if ok else "result mismatch")) |
| n = len(items) |
| accuracy = sum(1 for i in items if i.ok) / n if n else 0.0 |
| blocked = sum(1 for i in items if i.blocked) |
| return {"accuracy": accuracy, "n": n, "blocked": blocked, "items": items} |
|
|