Asif EjazAI engineering / interactive demo← All demos
One statement per question. Checked first, then run read only.

Ask the job cost ledger in plain English, and see the SQL it ran

This began as a 2023 freelance job: a language model agent that answered questions about a construction company's accounts by writing and running its own SQL. This version keeps the idea and removes the trust. The model writes one statement, the server checks it before anything runs, and every answer arrives with the query that produced it.

10
tables of synthetic job costing data
17,933
rows, generated from a fixed seed
1 s
time limit on every statement
115
hostile statements in the test file, all refused or cut off
Try it

Pick a recorded question, or type your own

The recorded questions run stored SQL through the same checks as SQL written by the model, so the rows you see are computed live either way. A typed question is sent to a hosted language model to be turned into SQL. This server keeps no log of what you type.

Recorded questions
Running a first question.
The data

10 tables of invented job costing data

One contractor in western Canada and 60 projects, with the books kept from 2023 to the end of 2025: budgets by trade, purchase orders, supplier invoices, crew timesheets and change orders. Every company and person in it is made up, and the patterns in it (which trades run over, which managers finish late) are ones I planted in the generator. Do not read industry facts into them.

Loading the tables.

What changed

The same idea, without the open door

The 2023 version worked in a notebook, and I would not have put it on a public page. These are the three things I rebuilt.

Agent to one statement

The model never touches the database

The original gave a LangChain agent a normal database connection and ran whatever GPT 4 chose to send. The only check was the model reviewing its own SQL, and nothing stood in the way of a write. Here the model returns text and nothing more. The server decides whether that text is allowed to run.

4 steps to 1 call

One call, not a conversation

In the notebook I kept, the two data questions took four tool steps each, every step another trip to the model. One loosely worded question pulled the full schema of all 16 tables into the conversation twice, and that single transcript runs to about 146,000 characters. This version sends 74 described columns once, about 7,300 characters, and gets one statement back. A second call happens only when that statement fails.

Client data to synthetic

A database I am allowed to show

The original ran on a client's accounts inside an accounting vendor's schema: 16 tables and 1,210 columns, of which 831 were empty or held one value in every row. None of that is here. I designed 10 narrow tables for this demo and filled them from a seeded generator, with 39 consistency rules checked at build time, such as every invoice total matching the sum of its lines.

Under the hood

Generated SQL is input, not code I trust

The prompt

What the model is asked for

It sees the tables, a one line meaning for every column, and a few facts the column names cannot carry: dates are text, the books close on 31 December 2025, and cost lives in two tables that must be totalled separately. Then one instruction: reply with a single SELECT, or with one fixed word when the question is not about this data. Checking for that word costs nothing, which is cheaper than a separate call to classify the question first.

The guard

Four checks between the model and the data

The statement must start with SELECT or WITH. SQLite then compiles it without running it, under an authorizer that allows reading the 10 tables and calling a fixed list of functions, and denies everything else: writes, PRAGMA, ATTACH, the schema tables, recursion, loading extensions. The compiled program is read back and refused if it would open anything for writing. Only then does it run, on a connection opened read only. I did not write a SQL parser for this. SQLite's own compiler decides what a statement is, so there is no gap between what my check believes the SQL means and what the database does with it.

The limits

Cut off, not trusted to finish

Every statement gets 1 second, 200 rows and 40 columns, and no single value may pass 1 MB. Joining the largest table to itself three times asks for hundreds of billions of rows. It is stopped at the time limit like anything else, and a statement stopped that way is not retried.

The repair

One second chance

When a statement is refused or fails to compile, the model gets the error text and one attempt to fix it. The page then shows both the statement that failed and the one that ran, so a repair is never hidden.

The tests

I tried to break it

The test file throws 115 hostile statements at the guard: stacked statements, second statements hidden behind comments, DROP and UPDATE, writes dressed up as a WITH query, ATTACH, PRAGMA, reads of the schema tables, forbidden functions, recursive loops and cartesian joins. Every one is refused or cut off, and the database file is byte for byte the same afterwards. A second list of ordinary statements has to pass untouched, because a guard that refuses honest SQL is broken in a different way. It is there because of a real mistake: my first version refused a plain COUNT over a WITH query. In all of these tests the model is a stand in that returns SQL I chose, including a wrong first answer followed by a correct repair.

The meter

A paid model behind a public page

Typed questions are limited to 6 a minute and 40 a day per address, 300 characters each, with a ceiling of 600 a day across all visitors. Recorded questions, and repeats of a question already answered, never call the model.

Limits

What this does not do

The guard makes a statement safe to run. It cannot make it right. A model can write SQL that runs cleanly and answers a slightly different question: the wrong date range, a join that counts something twice, completed projects where you meant all of them. That is why the SQL is always on screen. I have not scored this against a benchmark, so I make no accuracy claim. Each question stands alone, with no memory of the one before. Answers are tables, not sentences: I left out a second model call that would turn the rows into prose, because it doubles the cost and adds a place where a number can be misquoted. And the data is invented, so nothing here says anything about a real company.