The gap between a query that parses and a query that is right is your schema. Here is how that gap gets closed.
Translating "how many customers churned last month" into SQL is trivial once you know that churn is `subscriptions.cancelled_at`, that a customer is a row in `accounts` rather than `users`, and that the two join through `account_id`. Knowing that is schema linking, and it is where systems fail. The SQL grammar is small and every current model has it; your schema is large, private, and nothing was trained on it.
It works for the sample database in the tutorial. At two thousand tables the schema does not fit in a context window, and even where it fits, burying eight relevant tables in two thousand irrelevant ones measurably degrades the answer. SQLore builds a retrieval index over the schema locally — names, types, keys, distinct values, statistics — and searches it per question, so the model sees the eight tables the question is about and not the rest.
A one-shot system hands you a query and you find out at the meeting that the join fanned out. An agentic system runs the query itself and reads what came back. Zero rows from a question that should return thousands is a signal. A sum that is exactly four times too large is a signal. SQLore acts on those signals before returning an answer, which is why the query you are shown is the one that survived rather than the first one drafted.
Letting a model run generated SQL against your database is only reasonable if the generated SQL provably cannot do damage. SQLore parses every statement to an AST and validates it down to a single read-only SELECT — no writes, no DDL, no multiple statements, no stacked queries — then runs it on a read-only connection with a statement timeout. The guarantee is structural, so the agentic loop costs you nothing.
select c.name as category,
sum(oi.quantity * oi.unit_price_cents) / 100.0 as revenue
from order_items oi
join orders o on o.id = oi.order_id
join products p on p.id = oi.product_id
join categories c on c.id = p.category_id
where o.placed_on >= date_trunc('quarter', current_date) - interval '3 months'
and o.placed_on < date_trunc('quarter', current_date)
and o.status <> 'refunded'
group by c.name
order by revenue desc;Not one word of that question maps to a column. There is no revenue column — revenue is quantity multiplied by unit_price_cents, held in cents, which is why the query divides by a hundred. There is no category on an order either; the path runs from order_items through products to categories, and it was read out of the foreign keys rather than inferred from the names. And "refunded" is spelled the way it is actually stored in orders.status, because introspection cached the distinct values of that column. A model given this question and no schema misses all four, and still returns a query that runs.
On first connection it reads the system catalog: tables, columns, types, primary and foreign keys, and statistics. The result is cached locally so this happens once, not per question.
Your question is matched against that index on your own machine. A two-thousand-table database yields the eight tables the question is about.
The model writes SQL against the retrieved slice — real table names, real keys, real column values — on a local model by default or a live one if you asked for it.
The statement is parsed to an AST, validated to a single read-only SELECT, and executed on a read-only connection under a statement timeout.
If the result contradicts the question, it revises and runs again. What you are shown is the answer in plain English with the result charted or tabulated, and under it the query that worked, the rows it returned and how long it took.
More answers on the main FAQ, or ask us directly.
For people who already write SQL
The category, explained
For analysis
Every page in this section covers a different question about the same product. See all solutions.
Start free, upgrade when you are asking enough questions for it to matter.