NL2SQL 0.7.0
Wave A of the redesign: every run publishes typed events that observers log, audit or export, hooks can change or stop a run and ask for a person's approval, evals become engine.evals and nl2sql eval with a regression gate, routing votes across five signals, and answers are checked for join fan-out and empty columns. It also adds per-question cost caps, schema drift checks and an MCP server. Several APIs and config keys change: read Upgrading before you install it.
Released 2026-10-15.
Highlights
- Events, observers and hooks. Every run publishes typed events on one bus: run, node, answer, LLM call, policy denial, index and drift events, each in a catalog.
stream_queryyields those events from a caller-safe allow-list, ready for SSE. Theobserverssection sends them to a log, a JSON-lines audit log understorage.path, or OpenTelemetry. Six hook points let your code modify a run before the validator and deny it after, and a hook can stop a run asneeds_approvaluntil an approver re-runs it. - Evals as a product.
engine.benchmarkis nowengine.evals, andnl2sql benchmarkis nownl2sql eval. One case format, five scorers and three modes (full,plans,retrieval), with the Chinook gold set moved unchanged and pinned by its digest. A regression gate on execution accuracy runs in CI, a new routing suite of 78 cases covers the demo's three databases, andnl2sql eval seeddrafts cases from your own database. - Better routing and safer answers. Each example question is its own index entry. Routing fuses five signals (description, examples, verified queries, tables, columns) by rank and drops a datasource far less similar than the best. A question that needs too many sub-queries is refused as
QUESTION_TOO_BROAD. Join keys of different types are converted before a cross-database join. A count inflated by a join is caught and retried (JOIN_FAN_OUT), and a column that came back all null is flagged. - Cost caps, drift and MCP.
settings.pipeline.max_llm_callsandmax_cost_usdstop a question before the call that would cross them, with statusstopped.nl2sql index --checkreports schema drift and what it breaks, without re-indexing.nl2sql mcpserves the engine to MCP clients over stdio, as one role.
Real-database results
- The CI
realdbjob passed on the PRs that touched database code. - The run on the GCP test bed against
mainat5920d3f1: 111 passed, 9 skipped, 0 failed, in 9.5 minutes.
What's included
Features
- The foundation: the event bus and catalog, the run scope, the
observer,hookandscorerregistry kinds, the new config sections and settings, theRunStatusvalues andnl2sql --log-format text|json(#313). - Run events on every run,
stream_queryyieldingEvents from a caller-safe allow-list,engine.events.subscribe()andcatalog(), and the run'strace_idon every log line (#314). - Hooks: six points (
run.before,datasources.after,plan.after,execute.before,execute.after,answer.after), run in config order, failing closed unless a hook setson_error: open(#315). - Routing: one index entry per example question, five signals fused by rank, a similarity floor, and the
settings.routingandsettings.retrievallimits (#316). engine.evals: the suite format, the Chinook suite, five scorers, modesfull,plansandretrieval, andregeneratethrough the datasource's adapter (#317).- The
log,auditandotelobservers, withllm.callandllm.changedon the bus. The audit log moves understorage.path(#318). - Approval: a hook can ask for one, the result is
needs_approvalwith a request, and an approver's re-run pins the same plan.run_query(plan=SuppliedPlan(...))runs a plan you already hold (#319). nl2sql eval run,regenerateandpublish, andengine.evals.gate(), a regression gate on execution accuracy, with records kept by mode and suite (#320).- The Phase 2 events are published: index builds, failed datasource checks, parse retries, policy denials and purges (#321).
- A limit on sub-queries per question (
QUESTION_TOO_BROAD), join keys converted to one type before a cross-database join, and a validator check for join keys of incompatible types (#322). - The routing suite of 78 cases over the demo's three databases, and its gate on top-1 and shortlist recall in CI (#323).
- Answer checks for join fan-out and all-null columns, closest values from the index's sample values, and typed
RunWarnings (#324). nl2sql mcp, an MCP server over stdio withask,list_datasourcesanddescribe_table, all limited to the caller's role (the newmcpextra) (#325).- The dated price table in
llm/prices.py, every LLM call priced,engine.llm.prices(), the per-question caps and aBudgetyou can use outside a question (#326). - Schema drift:
engine.index.check(),versions()anddiff(),nl2sql index --check, and the verified queries and policy rules a change affects (#327). engine.evals.seed()andnl2sql eval seed: draft eval cases from your own datasource, as typed plans the engine validates and runs read-only, under a cost cap (#328).- SQL Server connections take
encrypt: falsefor a local server without TLS. Encryption stays on by default (#307).
Docs
- The Wave A design (#308), and the designs for Waves B, C and D (#310, #311, #312). New pages: Events, Observers, Hooks, Evals, Seeding, Schema drift and MCP.
- Two rules in
CLAUDE.mdcorrected to match the code (#306).
Known limits
- Routing is not perfect. On the routing suite, top-1 is 76 of 78 and the shortlist 77 of 78. "Which customers complained most about refunds?" and "How many purchases were there per month in 2013?" go to Chinook first.
- Approvals are not stored. The caller keeps the request and sends the approval back with the re-run. Stored approvals come with the REST API in Wave D.
needs_clarificationis a reserved status. Nothing returns it until Wave B.- MCP runs over stdio only, as one role. MCP over HTTP with API keys comes in Wave C.
- Schema drift is measured, not acted on. Nothing re-indexes or fixes a verified query by itself.
- When a hook is attached at
execute.before, the value and fan-out checks run no probe query. Only the all-null check runs. - While chromadb is a core dependency, it brings OpenTelemetry with it, so the
otelextra installs nothing new yet. - Seeded drafts have no rows. Run
nl2sql eval regenerateafter you approve them.
Upgrading
Add [otel] for the OpenTelemetry observer and [mcp] for nl2sql mcp. Third-party store authors: pip install "nl2sql-sdk==0.7.0".
Breaking changes: several. Each needs a look before you upgrade.
engine.benchmarkis nowengine.evals, andnl2sql benchmarkis nownl2sql eval(#317, #320). There is no alias. The gold files are nowsuites/chinook.yamlwith renamed keys (expectedisoutcome,gold_sqlissql,alt_gold_sqlisalt_sql), and the gate reads execution accuracy. What to do: callengine.evals.run()andgate(), ornl2sql eval run. Move your own gold files to the case format. A 0.6 record starts a new series in the gate.QueryEventis gone;stream_queryyieldsEvents (#314). The stream carriesrun.started,node.*,answer.delta,result.manifest,sanity.flagged,run.limit_reached, then onerun.finished(with theQueryResultindata.result) orrun.failed. Policy, hook and LLM call events never reach it. Each SSEdataline ofPOST /api/v1/query/streamis now the whole event:name,at,trace_id,tenant_idanddata. What to do: read events byname, and take the result fromrun.finished. See Events.QueryResult.warningsare typedRunWarnings (#324). Each is{code, node, message, sub_query_id, detail}. Over REST,error_codeis nowcode, andseverityis gone: an earlier attempt that a retry replaced hasdetail.recovered: true. What to do: readcodeinstead oferror_code, anddetail.recoveredinstead ofseverity.QueryResult.statusis aRunStatus(#313, #319, #326). Besidesuccess,plan_onlyanderror, a run can now endneeds_approval(withapprovals_needed) orstopped(withstop, at a cost or call cap). What to do: handle the two new values.engine.datasources.schema()requiresuser_context(#325), and drops the tables the caller may not read.engine.index.check(),versions()anddiff()take it too (#327). What to do: pass the caller'sUserContext.settings.observabilityis gone; use theobserverssection (#313, #318). A config that still sets it fails to load. By default the engine runs thelogandauditobservers, and the audit log is now<storage.path>/audit/<date>.jsonl, notlogs/audit_events.log. What to do: moveexporterandotlp_endpointto anotelobserver. See Observers.- OpenTelemetry is the
otelextra (#318). It has left the core dependencies, and anotelobserver without it is aConfigError. What to do:pip install "nl2sql-engine[otel]"if you export to a collector. TABLE_K,PLANNING_KandMAX_DECOMPOSER_TABLESare settings (#313, #316):settings.retrieval.table_k,planning_kanddecomposer_tables, with the same defaults. What to do: if you patched the constants, set the settings instead.- Routing changed (#316).
settings.routing.candidatesnow defaults to 3, a datasource far less similar than the best is dropped (floor_ratio, 0.5), andRetriever.route()replacesretrieve_datasource_candidates. What to do: runnl2sql indexafter upgrading, so each example question gets its own entry. Until you do, routing has no example votes. - A question needs at most
settings.routing.max_sub_queriessub-queries (#322), 6 by default. More ends the run withQUESTION_TOO_BROAD. What to do: raise the setting if your users ask wider questions. - Store interface version 2 (#327).
StateStoregainsput_schema_diffandget_schema_diff, and a version 1 store is refused when it loads. The built-insqlitestore migrates itself. What to do: third-party state stores add the two methods and setinterface_version = 2. - The price table moved to
nl2sql/llm/prices.py(#326).nl2sql.evaluation.pricesis gone, with no alias. A model the table does not know now costsNone, not 0, and a token count the provider did not report isNone. What to do: import fromnl2sql.llm.prices, price your own models withllm.prices, and handleNonein usage and cost. Withmax_cost_usdset, an unpriced model is aConfigError. - The
mcpextra (#325).nl2sql mcpneedspip install "nl2sql-engine[mcp]".
New, not breaking: run_query(plan=SuppliedPlan(...)) runs a plan you hold, through plan.after and the validator, with no planning (#319).
What's next
Next: 0.8, Wave B: business knowledge, the schema profiler, clarifying questions and a cost guard.
Full changelog: v0.6.0...v0.7.0