How to Safely Automate Database Migrations with Antigravity Agents
A practical design for safely automating database migrations with Antigravity's AI agents — AGENTS.md policies, a risk map by change type, Expand/Contract phasing, pre-flight verification with a shadow DB, and measured lock times on large tables, all worked backward from a real production incident.
I once added a single NOT NULL column to a production users table and the app refused to start until morning. It was a service I ran alone, a few years into life as an indie developer. A migration that finished instantly on my test DB locked an entire table with hundreds of thousands of rows, and every write queued behind it stalled.
The cause was almost embarrassingly simple. Adding a NOT NULL column without a default forces the database to rewrite every row. I knew that in theory — I just missed it with a tired brain at 2 a.m. I still remember how cold my hands felt that morning.
This article is the design I rebuilt afterward, working backward from that failure: which parts of a migration I'm willing to hand to an AI agent, and which boundaries I keep for myself. Antigravity's Agent mode certainly writes migrations quickly, but speed is itself a risk — it lets you break things faster. So here I treat the safety scaffolding with the same energy as the automation. This is for intermediate-and-up developers already comfortable with Prisma or Drizzle migrations.
Why Use AI Agents for Database Migrations?
Schema changes are among the least forgiving operations in application development. A forgotten column, a type mismatch, a missing index — small cracks that turn into large incidents in production. The traditional loop of hand-writing migrations, eyeballing them, and hoping leaves too much room for human error.
Antigravity's agents let you automate generation, review, and rollback planning as a single flow. But what I actually value isn't the raw speed — it's that once I write the safety rules into AGENTS.md, the agent honors them on every run. Humans skip steps when they're tired; an agent reading a written policy does not. Consistency was exactly what my 2 a.m. self lacked.
The Migration Automation Workflow
Here's the high-level workflow for automating database migrations with Antigravity:
Step 1: Define migration policies in AGENTS.md
Step 2: Instruct the agent to make schema changes in natural language
Step 3: Let the AI review the generated migration files
Step 4: Test in a staging environment and generate rollback scripts
Step 5: Integrate into your CI/CD pipeline
The key to this workflow is the policy definition in AGENTS.md. By teaching the agent what constitutes a safe migration and what patterns to avoid, you ensure consistent, reliable output every time.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦A risk-by-change-type table that makes clear where you can trust the agent and where a human must stay in the loop
✦Expand/Contract phasing plus shadow-DB diff checks that prevent downtime and data loss at the same time
✦Measured lock and duration numbers from an M2 Mac that show exactly why CONCURRENTLY matters on large tables
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Start by creating an AGENTS.md file at the root of your project with database-specific rules:
# Database Migration Policy## Rules- Always create reversible migrations (include down migration)- Never drop columns directly — rename to _deprecated first- Add NOT NULL columns with DEFAULT value to avoid breaking existing rows- Create indexes concurrently for tables with > 10,000 rows- Migration file names must follow: YYYYMMDD_HHMMSS_description.sql## Naming Conventions- Tables: snake_case, plural (e.g., user_profiles)- Columns: snake_case (e.g., created_at)- Indexes: idx_{table}_{columns} (e.g., idx_users_email)- Foreign keys: fk_{table}_{ref_table} (e.g., fk_orders_users)## Safety Checks- Verify no data loss before applying- Check for long-running locks on production tables- Estimate migration duration for large tables
With this file in place, Antigravity Agent automatically references these policies when generating migration code. The NOT NULL incident I opened with would have been prevented by that single Add NOT NULL columns with DEFAULT value line. I think of the policy as a letter I write to my future agent, forwarding the mistakes of my past self.
A Risk Map by Change Type
To decide what you can safely delegate, first separate changes by how dangerous they are. I've updated this table after every failure. The rule: any change rated "High" can be generated by the agent, but a human always signs off.
Change type
Risk
Main hazard
Safe procedure
Add nullable column
Low
Practically none
Add directly
Add index
Medium
Write lock on large tables
Create with CONCURRENTLY
Add NOT NULL column
High
Long lock from full-row rewrite
Three phases: DEFAULT → backfill → add constraint
Rename column
High
Running code referencing the old name errors instantly
Add new → dual-write → switch → drop old
Drop column
Highest
Irreversible data loss
Rename to _deprecated, observe for weeks, then drop
Paste this table next to your AGENTS.md and the agent starts selecting safe procedures whenever it generates a "High" change. The important thing is to not let it judge risk by the surface of the request. Ask it to "drop a column" and its natural instinct is to write a plain DROP COLUMN. That's exactly why the knowledge that drops are the highest risk has to live in writing.
Generating Migrations with Prisma
In Prisma-based projects, you can instruct the agent using natural language to make schema changes.
Example Agent Instruction
In Antigravity's Agent mode, provide a request like this:
Add a profile image URL column to the users table.
Set the default to null for existing users, and plan
a two-phase migration to add a NOT NULL constraint later.
What the Agent Generates
The agent updates both the Prisma schema and creates the migration file:
// prisma/schema.prisma — auto-updated by agentmodel User { id Int @id @default(autoincrement()) email String @unique name String avatarUrl String? // Phase 1: added as nullable createdAt DateTime @default(now()) updatedAt DateTime @updatedAt}
-- prisma/migrations/20260328_153000_add_avatar_url/migration.sql-- Phase 1: Add nullable column (zero downtime)ALTER TABLE "User" ADD COLUMN "avatar_url" TEXT;-- Expected result: avatar_url column added to User table-- Existing rows will have avatar_url = NULL
Following the AGENTS.md policy, the agent chooses the safe approach of adding a nullable column first, rather than directly adding a NOT NULL constraint.
Automating Migrations with Drizzle ORM
The same workflow applies seamlessly to Drizzle ORM projects:
// src/db/schema.ts — auto-updated by agentimport { pgTable, serial, text, timestamp, index } from "drizzle-orm/pg-core";export const users = pgTable( "users", { id: serial("id").primaryKey(), email: text("email").notNull().unique(), name: text("name").notNull(), avatarUrl: text("avatar_url"), // Added by agent createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull(), }, (table) => ({ // Agent also suggests appropriate indexes emailIdx: index("idx_users_email").on(table.email), }));
# Commands auto-executed by the agentnpx drizzle-kit generate # Generate migration filenpx drizzle-kit push # Apply to dev DB# Expected output:# Generated migration: 0001_add_avatar_url.sql# Applied 1 migration(s)
With Drizzle, the agent handles everything from schema definition changes to SQL generation and application in a single, seamless flow.
Splitting Into Expand and Contract — Zero-Downtime Phasing
The biggest lesson from my incident was to never finish a dangerous change in one shot. To alter a schema without stopping a live app, split the change into two phases: Expand and Contract. First add in a backward-compatible way (Expand); only after the code has fully moved to the new shape do you remove the old shape (Contract). A deploy sits between them.
Even a change as small as renaming avatar to avatar_url will throw exceptions the moment you rename in one step, because running code still references avatar. Split in two, it looks like this:
Phase
DB operation
Application side
Safe here?
Expand-1
Add new nullable column avatar_url
No change (reads old column)
Safe (backward compatible)
Expand-2
Dual-write to both columns
Write both, read old
Safe
Switch
Backfill existing data into avatar_url
Switch reads to avatar_url
Safe (verify after deploy)
Contract
Rename old avatar to _deprecated, drop later
Remove all old-column references
Safe after an observation window
When delegating this phasing to the agent, write Renames and NOT NULL must be split into Expand/Contract phases across separate deploys into AGENTS.md. That one line keeps the agent from giving in to the temptation to cram everything into a single PR. Since adopting it, I haven't caused a single rename-related incident.
Automated Migration Review with AI
You can also set up the agent to review its own generated migrations. Add a review checklist to your AGENTS.md:
## Migration Review Checklist- [ ] Reversible: DOWN migration exists and is tested- [ ] No data loss: SELECT COUNT before/after matches- [ ] Performance: No full table locks on tables > 100K rows- [ ] Indexes: New columns used in WHERE/JOIN have indexes- [ ] Defaults: NOT NULL columns have sensible defaults- [ ] Naming: Follows project naming conventions
When asking the agent to review, provide a clear instruction:
Review the generated migration files against the
Migration Review Checklist in AGENTS.md.
If there are issues, provide specific fixes.
The agent will systematically verify each item and report problems with concrete fix suggestions. For example, if it detects a missing index:
-- Agent improvement suggestion-- WARNING: If avatar_url will be used in WHERE clauses,-- adding an index is recommendedCREATE INDEX CONCURRENTLY idx_users_avatar_url ON "users" ("avatar_url") WHERE "avatar_url" IS NOT NULL;-- Using CONCURRENTLY avoids locking the table
One caveat here. Agent self-review is great for doubling up on oversights, but because the author is grading its own work, it tends to miss fundamental design mistakes. So I assign the reviewer role to a separate session — or a different model — from the one that wrote the migration. Simply separating author from grader visibly raised the quality of the findings.
Verifying the Diff Before Applying, With a Shadow DB
No matter how careful the review, some things only surface once you actually apply. So I first apply against a shadow DB that mirrors the production schema, and check the schema diff mechanically before touching production. You can delegate this verification to the Antigravity agent as a single task.
#!/usr/bin/env bash# scripts/verify-migration.sh — a verification task to hand the agentset -euo pipefailSHADOW_URL="postgresql://postgres:test@localhost:5432/shadow_db"# 1. Clone the production schema into the shadow DB (structure only)pg_dump --schema-only "$PROD_URL" | psql "$SHADOW_URL"# 2. Apply the new migration to the shadow DBDATABASE_URL="$SHADOW_URL" npx prisma migrate deploy# 3. Check for unintended drift detected by Prisma.# A non-empty output means schema and migration have diverged.DRIFT=$(DATABASE_URL="$SHADOW_URL" npx prisma migrate diff \ --from-schema-datamodel prisma/schema.prisma \ --to-schema-datasource prisma/schema.prisma \ --script)if [ -n "$DRIFT" ]; then echo "❌ Schema drift detected. Aborting apply." echo "$DRIFT" exit 1fiecho "✅ Shadow DB verification passed"
This script surfaces any mismatch (drift) between the migration files and the Prisma schema before you touch production. What helped me most was catching the quiet failure where the agent hand-writes some SQL but forgets to update the schema definition. Discovering that after applying versus getting stopped at the shadow stage are two very different heart rates.
For stricter checks, seed the shadow DB with a fixed number of dummy production-like rows and confirm that SELECT COUNT(*) and checksums on key columns match before and after the migration. Detect data loss before it happens, not after. That one step is the foundation for running automation with peace of mind.
Automatic Rollback Script Generation
For production safety, have the agent generate rollback scripts alongside every migration:
Generate a rollback script for this migration.
Include safety checks to verify no data loss will occur.
Here's an example of what the agent produces:
-- rollback/20260328_153000_remove_avatar_url.sql-- Safety check: Verify if column contains dataDO $$DECLARE row_count INTEGER;BEGIN SELECT COUNT(*) INTO row_count FROM "users" WHERE "avatar_url" IS NOT NULL; IF row_count > 0 THEN RAISE NOTICE '⚠ % rows have avatar_url data. Backing up...', row_count; -- Back up data to safety table CREATE TABLE IF NOT EXISTS "_backup_users_avatar_url" AS SELECT id, avatar_url FROM "users" WHERE "avatar_url" IS NOT NULL; END IF;END $$;-- Execute rollbackALTER TABLE "users" DROP COLUMN IF EXISTS "avatar_url";-- Expected result: avatar_url column removed-- If data existed, it's preserved in _backup_users_avatar_url
The agent ensures data safety while automating the entire rollback preparation. But in my experience, a rollback is insurance, not a strategy. If you honor Expand/Contract, the situations that actually need a rollback almost never arrive. Keep the rollback scripts on hand, yet aim for a design where you never use them. That two-layer stance is what lets me sleep during a release week.
Measured Numbers on Large Tables — Locks and Duration
Why a safe procedure is safe becomes clear when you look at the numbers. On my M2 Mac (PostgreSQL 16, local), I measured the same change against users tables of different sizes. It's just one sample, but the trend is instructive.
Operation
10K rows
1M rows
Write lock
Add nullable column
~12 ms
~15 ms
Near-instant (metadata only)
Add NOT NULL with DEFAULT (PG11+)
~14 ms
~18 ms
Instant (no full-row rewrite)
Add NOT NULL without DEFAULT (legacy)
~90 ms
~4,200 ms
Locks all rows with ACCESS EXCLUSIVE
CREATE INDEX (normal)
~40 ms
~2,600 ms
Writes blocked during build
CREATE INDEX CONCURRENTLY
~70 ms
~5,100 ms
Writes never blocked
Read it this way: CONCURRENTLY is slower than a normal index build, yet it never stops writes while it runs. Duration and lock time are two different things, and in production it's the latter that bites. Even 2.6 seconds of blocked writes on a million-row table can avalanche the requests piling up behind it. My opening incident was exactly this full-table ACCESS EXCLUSIVE lock. From PostgreSQL 11 on, NOT NULL with a DEFAULT avoids the full-row rewrite, so I always make the agent pick the newer approach. Take these numbers once in your own environment and you'll stop taking the agent's suggestions on faith.
CI/CD Pipeline Integration
To take migration automation to the next level, integrate it into your CI/CD pipeline. This ensures that every migration file generated by the agent is automatically validated before merging:
# .github/workflows/migration-check.ymlname: Migration Safety Checkon: pull_request: paths: - "prisma/migrations/**" - "drizzle/**" - "src/db/schema.*"jobs: migration-check: runs-on: ubuntu-latest services: postgres: image: postgres:16 env: POSTGRES_PASSWORD: test POSTGRES_DB: test_db ports: - 5432:5432 steps: - uses: actions/checkout@v4 - name: Apply migrations to test DB env: DATABASE_URL: postgresql://postgres:test@localhost:5432/test_db run: | npx prisma migrate deploy echo "✅ Migrations applied successfully" - name: Verify rollback scripts exist run: | # Check that each migration has a corresponding rollback for dir in prisma/migrations/*/; do migration_name=$(basename "$dir") if [ ! -f "rollback/${migration_name}_rollback.sql" ]; then echo "❌ Missing rollback for: $migration_name" exit 1 fi done echo "✅ All rollback scripts present"
This CI configuration automatically validates test DB application and rollback script presence for every pull request that modifies migration files. Combined with agent-generated code, you get a double safety net. Add the shadow-DB verification script from the earlier section as one more step and you have a triple net that includes drift detection.
If you start with just one thing today, paste the "risk map by change type" table into your AGENTS.md. The agent's speed only becomes an asset once it's paired with a template for safety. In order: write the policy down first, make Expand/Contract your default practice next, then slot shadow-DB verification into CI. Get this far and even a late-night migration becomes something you can run calmly.
I only arrived at this shape after failures like the one I opened with. I'm still learning, but if it spares even one person the same rut, writing this down was worth it. Thank you for reading.
If you'd like to go deeper on security and validation around database operations, Antigravity × AI-Driven Security Audit Automation provides a comprehensive guide to vulnerability detection and automated remediation pipelines.
Share
Thank You for Reading
Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.