Scene (composite, not a production postmortem): a nightly SQL review bot posts a merge comment that sounds complete, calm, and specific. The query joins orders to customers, and the bot claims an index already covers the foreign key. No reviewer opens the catalog, because the comment already named the index and estimated the cost. The guess never appeared as a guess, so the pull request merged with an undeclared hole.
Agentic review tools now sit beside linters, and they fail in a quieter way than syntax errors. They fill missing catalog facts with fluent language instead of stopping. Cheap model calls make a second opinion easy to request, which is useful only if the opinion cannot smuggle invented indexes, nullability, or row counts into a merge comment.
This article stages a structured debate for teams that already generate SQL with models. Position A requires a machine-checked assumption ledger before any finding may be published. Position B keeps a single free-form pass and treats extra structure as token waste. The artifact is a fixture ledger, a checker, and a decision rule you can apply without adopting a vendor narrative.
Why silent guesses survive cheap reviews
SQL review is not a closed-book exam. A model can emit a plausible index name that matches local naming habits, and humans read that name as evidence. Pull-request culture rewards speed, so a confident comment often replaces a catalog lookup. Free-tier tokens make it tempting to review every statement, including ones whose safety depends on facts the prompt never contained.
The failure is not that models reason. The failure is that findings and guesses share the same prose channel. Once those channels mix, a reviewer cannot tell which clauses were checked against a dump, a live catalog, or nothing. That mixing is the debate, not the brand of the model endpoint.
Position A: publish a ledger, then allow findings
Position A says a review agent may not emit safe, blocking, or index-covered until it writes a structured ledger. Each ledger row names an object, a claim kind, a confidence tag, and a source. Human-supplied schema facts are given. Model-invented facts are guess. The checker fails the job when a finding depends on a guess that affects locks, indexes, or cardinality.
Advocates treat this as an interface contract, not as a writing style. Compilers already refuse undeclared symbols; review bots should refuse undeclared world facts. The ledger also creates a diffable artifact for later incidents, because you can see whether the bot assumed orders.deleted_at or whether a human asserted it.
A two-pass shape follows from that contract. Pass one may only propose unknowns and copy human facts. Pass two may write findings that cite ledger ids. If the free allocation is large enough for two short calls, the extra pass is an accounting choice rather than a research project. Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode is an open project that currently offers free model access and a free server option, which is enough to host this checker beside a second model pass without inventing extra product claims.
Position B: one free-form pass is the honest budget
Position B says the ledger is a second product hiding inside a review bot. Incomplete ledgers fail closed and block harmless queries, which trains authors to ignore the bot. Models that must fill JSON often hedge, drop edge cases, or spend tokens restating the schema instead of inspecting the join. Teams with partial dumps cannot populate given rows without pretending certainty.
Token cost is a first-class objection here, not a footnote. A second structured call can double prompt size when the schema fragment is already large. If the review is local, advisory, and read by the author only, free-form prose with hedging words may be cheaper and equally safe. Position B also notes that a weak SQL parser in the checker will false-flag aliases, CTEs, and generated columns, which makes the ledger look stricter than it is.
In this view, the remedy is narrower input, not more ceremony. Feed only the changed statement plus a trimmed catalog excerpt, ask for findings, and keep humans as the catalog authority. Spending free tokens on a second pass does not create truth if the catalog excerpt was already wrong.
Evidence both sides can inspect without theater
Neither position needs a leaderboard. Both sides can use the same three fixtures and record whether the bot invented an index, ignored a missing predicate, or blocked a safe SELECT. Label the runs as fixtures until you replay them on your engine. Do not cite unpublished latency numbers, and do not treat a single model sample as a benchmark.
Fixture 1 — invented covering index. The statement filters orders.customer_id with no index in the catalog excerpt. A free-form bot often names orders_customer_id_idx. A ledger bot must tag that name as guess and therefore cannot say covered.
Fixture 2 — lock-adjacent update. The statement updates a hot table with a non-sargable LOWER(email) predicate. Position A wants an explicit unknown for selectivity. Position B argues a single paragraph already warns about the function, and the extra JSON changes nothing.
Fixture 3 — CTE alias noise. The statement projects o.id from orders AS o. A naive checker that does not understand aliases will demand o as a table. Position B uses this fixture to cap how much authority the checker deserves.
A reproducible ledger and tripwire
The following schema is a proposal. It is small enough to keep next to the review script on a free server. It is not a parser for every dialect, and it will not replace EXPLAIN on the target engine.
{
"review_id": "fixture-2026-09-06-01",
"statement_sha256": "replace-with-hash",
"assumptions": [
{
"id": "A1",
"kind": "table_exists",
"object": "public.orders",
"confidence": "given",
"source": "human"
},
{
"id": "A2",
"kind": "index_exists",
"object": "public.orders_customer_id_idx",
"confidence": "guess",
"source": "model"
}
],
"unknowns": ["public.orders.customer_id_selectivity"],
"findings": [
{
"id": "F1",
"severity": "blocking",
"claim": "index-covered",
"depends_on": ["A2"]
}
]
}
Numbered workflow
- Export a human catalog excerpt as YAML or JSON, and mark every object
source: humanbefore any model call. - Send the SQL plus that excerpt to a free-tier chat model, and ask only for new
unknownsandguessrows, not for merge advice. - Merge those rows into the ledger, then run the checker below on disk, without network access during the check.
- If the checker exits non-zero, discard findings and publish the ledger plus unknowns instead of a green comment.
- If the checker exits zero, allow a second model call that may cite ledger ids in findings, still without inventing objects.
- Store the ledger beside the review log on the same host, so an incident review can see guesses without scraping chat prose.
Checker (labeled fixture code)
#!/usr/bin/env python3
"""Fail a SQL review JSON file when findings depend on model guesses.
Proposal / unexecuted against production traffic. Extend the kind set
before you trust this near merge gates.
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
SAFETY_KINDS = {
"index_exists",
"column_not_null",
"unique_key",
"row_estimate",
"lock_mode",
}
BLOCKING_CLAIMS = {"safe", "index-covered", "low-cost", "no-lock-risk"}
def load_review(path: Path) -> dict:
data = json.loads(path.read_text(encoding="utf-8"))
for key in ("assumptions", "findings"):
if key not in data or not isinstance(data[key], list):
raise ValueError(f"missing list: {key}")
return data
def index_assumptions(review: dict) -> dict:
out = {}
for row in review["assumptions"]:
out[row["id"]] = row
return out
def violations(review: dict) -> list[str]:
problems = []
by_id = index_assumptions(review)
for finding in review["findings"]:
claim = str(finding.get("claim", "")).lower()
depends = finding.get("depends_on") or []
if claim in BLOCKING_CLAIMS and not depends:
problems.append(f"{finding.get('id')}: {claim} has empty depends_on")
continue
for dep in depends:
row = by_id.get(dep)
if row is None:
problems.append(f"{finding.get('id')}: missing assumption {dep}")
continue
guessed = row.get("source") == "model" or row.get("confidence") == "guess"
if guessed and row.get("kind") in SAFETY_KINDS and claim in BLOCKING_CLAIMS:
problems.append(
f"{finding.get('id')}: {claim} depends on guessed {row.get('kind')} "
f"{row.get('object')}"
)
return problems
def main() -> int:
if len(sys.argv) != 2:
print("usage: check_ledger.py review.json", file=sys.stderr)
return 2
review = load_review(Path(sys.argv[1]))
problems = violations(review)
if problems:
print("LEDGER_FAIL")
for item in problems:
print(item)
return 1
print("LEDGER_OK")
return 0
if __name__ == "__main__":
raise SystemExit(main())
Commands for the fixture
python3 check_ledger.py fixture-2026-09-06-01.json
# expected for the sample above: LEDGER_FAIL and a line naming A2
# After a human deletes finding F1 or downgrades A2 out of SAFETY_KINDS:
python3 check_ledger.py fixture-2026-09-06-01.json
# expected: LEDGER_OK
Keep the model prompt boring. Ask for ledger rows only, then run the checker, then optionally ask for findings. If you place that loop on a free server, the important part is the non-zero exit, not the hosting brand. Ten million free tokens make a second pass affordable for fixture suites; they do not make a guessed index real.
A decision rule you can apply on Monday
Use Position A, the ledger plus checker, when any of the following is true.
- The bot can comment on a shared pull request or flip a merge gate.
- The SQL can take locks, rewrite a hot table, or disable an index.
- The catalog excerpt in the prompt is partial, stale, or generated by another model.
- Authors have started quoting the bot as evidence instead of opening
psqlor the equivalent.
Use Position B, one free-form pass, only when all of the following are true.
- Output stays local to the author and never becomes a review comment.
- The database under review is a disposable fixture with a known dump.
- The statement is read-only, and the author already ran
EXPLAINon the target engine. - A second model call would crowd out reviews you actually read this week.
If the two lists both fire, fail closed: publish unknowns, not a green finding. That rule is deliberately boring. It prefers a missed nit over an invented covering index in a merge thread.
Limitations and who should skip this
The checker does not parse SQL, bind aliases, or understand partitioning. It only enforces citations between findings and ledger ids. Teams without a human catalog source will fill given rows with folklore, which is worse than free-form hedging. Do not use this approach as a substitute for engine-native EXPLAIN, lock graphs, or statement timeouts.
Skip the ledger if you need sub-second interactive hints inside an editor. Skip it if your dialect relies on generated columns and the checker would fail every CTE. Skip it if nobody will read LEDGER_FAIL output, because ignored ceremony is how Position B wins by default. Free model access and a free server do not change those limits; they only remove the excuse that a second pass was unaffordable to try on fixtures.
The useful outcome is a split channel: guesses in JSON, judgments only after a local tripwire. If you already have free model access and a host, run the sample file through the checker before you let a bot speak in a merge comment.
Top comments (0)