SQLVerify Documentation
SQLVerify formally proves that two SQL queries return the same result on
every database — or hands you a concrete database where they
disagree. It is a solver, not a linter: an equivalent verdict
is a proof, and a divergent verdict comes with the
counterexample that produced it.
Quickstart
Every surface talks to the same engine. Pick whichever fits where the query lives. You'll need an API key from the API Keys page first.
HTTP
CLI
AI agents (MCP)
Expose verification as a tool to Claude Code, Claude Desktop, or Cursor so an agent can check its own SQL rewrites in-loop and revise against a real counterexample instead of guessing.
Setup instructions →API reference
Authentication
Send your key as Authorization: Bearer sqv_… or
X-API-Key: sqv_…. Keys are shown once at creation and stored
only as a hash — if you lose one, revoke it and mint another.
Request body
| Field | Type | Default | Description |
|---|---|---|---|
| ddl_sql | string | required | Flyway-style CREATE TABLE DDL defining the schema both queries run against. ALTER TABLE statements are folded in statement order. |
| sql_v1 | string | required | The original / trusted SELECT query. |
| sql_v2 | string | required | The rewritten query to prove equivalent to sql_v1. |
| dialect | string | generic | Parser dialect hint. Both queries are read as the same dialect — cross-dialect comparison is out of scope. |
| bound | integer | 3 | Maximum rows per table Z3 explores. Higher catches rarer bugs and costs solve time. Values above 6 are rejected. |
| timeout_ms | integer | 60000 | Solver budget in milliseconds. Clamped to 120,000. On timeout the status is unknown — never a wrong verdict. |
| project_id | string | null | null | Optional project UUID to tag the run with. Silently dropped if the project isn't yours. |
Response body
| Field | Type | Description |
|---|---|---|
| status | string | equivalent, divergent, unknown, or error. See the table below. |
| divergence_reason | string | null | Short summary of how the two queries differ. Present when status is divergent. |
| counterexample_db | object | null | A concrete database — table name to rows — on which the two queries disagree. This is the proof, and it is replayable. |
| query_v1_output | array | null | Rows sql_v1 returns when run on counterexample_db. |
| query_v2_output | array | null | Rows sql_v2 returns when run on counterexample_db. |
| error_message | string | null | Why the input was rejected. Present when status is error. |
| explanation | string | null | Prose explanation of the divergence, written by the configured LLM. Populated automatically for divergent results. |
Status values
| equivalent | Proven: no database within the bound distinguishes the two queries. |
| divergent | The queries disagree. counterexample_db holds a database that proves it, and both outputs are replayed on it. |
| unknown | The solver ran out of time. Not a proof of equivalence — it means we don't know. |
| error | The input used SQL outside the supported subset, or the DDL didn't parse. SQLVerify fails closed rather than silently ignoring what it can't encode. |
Example: a divergent result
The quickstart request above returns this — the rewrite silently drops
users whose org_id is NULL:
Error responses
| 401 | Missing, malformed, or revoked API key. |
| 402 | Free-tier monthly run limit reached. See pricing. |
| 422 | Body failed validation — a missing field, or a bound above 6. |
| 429 | Per-IP rate limit exceeded. Back off and retry. |
Note that a divergent verdict is still an HTTP 200 —
the request succeeded, the queries just aren't equivalent. Only the codes
above indicate a failed call.
Limits
boundcaps at 6 rows per table.timeout_msdefaults to 60,000 and is clamped to 120,000.- Uploaded SQL files cap at 512 KB each.
- An
equivalentverdict is sound within the bound — it rules out every counterexample up to that many rows per table, which covers the overwhelming majority of real rewrite bugs.
Supported SQL
SQLVerify handles multi-table join chains mixing INNER,
LEFT, RIGHT, and FULL joins,
GROUP BY and HAVING with aggregates,
three-valued NULL logic, non-recursive CTEs, and uncorrelated
IN (SELECT …) subqueries. NULLs, primary keys, foreign keys,
and NOT NULL constraints are all modelled.
Anything outside that subset — window functions, UNION,
SELECT DISTINCT, LIMIT, correlated subqueries —
returns error rather than a guess. That is deliberate: a
verifier that quietly skips what it can't encode would report
equivalent for queries it never actually checked, which is the
one failure mode a proof tool must not have.