Check an AI-written SQL query against hand-computed SQLite results
Treat an AI-generated SQL query as a draft and test it on a tiny SQLite database with known answers. Compare the AI result with a manually calculated expected table before using the query on real data.
Content checked 2026.09.20Hand-checked example
Show contents
Who this is forThis guide is for people who use AI to draft SQL and want a small reproducible test before running the query on important data.
What you need
Python 3.12 and a terminal command that starts that version.
A working folder where the scripts can create folders beneath outputs.
Basic familiarity with SELECT, JOIN, GROUP BY, and aggregate functions.
Only the Python standard library is required: csv, pathlib, and sqlite3.
01State the business question before reviewing SQL
Suppose the requirement is: for every customer, report the total value of PAID orders placed during August 2026. Customers with no qualifying orders must still appear with a total of 0.
An AI assistant proposes an INNER JOIN between customers and orders, followed by WHERE conditions for status and date. The query looks reasonable, but the INNER JOIN removes customers who have no matching order rows. That violates the requirement to include zero-total customers.
02Create a synthetic SQLite database
The following database is synthetic and was written specifically for this article. It has three customers and six orders chosen to test paid versus unpaid status, month boundaries, and a customer with no August paid order.
Ana has two PAID August orders: 40 and 60, so her total is 100. Her cancelled order does not count. Ben has one qualifying August order worth 25; his September order does not count. Cara has only a cancelled August order, so she must still appear with 0.
customer_id
customer_name
Expected August paid total
1
Ana
100
2
Ben
25
3
Cara
0
The expected grand total is 125. There should be exactly 3 output rows because the requirement says every customer must appear.
04Compare the AI query with a revised query
The AI query below filters qualifying orders in the WHERE clause after an INNER JOIN. Cara has no matching PAID August order, so she disappears entirely.
sql
SELECT
c.customer_id,
c.customer_name,
SUM(o.amount) AS august_paid_total
FROM customers AS c
JOIN orders AS o
ON o.customer_id = c.customer_id
WHERE o.status = 'PAID'
AND o.order_date >= '2026-08-01'
AND o.order_date < '2026-09-01'
GROUP BY c.customer_id, c.customer_name
ORDER BY c.customer_id;
The revised query starts from all customers and uses a LEFT JOIN. The order filters are placed inside the ON condition so nonmatching customers remain. COALESCE converts the resulting NULL aggregate to 0.
sql
SELECT
c.customer_id,
c.customer_name,
COALESCE(SUM(o.amount), 0) AS august_paid_total
FROM customers AS c
LEFT JOIN orders AS o
ON o.customer_id = c.customer_id
AND o.status = 'PAID'
AND o.order_date >= '2026-08-01'
AND o.order_date < '2026-09-01'
GROUP BY c.customer_id, c.customer_name
ORDER BY c.customer_id;
05Run both queries and compare them with the expected rows
Save the following script as ai_sql_check.py. It runs both queries, compares their results with the hand-computed expected table, and writes a CSV review report. It does not modify the synthetic database.
python
import csv
import sqlite3
from pathlib import Path
DATABASE = Path("outputs") / "ai_sql_demo" / "orders.sqlite"
OUTPUT_DIR = Path("outputs") / "ai_sql_check_result"
REPORT = OUTPUT_DIR / "sql_check.csv"
AI_SQL = """
SELECT c.customer_id, c.customer_name, SUM(o.amount)
FROM customers AS c
JOIN orders AS o ON o.customer_id = c.customer_id
WHERE o.status = 'PAID'
AND o.order_date >= '2026-08-01'
AND o.order_date < '2026-09-01'
GROUP BY c.customer_id, c.customer_name
ORDER BY c.customer_id;
"""
REVISED_SQL = """
SELECT c.customer_id, c.customer_name, COALESCE(SUM(o.amount), 0)
FROM customers AS c
LEFT JOIN orders AS o
ON o.customer_id = c.customer_id
AND o.status = 'PAID'
AND o.order_date >= '2026-08-01'
AND o.order_date < '2026-09-01'
GROUP BY c.customer_id, c.customer_name
ORDER BY c.customer_id;
"""
EXPECTED = [
(1, "Ana", 100),
(2, "Ben", 25),
(3, "Cara", 0),
]
def run_query(connection, sql):
return connection.execute(sql).fetchall()
def main() -> None:
if not DATABASE.is_file():
raise FileNotFoundError(f"Database not found: {DATABASE}")
if OUTPUT_DIR.exists():
raise FileExistsError(f"Output folder already exists: {OUTPUT_DIR}")
connection = sqlite3.connect(f"file:{DATABASE.resolve()}?mode=ro", uri=True)
try:
ai_rows = run_query(connection, AI_SQL)
revised_rows = run_query(connection, REVISED_SQL)
finally:
connection.close()
OUTPUT_DIR.parent.mkdir(parents=True, exist_ok=True)
OUTPUT_DIR.mkdir()
with REPORT.open("x", encoding="utf-8", newline="") as stream:
writer = csv.writer(stream)
writer.writerow(["query", "rows", "matches_expected"])
writer.writerow(["AI", repr(ai_rows), ai_rows == EXPECTED])
writer.writerow(["REVISED", repr(revised_rows), revised_rows == EXPECTED])
print(f"Expected rows: {len(EXPECTED)}.")
print(f"AI rows: {len(ai_rows)}; matches expected: {ai_rows == EXPECTED}.")
print(
f"Revised rows: {len(revised_rows)}; "
f"matches expected: {revised_rows == EXPECTED}."
)
print(f"Report: {REPORT.as_posix()}")
if revised_rows != EXPECTED:
raise RuntimeError("Revised SQL does not match the expected result.")
if __name__ == "__main__":
main()
text
python ai_sql_check.py
06Check the expected output
The AI query should return only Ana and Ben, so it produces 2 rows and does not match the expected result. The revised query should return all 3 customers and exactly match the hand-computed table.
Query
Rows returned
Expected grand total
Matches expected table
AI
2
125
No
Revised
3
125
Yes
The AI result can still have the correct grand total of 125 even though it is wrong because Cara's required zero row is missing. This is why checking only totals is insufficient.
Include customers with zero matching records in the synthetic data when the requirement says they must appear.
Test date boundaries such as August 31 and September 1.
Include excluded statuses such as CANCELLED.
Run the checker again without changing OUTPUT_DIR. It should stop with FileExistsError rather than overwrite the report.
Common problem
What to inspect
Unexpected missing rows
Check JOIN type and whether filters in WHERE remove unmatched LEFT JOIN rows.
Totals too large
Look for one-to-many joins that duplicate rows before aggregation.
Wrong date range
Use an explicit lower bound and exclusive upper bound appropriate to the stored date format.
NULL instead of zero
Decide whether missing matches should stay NULL or be converted with COALESCE.
Correct total but wrong detail
Compare complete expected rows, not only the grand total.
This test proves only that the revised query matches this small synthetic example. Real schemas may contain duplicate relationships, NULL values, timestamps, time zones, refunds, or additional business rules. Expand the test data whenever those conditions matter.
Execution and verification record
2026-09-20 · hand-checked example · target: Python 3.12 · standard library: csv, pathlib, sqlite3 · no execution
Manually calculated Ana's qualifying August total as 40 + 60 = 100.
Manually calculated Ben's qualifying August total as 25 and excluded his September order.
Manually determined that Cara must appear with 0 because her only August order is CANCELLED.
Manually calculated the expected grand total as 125 and expected row count as 3.
Inspected the AI query and determined that its INNER JOIN and WHERE filters omit Cara, producing 2 rows.
Inspected the revised LEFT JOIN query and expected it to return the three hand-computed rows.
Derived the expected console output and report comparison by hand.
Verification limits
The code was not executed by the author of this response; SQLite and filesystem behavior were not tested here.
Only the listed synthetic rows were evaluated; NULL amounts, duplicate joins, timestamps, time zones, refunds, and large datasets were not tested.
Matching this example does not prove the query is correct for every production schema or business rule.
The official documentation URLs were provided from known documentation locations but were not checked live.
Treat an AI-generated regex as a draft, not a finished rule. Build a small synthetic test table, compare expected and actual matches, revise the pattern, and save a review report before using it on real data.
Show an AI a few examples of the exact output structure you want, then validate the returned JSON before using it. A small synthetic ticket dataset demonstrates how formatting examples and automatic checks work together.
Break an AI summary into individual claims, link each claim to specific source sentences, and mark unsupported or overstated statements before reuse. A small synthetic document shows why fluent summaries still need evidence checks.
Turn “automate this” into an executable task description. Attach a synthetic sample with no sensitive data and a hand-checked expected result to complete a request for a script that totals work logs by team.