Concepts¶
An eval is four building blocks: a case, a solver, one or more scorers, and a
platform. assert_eval runs the solver, executes its SQL on the
platform, and asserts the scorers pass.
Cases¶
A case pairs a question with its expected answer. You declare one with the
@eval_case decorator, which injects a prepared
EvalCase as the pytest case fixture — no conftest.py required, since
installing evaldata registers its pytest plugin.
The expected value determines how the result is judged. There are four shapes:
expected shape |
Meaning |
|---|---|
{"rows": [...]} |
Untyped result set — compare values only. |
{"rows": [...], "schema": [{"name", "type"}]} |
Typed result set — values and column types. Fails if the right value comes back with the wrong type. |
{"kind": "gold_query", "sql": "..."} |
Gold query — run a reference query and compare its executed result, not its SQL text (execution accuracy). |
{"kind": "expectation_suite", "expectations": [...]} |
Expectation suite — structural checks (row_count, not_null, unique) instead of exact rows. |
Solvers¶
A solver is the system under test: it turns a case's question into SQL.
CallableSolverwraps a function returning SQL. Use it for fixed SQL (deterministic evals) or to plug in any logic you already have.PromptSolvercalls a model through litellm and expects a structured reply containing the SQL. Because litellm normalises providers, the same solver drives a local Ollama model and a hosted API model — only the model id changes. Requires thelitellmextra.
Swapping the AI under test is a one-line change:
solver = CallableSolver(lambda c: "SELECT ...") # fixed SQL
solver = PromptSolver(model="ollama_chat/gemma4") # local Ollama model
solver = PromptSolver(model="openai/gpt-4o-mini") # hosted model
Scorers¶
A scorer judges the solver's result against the case's expected.
ResultSetEquivalencecompares result rows — for the untyped, typed, and gold-query shapes. See Equivalence for how the row diff works.SemanticEquivalencecompares the AI's query against a gold query without running either: it confirms a match or returnsunknownwhen it can't — it never refutes.observed_equivalence()composes it withResultSetEquivalence, so a confirmed match skips running the queries while everything else is decided by running both and comparing the results. See Check semantic equivalence.ExpectationSuiteScorerevaluates an expectation suite's structural checks.LlmJudgeasks a grader model to score the AI's SQL against criteria you write, for cases where comparing rows or matching syntax isn't enough. It returns a pass/fail verdict with the grader's score and rationale. See Score with an LLM judge.
Pass a list to assert_eval, so a single case can be scored several ways.
Verdicts and basis¶
Every scorer returns a ScoreResult whose verdict is one of three
values: pass, fail, or inconclusive — the last meaning the scorer couldn't decide either way
(a grader call that errored, a query that couldn't be parsed). Inconclusive doesn't mean the
answer is wrong.
A decided result also carries a basis — how strong the evidence behind it is:
proven— sound on every dataset, by reasoning about the queries (SemanticEquivalence).observed— seen to hold on the data that was run (ResultSetEquivalence).judged— a probabilistic judgment from a grader model (LlmJudge).
Platforms¶
A platform is the database the SQL runs on. A platform reference is a
lightweight value (e.g. from duckdb_platform(...) or databricks_platform(...)); resolve
turns it into a live adapter when the eval runs.
Supported adapters today: DuckDB, Postgres, Databricks, and Snowflake (a BigQuery adapter is planned). On a real warehouse the adapter does more than ship SQL — for example, the Databricks adapter resolves precise column types and pushes expectation and equivalence checks down into SQL so rows aren't pulled back to compare. Cloud platforms authenticate through their own driver or SDK, which reads your credentials from the environment — they aren't passed through the platform reference.