All posts

Deterministic checks for AI-written migrations

Coding agents write database migrations quickly and confidently, and the mistakes they make are the same ones people make. Why the check that catches them has to be a rule, not another model.

A coding agent asked to "add a status column to orders" will do it in seconds. It will also, more often than not, write the version that fails on a table with rows in it, or the version that takes an exclusive lock and holds it for the whole rewrite. Not because the model is bad at SQL — because the dangerous form and the safe form look almost identical, and the difference only matters at production scale, which the model never sees.

-- what the agent wrote
ALTER TABLE orders ADD COLUMN status text NOT NULL;

-- what survives contact with a table that has rows
ALTER TABLE orders ADD COLUMN status text;
UPDATE orders SET status = 'open' WHERE status IS NULL;  -- in batches
ALTER TABLE orders ALTER COLUMN status SET NOT NULL;

That first statement is BV002. It is one of the oldest and best-understood migration mistakes there is, and it is exactly the kind of thing an AI reviewer will sometimes catch and sometimes wave through, depending on the prompt, the context window, and the day.

Why the reviewer cannot be another model

The obvious fix is to ask a second model to review the first model's migration. It works often enough to feel like it works. It does not work in the way a team can build a process on, for three reasons.

  • It is not repeatable. The same migration reviewed twice can get two verdicts. A pull-request check that is red on Tuesday and green on Wednesday for the same diff teaches people to click through it.
  • It cannot say what it did not check. A model that returns "looks fine" gives you no list of the properties it verified. A rule corpus is a list by construction: these 104 patterns were checked, these fired, these did not.
  • Its claims are not verifiable. When a rule says "this rewrites the table under an ACCESS EXCLUSIVE lock", that claim was tested against a real Postgres and the fixture is in the repository. A model's claim is a sentence.

None of this is an argument against using agents to write migrations. It is an argument about where the trust boundary goes. The agent proposes; something deterministic disposes.

What a deterministic check looks like in an agent workflow

The check has to sit where the agent can reach it, and it has to return the same shape every time so the agent can act on it without interpretation. Three placements cover most setups.

  1. Inside the agent's loop. Bolvrk runs as an MCP server. An agent that has just drafted a migration calls the check tool, gets findings back as JSON with a rule id, a severity and the fix, and revises before the human ever sees the draft. The skills library in the public repository tells the agent when to do this unprompted.
  2. In the terminal. npx bolvrk check migration.sql — no account, no config, exit code 1 on findings. This is the same rule set the MCP server runs, so a person double-checking an agent's work sees exactly what the agent saw.
  3. On the pull request. The GitHub Action posts one summary comment and fails the job above the severity the team chose. Whoever, or whatever, opened the PR, the gate is the same.

The point of running the same corpus in all three places is that the verdict does not change as the migration moves from the agent's draft to the terminal to CI. The agent cannot talk its way past the Action, because the Action is not listening to arguments.

The rules that matter most for generated SQL

Looking at what agents actually produce, a handful of rules do most of the work.

  • BV002ADD COLUMN … NOT NULL without a default.
  • BV003 — an index created without CONCURRENTLY, blocking writes for the whole build.
  • BV034 — a lock taken with no lock_timeout, so one slow transaction turns into a queue behind it.
  • BV030 — a DROP COLUMN while code that reads the column is still deployed.

Every one of these has a page that says what the rule catches, the SQL it fires on, and the safe pattern. That is deliberate: when an agent explains a finding to a person, or a person to an agent, the explanation should come from the same place and say the same thing.

What this is not

A rule corpus does not know your business. It cannot tell you that the backfill will take four hours on your data, or that the column you are dropping is read by a report nobody has looked at since 2024. Live-schema context narrows that gap — a read-only connection lets the rules see the real table sizes and constraints — but judgement stays with the team. The corpus removes the category of mistakes that need no judgement at all, so the judgement can go where it is needed.

If you are letting agents write migrations, the question is not whether to review them. It is whether the review is something you can rely on the same way every time. Start with npx bolvrk check on the last migration an agent wrote for you.

Check the last migration an agent wrote for you

npx bolvrk check runs the free rules locally — no account, no config, the same verdict every time.