{"slug": "why-ai-generated-migrations-need-a-different-gate-than-code-patches", "title": "Why AI-Generated Migrations Need a Different Gate Than Code Patches", "summary": "MonkeyCode proposes a separate pre-merge gate for AI-generated database migrations, distinct from code patch reviews, because migrations can make changes irreversible. The gate crash-tests up.sql and down.sql pairs against a throwaway Postgres schema to ensure the down migration restores the original schema, forcing explicit human approval for one-way doors.", "body_md": "Most patch reviews ask: does this change behave the way we intended? A migration review has to answer a harder question: what does this change make impossible to undo?\n\nA code change can usually be reverted by applying the old diff again. A migration that drops a column, truncates a table, or rewrites data may make the previous state unrecoverable even if the commit is reverted. That asymmetry is why schema changes deserve a separate pre-merge gate from ordinary AI-generated code.\n\nOne practical way to use MonkeyCode's free model access and free server option is not to generate final schema changes and push them, but to generate `up.sql`\n\nand `down.sql`\n\npairs and then crash-test them against a throwaway Postgres schema before a human reviewer spends time on them. Disclosure: This article was prepared as part of MonkeyCode's product outreach. I won't quote quota or hardware specifics because those change; check the current plan before building a pipeline around them.\n\nA typical AI patch gate checks that generated tests pass, that a diff is small, and that fixtures still match. That is useful for application logic, but it can pass a migration that has all three of these problems:\n\n`down.sql`\n\n.`down.sql`\n\nthat recreates the column but cannot restore the data.The result is often discovered too late: a developer approves a reasonable-looking `ALTER TABLE`\n\n, CI is green, and then production data disappears or the deploy stalls while the DDL waits on a lock.\n\nThe gate below checks three things before a human sees the migration:\n\n`DROP TABLE`\n\n, `TRUNCATE`\n\n, or unguarded `UPDATE`\n\n.`up.sql`\n\nagainst a throwaway database.`down.sql`\n\nand comparing the schema snapshot with the baseline.The third check is the important one. If the down migration cannot restore the original schema, then the up migration is proposing a one-way door, and a human should be forced to approve that explicitly.\n\nThe script below assumes you have local `psql`\n\nand `pg_dump`\n\ninstalled and can create databases on the server. Treat it as a runnable starting point, not a guarantee.\n\n``` bash\n#!/usr/bin/env python3\n\"\"\"\nmigration_probe.py: run a migration pair against a throwaway Postgres database.\n\nUsage:\n  python migration_probe.py \\\n    --uri postgresql://user:pass@localhost:5432/postgres \\\n    --dir migrations/2026_08_14_add_flags\n\"\"\"\nimport argparse\nimport os\nimport subprocess\nimport sys\nimport uuid\n\ndef sh(args, label=\"\"):\n    result = subprocess.run(args, capture_output=True, text=True)\n    if result.returncode != 0:\n        print(f\"[fail] {label}\\n{result.stderr}\")\n        sys.exit(result.returncode)\n    return result.stdout\n\ndef main():\n    parser = argparse.ArgumentParser()\n    parser.add_argument(\"--uri\", required=True)\n    parser.add_argument(\"--dir\", required=True)\n    args = parser.parse_args()\n\n    base = args.uri\n    db = f\"shadow_{uuid.uuid4().hex[:10]}\"\n\n    def schema(uri):\n        return sh(\n            [\"pg_dump\", \"--schema-only\", \"--no-owner\", uri],\n            \"schema snapshot\",\n        )\n\n    before = schema(base)\n\n    sh([\"psql\", base, \"-c\", f'CREATE DATABASE {db}'], \"create shadow db\")\n    db_uri = base.replace(\"/postgres\", f\"/{db}\")\n\n    try:\n        sh([\"psql\", db_uri, \"-f\", os.path.join(args.dir, \"up.sql\")], \"apply up\")\n        after = schema(db_uri)\n\n        sh([\"psql\", db_uri, \"-f\", os.path.join(args.dir, \"down.sql\")], \"apply down\")\n        rolled_back = schema(db_uri)\n    finally:\n        sh([\"psql\", base, \"-c\", f'DROP DATABASE IF EXISTS {db}'], \"drop shadow db\")\n\n    if after == before:\n        print(\"[warn] up migration produced no schema change\")\n\n    if rolled_back != before:\n        print(\"[fail] down migration did not restore original schema\")\n        diffs = subprocess.run(\n            [\"diff\", \"-\", \"-\"],\n            input=f\"{before}\\n{rolled_back}\",\n            capture_output=True,\n            text=True,\n        )\n        print(diffs.stdout)\n        sys.exit(1)\n\n    print(\"[ok] migration pair is schema-roundtrip clean\")\n\nif __name__ == \"__main__\":\n    main()\n```\n\nPair that with a simple destructive-keyword check that runs before the shadow apply:\n\n```\nDESTRUCTIVE = [\"DROP TABLE\", \"DROP COLUMN\", \"TRUNCATE\", \"DELETE FROM\"]\n\ndef blockers(path):\n    text = open(path, encoding=\"utf-8\").read().upper()\n    return [kw for kw in DESTRUCTIVE if kw in text]\n```\n\nIf the blocker list is not empty, the workflow should not reject automatically. It should switch the migration from normal review to **destructive-review mode**, requiring an explicit note about what data is lost and whether the down migration can recover it.\n\nThe distinction from a patch gate is the success condition. For code, the useful question is often: does the new behavior match specification? For a migration, the useful question is: can we return to the previous state?\n\nA schema round-trip test catches the common failure where `down.sql`\n\nrecreates a column but leaves out a default, loses a constraint, or reverses operations in the wrong order. It cannot prove the migration is safe, but it moves the obvious failures from production to a disposable server.\n\n`pg_dump --schema-only`\n\ncompares structure, not data. A down migration may restore a table but erase rows. Use a data fixture or a subset dump for critical changes.This is not a replacement for a human reviewer who understands the production schema, the retention policy, and the business reason for deleting data. Skip the automatic path entirely for migrations that touch high-risk tables, regulated data, or anything where deletion is unrecoverable by design.\n\nA sensible loop is to let a model generate the up-and-down pair, let the probe filter out the reversible failures, and then take the remaining destructive cases to a human with the full context. That makes AI-generated migrations useful without making them authoritative.", "url": "https://wpnews.pro/news/why-ai-generated-migrations-need-a-different-gate-than-code-patches", "canonical_source": "https://dev.to/github_7727/why-ai-generated-migrations-need-a-different-gate-than-code-patches-1m7", "published_at": "2026-08-14 00:55:37+00:00", "updated_at": "2026-08-14 01:45:33.131579+00:00", "lang": "en", "topics": ["ai-tools", "developer-tools", "ai-products"], "entities": ["MonkeyCode", "Postgres"], "alternates": {"html": "https://wpnews.pro/news/why-ai-generated-migrations-need-a-different-gate-than-code-patches", "markdown": "https://wpnews.pro/news/why-ai-generated-migrations-need-a-different-gate-than-code-patches.md", "text": "https://wpnews.pro/news/why-ai-generated-migrations-need-a-different-gate-than-code-patches.txt", "jsonld": "https://wpnews.pro/news/why-ai-generated-migrations-need-a-different-gate-than-code-patches.jsonld"}}