LLM-Generated SQL Needs a Deterministic Guard: When the Fix Creates the Bug
SQL evaluates AND before OR, and most engineers do not think about this rule any more. However, the rule has again become relevant, due to the ability of today’s language models to generate SQL queries for others who will not see the generated code.
There are increasingly more organisations developing a type of tool that takes the questions posed by an operations manager, translates them into SQL, executes them against their warehouse and provides a numerical answer. This is an excellent utility, but the current versions of these tools are not yet accurate. When attempting to identify unused accounts via an NLP to SQL converter, the tool correctly wrote the is_deleted condition for the soft-delete pattern. However, it failed to add the additional deleted_at IS NULL condition to account for users who had been terminated long ago. Although the results looked good, the actual results were not. Another example of how LLMs produce poor-quality results is when someone asked a model to determine the average revenue per user (ARPU). The model produced the ideal query, with an INNER JOIN between the customers and orders tables. However, the model omitted customers who had registered for the service but had not completed an order, and so skewed the ARPU higher. Finally, if someone asks a model to evaluate month-over-month growth, there are several methods to accomplish this within SQL, such as BETWEEN, DATE_SUB() or even LIKE ‘2025-10-%’. If the LLM selects BETWEEN, which produces an inclusive result set, it will count the orders placed at the boundary between two months twice.
According to a 2025 paper detailing an internal text-to-SQL chatbot developed by LinkedIn, approximately 53% of the responses provided by the chatbot were deemed correct or close to correct, based upon reviews from expert evaluators. Furthermore, the chatbot described includes a feature that automatically corrects “hallucinations” and “syntax errors”. None of this should deter organisations from using these tools, but it does indicate why organisations should decide what these tools must never get wrong. The story consists of three main components: a flaw in the data, a correction to the prompt and the bug that the correction creates, and lastly a guard that sits between the model and the database.
Two ways of saying no
Consider a common issue associated with legacy databases. In legacy databases, a soft-delete flag indicates whether a record has been removed, and “not deleted” is represented in different ways throughout the same table, sometimes as 0 and sometimes as NULL. Many people have come across data that represents identical meanings differently, such as 0 and NULL, or 0 and blank. Much of it happens when applications graduate from legacy stacks to newer technologies, which support object-relational mapping (ORM) out of the box and resolve the ambiguity in values from then on. Additionally, some ambiguities occur simply because similar values are often determined by the user interface and the API consumers, and are left unverified by the backend services that should ensure consistency in value representation. A filter written as is_deleted = 0 silently drops every row stored as NULL, and a filter written as is_deleted IS NULL silently drops every row stored as 0. Because most reporting questions exclude deleted records, an OR expression that includes both NULL and 0 values is required in the WHERE clause of almost every query written by such a tool.
The fix that creates the bug
The obvious solution is to instruct the prompt in how to handle this problem: teach it that whenever a query uses is_deleted in its filtering logic, it must check for either NULL or 0. Once instructed appropriately, the model is likely to begin writing queries in compliance with the instruction at once, and it may write them like this (names and dates are illustrative):
WHERE created_on BETWEEN '2026-03-02' AND '2026-03-08'
AND is_deleted IS NULL OR is_deleted = 0
However, since SQL binds AND more tightly than OR, it will interpret the previous clause as two conditions rather than one:
WHERE (created_on BETWEEN '2026-03-02' AND '2026-03-08' AND is_deleted IS NULL)
OR (is_deleted = 0)
What is being described here are two alternatives, and the date filter applies only to the first. The first will only include rows whose created_on value falls within the specified date range, between 2026-03-02 and 2026-03-08. The second will return every row stored as 0, regardless of when it was actually created, even though the date filter was meant to exclude it. While the query itself runs fine and the result it provides appears to be logical and reasonable, it is completely incorrect. Only one additional set of parentheses is needed to create a version of the query that returns only the intended rows:
WHERE created_on BETWEEN '2026-03-02' AND '2026-03-08'
AND (is_deleted IS NULL OR is_deleted = 0)
There is value in specifically calling out why this behaviour occurs. It is not a hallucination, or a product of creative thinking. There is simply a predefined rule that the model has been given and is applying incorrectly. Specifically, it has misapplied operator precedence, resulting in the inclusion of rows from outside the date range. Every single rules-based prompt that is used to teach models about correctness issues is simultaneously creating an opportunity for the model to misapply the syntax for that rule.
Why a better prompt is not the answer
A lot of people believe that writing a better prompt will prevent problems like this. While a well-written prompt is likely to reduce the number of times a mistake like this happens, there is still no guarantee that a prompt will cover all possible scenarios. Additionally, while a prompt provides the model with the knowledge base required to make decisions and act appropriately, it does not replicate the experience and insights a person builds up over time through working with data. Further, some organisations rely on these NLP to SQL converters to inform business decisions, where small mistakes can have large consequences and give competitors an advantage.
One invariant, checked by a parser
The defence that holds is very specific and limited, yet it meets OWASP’s security guidance for LLM applications, which names improper output handling among the risks. That guidance advises treating the model like any other user, and validating what it returns before allowing any subsequent systems to use its results. The defence uses an open-source SQL parsing library, such as sqlglot, to parse each generated query in the warehouse’s own SQL dialect. It restricts the validation to WHERE and HAVING clauses, whether they occur directly or within common table expressions. The guard rejects only one case: an unparenthesised AND appearing as an operand of a top-level OR.
The check can be this precise because explicit parentheses produce separate nodes in the parse tree. As such, a query written as (a AND b) OR (c AND d) passes the test, because each AND is contained within its own parentheses. Likewise, a simple x = 1 OR x = 2 passes, because there are no AND operators present. The guard tests one condition on purpose, and tests nothing else.
When the guard determines that a problem exists with a query it validates, rather than attempting to modify the query to resolve the issue, it sends an error message back to the agent, requesting that the agent rewrite the query with appropriate parentheses around the problematic conditions. If the guard identifies an invariant failure, such as a fan-out bug or a missing soft-delete check, and attempts to repair it by modifying the SQL string, it transforms a lightweight NLP to SQL converter into a full-fledged SQL compiler. Such repairs mean injecting strings, adding clauses or wrapping subqueries in parentheses. At this point, the guard ceases to serve as an independent arbiter of the validity of queries and instead becomes a collaborator in generating them. The cost of rejecting rather than repairing is one additional round trip whenever the guard fires.
If the parser fails to interpret a query, the guard takes no action and allows the database engine to reject any malformed query. Most people would expect a safety check to fail closed. I recommend failing open in this circumstance, because if the parser cannot interpret a query, then something is wrong with the query, and it should be submitted to the database engine for execution. The database engine performs its primary function, rejecting malformed queries, and failing open minimises the potential for false positives, because there are situations in which the parser will not understand advanced syntax or keywords. In such cases, log messages will indicate why the parser was unable to process the query. Ultimately, it is up to the database engine to determine whether a query is valid and to return meaningful results.
Testing the guard against its own mistakes
A guard needs to be tested using the same process as any other classifier. For instance, you would use a test set to see how well the guard handles a trap, a legitimate query that contains an OR at the top level and should therefore pass unchanged. One way to start, and one that would fail this type of test, would be to simply look for any AND below a top-level OR. This approach would incorrectly flag a deliberate (a AND b) OR (c AND d), since it flags every AND, with or without parentheses. Looking only for an unparenthesised AND is what makes the check precise. Any guard, whether it is based on NLP or not, is a form of classifier and therefore requires consideration of its false-positive rate. Even so, I personally have a greater concern about a query being allowed through that contains incorrect information and invokes some sort of action, potentially damaging multiple parties’ interests. In contrast, inaction caused by a wrongly blocked query can typically be resolved relatively quickly. The next step in testing is also relatively inexpensive. Each time the prompt used to generate queries changes, all of the existing questions stored in a database are run again. Additionally, each filter applied to the flag is evaluated for both of its possible representations, 0 and NULL.
Who should own the invariants
The owner of the invariants should be either the data domain lead or the analytics team. A developer can build and test excellent NLP to SQL converters. However, developers frequently do not take into account the business context behind the values reported, or the potential consequences of a logical hole. An invariant should be clearly defined, and it should not reflect a changing reality. Rather than stating something similar to “the query generated from this prompt should appear acceptable to the operations group”, state something closer to “irrespective of what the user asks, the number of records returned must always be less than or equal to the number of records in the parent table”. When the guard passes a query as valid, but two similarly worded prompts produce different results, you know that your guard is slipping.
Guards help protect against silent errors in queries created by an LLM. Silent errors occur where a query runs correctly but the results are wrong because of some implicit assumption. Critical business decisions are sometimes dependent upon the results of queries generated by an LLM. If they are, document the few invariants which, if violated, silently cause a wrong result. Those invariants belong in code rather than in written documents. In doing so, you are allowing the prompt to determine intent and a parser to determine correctness. A single guard addresses just one such invariant and does not make a tool completely safe, but it does allow you to detect one type of silent error that previously went undetected.
Which of your own tables has two ways of saying no?