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.20Example code included
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
ChatGPT Python tool · Python 3.13.5 · standard library csv/pathlib/sqlite3
The hand-computed expected rows were confirmed as (1, Ana, 100), (2, Ben, 25), and (3, Cara, 0), with grand total 125.
The AI query returned exactly 2 rows: Ana=100 and Ben=25. Cara was omitted, so the result did not match EXPECTED.
The revised LEFT JOIN query returned exactly 3 rows: Ana=100, Ben=25, and Cara=0, matching EXPECTED exactly.
The main run printed exactly: 'Expected rows: 3.', 'AI rows: 2; matches expected: False.', 'Revised rows: 3; matches expected: True.', and 'Report: outputs/ai_sql_check_result/sql_check.csv'.
sql_check.csv contained a header plus two data rows. The AI row recorded "[(1, 'Ana', 100), (2, 'Ben', 25)]" with False, and the REVISED row recorded "[(1, 'Ana', 100), (2, 'Ben', 25), (3, 'Cara', 0)]" with True.
The AI result still summed to 125 despite omitting Cara, confirming the article's statement that the grand total alone is insufficient.
The SHA-256 hash of orders.sqlite was unchanged after running the checker, confirming the database was opened read-only and not modified.
A second checker run stopped with FileExistsError because outputs/ai_sql_check_result already existed.
Verification limits
NULL amounts, duplicate join relationships, additional date-boundary cases such as August 31, timestamps, time zones, refunds, malformed databases, permission failures, and large datasets were not separately stress-tested.
The Python runtime emitted an unrelated artifact-tool spreadsheet warmup warning on stderr, but both article scripts themselves executed successfully and the checker exited with code 0 on the verification run.
References
The explanations and examples were written for this site. See the official sources below for the related behavior and concepts.
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.
Check an aggregation function with four rows you can calculate by hand and 12 unit tests. Verify not only normal values but also empty input, zero, decimals, and invalid input.
Check an AI report against its source data, recalculate totals and rates, and use a copyable review prompt to flag unsupported claims and overstated causes before submission.
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.