← All notes

The model writes the plan, not the SQL

Pradip2 min read

There is a line in Insighter that surprises people when they first see the architecture: the language model does not write the SQL that touches your warehouse.

It reads the question. It resolves entities. It produces a plan — which metric, which grain, which filters, which time window. Then a deterministic generator turns that plan into SQL for the target dialect, using only definitions a human approved.

Why not let the model write it

Because SQL is not the interesting part of the problem, and letting a model freehand it imports three problems we would then have to solve anyway.

Correctness gets unbounded. Freehand SQL can join on anything, filter on anything, and invent an aggregation. Every one of those is a place to be subtly wrong, and reviewing generated SQL for subtle wrongness at query volume is not a job anyone will do for long.

Dialects multiply the surface. One warehouse is Oracle, the next is PostgreSQL. Date arithmetic, string handling, pagination and null ordering all differ. A model can get these right most of the time. “Most of the time” across two dialects and a hundred queries a day is a steady drip of incidents.

The output stops being reviewable. A plan is a small structured object — “revenue, by month, for EMEA, last four quarters”. A person can read that in three seconds and say yes or no. A forty-line query with three CTEs is technically more informative and practically unreviewable.

What the split looks like

The model’s job ends at a structure like this:

{
  "metric": "notional_exposure",
  "grain": ["counterparty", "month"],
  "filters": [{ "term": "region", "op": "=", "value": "EMEA" }],
  "window": { "kind": "trailing", "periods": 4, "unit": "quarter" }
}

Every name in it must already exist in the approved catalogue. If the model proposes a metric nobody approved, the plan fails validation and the user is told the question cannot be answered yet — which is a far better outcome than a number derived from an invented definition.

From there the generator is ordinary software: look up the metric’s approved expression, resolve the join path, emit dialect-correct SQL. It is testable in the way a compiler is testable, because the same plan always produces the same query.

The guard behind the guard

Belt and braces: whatever comes out of the generator is parsed before execution and checked to be read-only. Not string-matched for DROP — parsed, and any statement that is not a read is refused.

That check should be unreachable. The generator only emits selects. But the distance between “should be unreachable” and “is unreachable” is exactly where production incidents live, and the check costs a millisecond.

Where the model earns its keep

None of this is scepticism about language models. The genuinely hard part of the problem — mapping the sentence a human typed onto the concepts a business actually tracks, handling ellipsis, resolving “the same thing but for last year” — is squarely a language problem, and models are extraordinary at it.

So we spend the model where judgement is needed and use deterministic code where correctness is needed. That is not a compromise between the two approaches. It is just putting each one where it is good.