SQLFix Blog
Natural language to SQL: safety checks that matter
Translating a question into SQL is genuinely useful for first drafts, but auto-executing model output against a live database is how a typo becomes a deleted table. The safe posture is draft-only: the model proposes, a human or a guarded pipeline disposes. SQLForm follows that draft-only model and never runs what it generates without a review step.
1. Start with a draft-only pipeline
I built SQLForm around the idea that a language model is a drafting assistant, not an execution engine. When a user asks “show me last month’s sales by region”, the model returns a SELECT statement. That statement is placed in a draft queue and never sent to the production connector. A human operator or an automated gate must explicitly approve the draft before it leaves the queue. This separation creates a clear audit trail: every query that touches data has a recorded approval step.
The draft queue is persisted in a lightweight table with columns for user ID, timestamp, raw prompt, generated SQL, and approval status. If the system crashes, the queue can be replayed and no query is lost. I chose this design after seeing teams lose data because a demo script ran a DELETE statement that the model thought was a SELECT. The queue prevents that class of mistake by construction.
2. Detect mutating statements before they reach the queue
The first line of defense is a simple lexical scan. I look for the tokens DELETE, UPDATE, INSERT, DROP, TRUNCATE, ALTER, GRANT, REVOKE, and CREATE. If any of these appear, the draft is flagged as mutating and placed in a holding state that requires explicit approval from a senior reviewer. Even a SELECT that contains a WITH clause that calls a mutating function is caught because the scan is performed on the raw SQL string before any parsing.
I also check for statement separators. A semicolon followed by another token means the user might be trying to chain operations. For example, “SELECT * FROM users; DELETE FROM logs” would pass a naïve SELECT‑only check but is blocked because the scanner sees the DELETE token after the first semicolon.
To avoid false positives on legitimate object names (like a column named “delete_flag”), the scanner only treats tokens as keywords when they are followed by whitespace or a parenthesis, matching typical SQL grammar. This keeps the rule precise without banning useful identifiers.
3. Run drafts in a read‑only sandbox
Once a draft is approved, I execute it against a read‑only replica of the production schema. The replica is refreshed nightly from the primary and has no write privileges assigned to the connection role used by SQLForm. If the model somehow produces a mutating statement, the database will return an error and no data is changed. This turns a potentially costly mistake into a visible failure that the reviewer can see immediately.
I also enforce the read‑only property at the connection level by setting the PostgreSQL parameter default_transaction_read_only = on for the session. In MySQL I start the session with SET SESSION TRANSACTION ISOLATION LEVEL READ ONLY;. These settings cause the server to reject any WRITE, ALTER, or DROP command with a clear error code, which I surface in the UI.
Using a replica has an added benefit: the query can run against slightly stale data without affecting SLAs, and I can monitor query length and resource usage without risking production load. If the replica lags beyond a configurable threshold, the system holds the draft and notifies the approver that the data may be out of sync.
4. Parameterize approved queries
After a draft clears the human review step, I move the SQL into the application layer as a prepared statement. The model‑generated text becomes a template where any user‑supplied values are bound as parameters. I never concatenate raw strings, even after approval, because that re‑introduces the injection risk the draft was meant to avoid.
I follow the guidance from the OWASP SQL Injection Prevention Cheat Sheet, which lists prepared statements as the primary defense. In practice I use the database driver’s native API: pg_prepare / pg_execute for PostgreSQL, and PreparedStatement for MySQL. The driver takes care of escaping and type safety, so a value containing a quote or a semicolon is treated as data, not as SQL.
To make sure the template stays valid, I parse the approved SQL with a lightweight SQL parser (such as sqlparse) and verify that every placeholder corresponds to a parameter index. If the parser finds a stray token that looks like a value but is not bound, the draft is sent back for revision.
5. Human review as the control point
The reviewer is not expected to be a SQL expert; the checklist is deliberately short. First, they verify that the SELECT list matches the columns mentioned in the original prompt. If the user asked for “total revenue per region” and the query returns COUNT(*), the reviewer flags it. Second, they confirm that no mutating token appears, relying on the scanner but also doing a visual scan for any clever obfuscation. Third, they glance at the WHERE clause to see if it inadvertently pulls more rows than intended (for example, missing a date filter that would cause a full table scan).
I treat the generated query as a suggestion, exactly as I would review SQL written by a new teammate. The reviewer can approve, request a change, or reject. If they request a change, they can edit the draft directly in the UI; the edited version becomes a new draft and goes through the same checks again. This loop ensures that the model’s output is never the final authority.
The speed gain comes from eliminating the need to write boilerplate SQL from scratch. I have seen teams cut the time to produce a first‑draft query from ten minutes to under two minutes, while still keeping a human in the loop for safety.
What this is NOT
I do not claim that SQLForm catches every possible mistake. The lexical scanner can be evaded by unusual encoding or by using database‑specific syntax that looks like a comment. A determined attacker could craft a prompt that makes the model output a statement that uses a stored procedure with side effects, which the scanner would not flag because the procedure name is not in our keyword list. The read‑only replica protects against accidental writes, but if the replica is misconfigured and inherits write rights, the safety net disappears.
Human review is essential, but reviewers can miss subtle logic errors, especially when the query is long or uses complex window functions. The process reduces risk, but it does not eliminate it. I treat SQLForm as a risk‑reduction tool, not a silver bullet.
References
https://cheatsheetseries.owasp.org/cheatsheets/LLM_Prompt_Injection_Prevention_Cheat_Sheet.html
https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html
https://cwe.mitre.org/data/definitions/798.html
https://cwe.mitre.org/data/definitions/89
FAQ
Only against a read-only replica or with a hard gate that blocks mutating statements. Auto-running generated SQL against production is how a draft error becomes data loss. Keep the model in a propose-only role.
A model draft is untrusted input until reviewed, the same class as CWE-89 SQL injection. A read-only role means even an accidental execution cannot change production data.
No. Once a query moves into application code, use prepared statements. Concatenating values back into a string reintroduces injection risk. Review and parameterization are separate controls.
DELETE, UPDATE, DROP, TRUNCATE, and privilege-granting statements should never run in an auto-execution path. They require explicit, separate approval.
Try the product: SQLForm
2026-09-16 · primary sources only · no fabricated traffic metrics
Related articles
Parameterized queries vs generated SQL
Explore the full AI micro-SaaS topic matrix →
Can NL2SQL be safe to auto-run?
Only against a read-only replica or with a hard gate that blocks mutating statements. Auto-running generated SQL against production is how a draft error becomes data loss. Keep the model in a propose-only role.
Why is a read-only context important? A model draft is untrusted input until reviewed, the same class as CWE-89 SQL injection. A read-only role means even an accidental execution cannot change production data. Does reviewing SQL mean I can skip parameterization?
A model draft is untrusted input until reviewed, the same class as CWE-89 SQL injection. A read-only role means even an accidental execution cannot change production data.
What statements should a safety layer block by default?
DELETE, UPDATE, DROP, TRUNCATE, and privilege-granting statements should never run in an auto-execution path. They require explicit, separate approval.
Decision-support only. Not a substitute for DBA review or production load testing. No performance guarantees.