A small model taught to write SQL on a laptop, behind a parser-based guard and a confidence score honest enough to act on.
SELECT touching approved tables. A keyword blocklist was built first and shown to fail in both directions.Natural-language SQL puts data access in the hands of people who cannot read SQL. That is the entire value, and precisely why the system must protect them: they cannot spot a query that is wrong, and certainly not one that is destructive.
Two questions. Can a model small enough to run locally be taught to write correct SQL for a specific schema and dialect? And can the surrounding system be made safe enough that a non-technical analyst using it is not a risk to the database?
4-bit CUDA training tooling does not run on Apple silicon at all, and 7–8B quantized weights plus optimizer state will not fit in 16GB shared with the OS. MLX does the same job, frozen quantized base, trainable adapters, natively on Metal. Measured peak memory in training was 1.1 GB at rank 16 on 8 layers; the constraint had headroom to spare.
A larger model already writes passable SQL, so fine-tuning moves it modestly. A small coder model fails in exactly the ways LoRA fixes, wrong dialect, invented columns, malformed joins, so the base-versus-tuned gap is wider. The failure dump bears this out: the base model's invalid queries are real schema errors ("no such column: T2.Country", aggregate-in-GROUP-BY), not formatting noise.
Local inference means no per-query cost and no schema leaving the machine, which is why the execution-accuracy numbers cost zero API quota: the fine-tuned model generates every query on the M1.
"I picked the smallest model that could learn the task, because the question I cared about was how much capability LoRA buys per parameter, not how high I could push absolute accuracy."
LoRA freezes the pretrained weight and trains two small matrices beside it, rank 16 on 8 layers, peak 1.1 GB, on a 16GB laptop.
No equations: the base weight matrix is frozen; two small matrices train beside it and are added to its
output. Rank is how much room the adjustment gets (16 here); alpha how
strongly it applies (32). B starts at zero, so the adapted model begins exactly equal to the
base and improves from there.
One module, imported by both the training project and the serving project. If they drift, the model is asked to work against a format it never saw, and the failure looks like "the fine-tune didn't work" rather than "the prompt changed".
Table name plus column names is the choice; types and sample rows roughly triple the token count for a modest gain, and sequence length is the binding constraint in 16GB. Examples over the cap are dropped and counted, never truncated, 73 of 7,000 (1.0%), because a truncated example teaches the model to emit truncated SQL.
Deliberately, 42.5% execution accuracy, 40% invalid SQL on 200 Spider dev examples, so the fine-tuned comparison cannot be quietly flattered after the fact.
The standard rule says watch validation loss; when it rises while training loss falls, you are overfitting, so stop. The run showed the exception. Validation (next-token) loss bottomed at iter 200 (1.41) and then climbed steadily to 2.09 at iter 2000, the textbook overfitting signal. But execution accuracy did the opposite: +0 points at 200 iters, +1 at 600, +5 at 2000. For a task metric, the language-modelling loss is the wrong early-stopping signal; you have to evaluate the task itself. Early-stopping on val loss would have shipped a model with no gain at all.
The blocklist is built first, and it fails 4 of 6 test cases, in both directions at once.
Legitimate queries refused because the word "delete" appears inside a string literal, and inside a comment, places where it has no power at all. Users blocked for no reason route around the tool, which is how shadow database access starts.
CREATE TABLE passes because nobody thought to list "create". Every new pattern is another round of whack-a-mole.
A blocklist of verbs has nothing to say about which tables you may read. An ordinary SELECT walks restricted data straight out.
String matching inspects characters. What matters is structure, the blocklist got 4 of 6 wrong; the AST guard blocked 300 of 300.
Then the AST guard: parse with sqlglot, allow only a single SELECT touching
approved tables. The shift is from denying what you can think of to allowing only what you
approved, a verb invented after the code was written is refused by default. Because the parser walks
the whole tree, a restricted table reached through a nested subquery is caught for free. Measured: 300 of
300 red-team queries blocked across 20 databases, 100%. It also caught the fine-tuned model's own
mistakes: when the model emitted a malformed nested subquery, the guard refused it as unparseable rather
than executing garbage.
A confidence score is a promise. Calibration measures whether it is kept, bucket by bucket, because averages hide the failure. Measured ECE: 0.21.
The guard stops catastrophe. It does nothing about a safe SELECT that answers the wrong
question. So each query carries a confidence score, and anything below a threshold goes to a human. That
plan is sound only if the score means something. Calibration is whether it does;
ECE is the average gap between claimed and delivered confidence, measured in bins. Measured
here: ECE = 0.21, moderately miscalibrated, and honestly reported as such rather than assumed to be
trustworthy.
The escalation tradeoff, the confidence score doing real work, cutting wrong auto-runs from 13 to 3 (50 examples):
| Confidence threshold | Auto-run correct | Auto-run wrong | Sent to human |
|---|---|---|---|
| 0.0 (no gate) | 30 | 13 | 0 |
| 0.5 | 29 | 6 | 8 |
| 0.7 | 29 | 3 | 11 |
| 1.0 (gate everything) | 28 | 2 | 13 |
Every query the guard admits is safe; a significant share (40% of them) still answer the wrong question. The guard is a hard boundary against catastrophe and offers nothing against being confidently, safely wrong. The confidence threshold addresses that, and it is strictly weaker: it depends on the score being calibrated, and at ECE 0.21 the score is only moderately trustworthy, the escalation table above is honest about how many wrong queries still slip through at each threshold.
A small amount of code that blocked 100% of a 300-query red team and refuses a destructive statement no matter how confident anything claims to be. Add the fine-tuned model behind it, it beats the base by 5 points and halves invalid SQL, and always return the generated SQL alongside the results.