Database work with Codex: schemas, migrations, and queries you can prove
PLCodex handles database work on a Drizzle and PostgreSQL stack as a pipeline: it designs the schema, generates forward-only migrations with drizzle-kit, writes queries against the indexes that exist, and proves every change on a seeded database in a worktree or a Codex cloud environment. Humans review the migration SQL and the evidence, not every generated line.
Your product manager wants a reporting dashboard that aggregates orders, revenue, and refunds per user by day, week, and month. The orders table already has tens of millions of rows, the current queries are slow, and one blocking CREATE INDEX during business hours would take checkout down. This page is for the developer who owns that change in Codex, and for the tech lead who has to decide what “proven safe” means before it reaches production.
What you’ll walk away with from Codex database work
Section titled “What you’ll walk away with from Codex database work”- An
AGENTS.mdblock that stops Codex from editing applied migrations or touching anything but a local or test database. - Five copy-paste prompts: schema design, migration, queries with tests, a cloud proof run, and a weekly query audit.
- A Codex cloud environment setup script that gives each task its own PostgreSQL.
- A read-only PostgreSQL MCP setup so Codex can read real query plans.
- A verification stack (migration lint, apply-from-zero, differential query tests, and plan assertions) and a clear line on what a human still reads.
Which Codex surface fits each database task?
Section titled “Which Codex surface fits each database task?”Database work splits cleanly across Codex’s surfaces. Pick by what the task has to reach.
| Task | Surface | Why |
|---|---|---|
| Schema design, iteration | CLI or IDE extension, interactive | You answer design questions as they come up |
| Migration and query code | CLI with codex --worktree | The branch and generated SQL stay out of your main checkout. The local database is shared, so give each worktree its own database (createdb app_<branch> and point DATABASE_URL at it) |
| Proof on production-sized data | Codex cloud task (codex cloud exec) | A seeded database runs in a container, and a best-of-N run (--attempts N) costs no local time |
| Real query plans | CLI plus a read-only PostgreSQL MCP server | Codex reads EXPLAIN output from staging instead of guessing |
| Weekly regression audit | Desktop app automation, or codex exec on a schedule | Runs without you and reports only what changed |
The CLI flags on this page are checked against codex 0.157.1 and drizzle-kit 0.31.11. The Codex worktrees guide covers the worktree mechanics, and the cross-tool database guide sets Codex beside Claude Code and Cursor.
Give Codex database rules in AGENTS.md
Section titled “Give Codex database rules in AGENTS.md”Codex reads AGENTS.md before it does any work, so database rules belong there rather than in every prompt. Add this block to the repository root:
## Database rules- Stack: PostgreSQL 16, Drizzle ORM. Schema in src/lib/db/schema.ts, migrations in drizzle/.- Generate migrations with `npx drizzle-kit generate --name <what_changed>`. Never hand-edit a migration that exists on main, and never edit drizzle/meta/ by hand.- Only connect to DATABASE_URL from the environment. Never connect to a host containing "prod" and never run a migration against anything but a local or test database.- Indexes on existing tables use CREATE INDEX CONCURRENTLY IF NOT EXISTS in scripts/concurrent/<index_name>.sql, never in drizzle/: drizzle-kit migrate runs every pending migration in one transaction, so CONCURRENTLY cannot go through it. Apply the file with `psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f <file>` after `npm run db:migrate`, then check pg_index.indisvalid for the new index.- Schema changes on existing tables follow expand/contract: add, backfill, switch reads, then drop in a later release. Never rename or drop a column in the same release that stops using it.- If `pg_isready` fails, start PostgreSQL with `service postgresql start`.- Before you finish: `npm run db:migrate`, then any new scripts/concurrent/ file with psql, `npm run db:lint`, `npm test -- tests/db`.npm run db:lint wraps the migration linter shown under verification. The AGENTS.md optimization guide covers how to keep the file short enough that rules like these are followed.
Design the schema before any migration exists
Section titled “Design the schema before any migration exists”Start in the CLI or IDE extension. Schema mistakes are the most expensive part of this work to undo, so the first prompt forbids generating anything that could be applied.
Push back on the proposal in the same thread: ask what happens to a summary row when an order is refunded a month later, and whether (user_id, day) needs to be unique. Answers to questions like these become the acceptance criteria for the query tests later.
Generate a migration that is safe on a large table
Section titled “Generate a migration that is safe on a large table”Start a worktree session so the migration lands on its own branch:
# Terminal, repo rootcodex --worktreeThree facts about drizzle-kit shape this prompt. It generates forward-only migrations: the 0.31.11 command list (generate, migrate, push, check, up, drop, and others) has no down or rollback command, and drop deletes a migration file rather than reverting a database. A generated CREATE INDEX is a plain, blocking one. And drizzle-kit migrate runs every pending migration in one transaction. So the rollback plan and the concurrent indexes are yours to ask for, and the concurrent indexes cannot live in drizzle/.
The concurrent index sits outside drizzle/ because two rules meet here. PostgreSQL refuses CREATE INDEX CONCURRENTLY inside a transaction block, and drizzle-kit migrate hands all pending migrations to the drizzle-orm PostgreSQL migrator, which applies them in a single transaction (checked in drizzle-orm 0.45.3 on 2026-09-26). A custom Drizzle migration (drizzle-kit generate --custom) runs inside that same transaction, so the index needs its own non-transactional psql step in the deploy, after db:migrate. A failed concurrent build leaves an INVALID index behind, and IF NOT EXISTS then skips it silently; that is why step 4 checks indisvalid rather than trusting a clean exit. Database migration patterns covers expand/contract and backfills in depth.
Write queries with tests that can fail
Section titled “Write queries with tests that can fail”A query that type-checks and returns rows proves little. The prompt below asks for two kinds of evidence: results checked against an independent SQL oracle, and plans checked for the index each query should use.
The differential test is the one that catches wrong answers: the oracle reads raw orders and knows nothing about the summary tables, so a double-counted refund or a time-zone shift shows up as a mismatch. Plan tests on a small fixture can be misleading, because the planner prefers sequential scans on tiny tables; that is why the prompt puts them in a separate file that runs only with PLAN_TESTS=1, so the local npm test -- tests/db gate skips them and the cloud proof run below turns them on against the production-sized dataset.
Prove the change on production-sized data in a Codex cloud task
Section titled “Prove the change on production-sized data in a Codex cloud task”Your laptop database has a few thousand rows. The questions that matter (how long the concurrent index takes, whether the planner picks it at 50 million rows) need a bigger dataset. A Codex cloud environment gives each task its own container, so each task can own a disposable PostgreSQL.
Configure the environment’s setup script in the Codex web settings under Environments, and name the environment db-proof: codex cloud exec --env accepts the environment’s name or its ID (checked in codex 0.157.1), and the proof command below uses the name.
#!/usr/bin/env bash# Codex cloud environment setup scriptset -euo pipefail
apt-get update && apt-get install -y --no-install-recommends postgresql postgresql-contribnpm ci
service postgresql startsu postgres -c "createuser --superuser root" # peer auth for the container's root user, no passwordcreatedb app_test
# Prove the database is reachable. Migrations run in the task, so it can time thempg_isreadySet DATABASE_URL to postgresql:///app_test?host=/var/run/postgresql as an environment variable in the same settings page, not as a secret, so the agent phase can read it however secrets are scoped (it holds no password anyway). Do not count on the server started by the setup script still running when the agent starts; the AGENTS.md rule above tells the agent how to start it.
Push your branch, then submit the proof task:
Find the task ID with codex cloud list --env db-proof, read the task with codex cloud status TASK_ID, then run codex cloud diff TASK_ID if it changed anything. The report is the artifact you attach to the pull request. A 50-million-row seed takes setup time on every run, so keep the row count to what reveals the plan you care about; the cloud environments guide covers caching and maintenance scripts that keep repeated runs fast.
Let Codex read real query plans through MCP
Section titled “Let Codex read real query plans through MCP”Seeded data answers “is this safe”. A staging database with real traffic answers “which queries are slow today”. Postgres MCP Pro (PyPI postgres-mcp; last PyPI release 0.3.0, 2025-05-16, checked 2026-09-26) exposes tools such as explain_query, get_top_queries, and analyze_query_indexes. Run it in restricted mode, which limits it to read-only transactions with an execution-time limit, and connect it as a read-only role:
# ~/.codex/config.toml. env_vars forwards DATABASE_URI from your shell; nothing secret is stored here[mcp_servers.postgres]command = "uvx"args = ["postgres-mcp", "--access-mode=restricted"]env_vars = ["DATABASE_URI"]enabled_tools = ["list_schemas", "get_object_details", "explain_query", "get_top_queries", "analyze_query_indexes"]Export DATABASE_URI with the staging read-only connection string before you start codex, and confirm the entry with codex mcp get postgres. The older @modelcontextprotocol/server-postgres package is deprecated on npm; do not copy snippets that use it. The database MCP guide has the SQL for the read-only role and the extensions the index tools need (pg_stat_statements, hypopg). The guide also covers alternatives such as Supabase and DBHub. If a package with no release since May 2025 is a problem for your team, DBHub is still actively released. Codex MCP setup covers approval modes and keeping secrets out of config.toml.
Audit query performance every week
Section titled “Audit query performance every week”Once the dashboard ships, regressions arrive through ordinary feature work. Schedule the audit as a thread automation in the Codex desktop app, so each run remembers the previous one, with a read-only sandbox and the MCP server above. The Codex automations guide covers setup, sandboxing, and running the same prompt with codex exec on an always-on host.
How do you verify Codex database work without reading every line?
Section titled “How do you verify Codex database work without reading every line?”Database changes are one of the few change classes where a person still reads the code itself, so the aim is to shrink what that person reads to the migration SQL and a page of evidence. The evidence-over-diffs approach explains the split.
-
Lint the migration. Squawk (
squawk-clion npm) flags blocking index builds, unsafe column changes, and other lock hazards in PostgreSQL migrations. Wire it up asdb:lint, for examplenpx --package squawk-cli squawk drizzle/*.sql scripts/concurrent/*.sql, and run it in CI on every pull request that touchesdrizzle/orscripts/concurrent/. -
Apply from zero, twice. CI creates an empty database, runs every migration, runs them again (nothing may re-apply), applies the
scripts/concurrent/files withpsql, and runsnpx drizzle-kit checkto catch inconsistent migration metadata. -
Run the differential and plan tests. The query tests compare each Drizzle result with an independent SQL aggregate and assert the intended index. A failure here is a wrong answer or a lost index, not a style issue.
-
Attach the cloud proof report. The pull request carries the timings, the
indisvalidcheck, and the plans from the production-sized run. No report, no merge. -
A named human reads the SQL and signs off. The developer who ran the task reads every
.sqlfile and the rollback note, which are short next to the generated application code. For changes to tables above your size threshold, a second reviewer (the tech lead or a DBA) signs off on the lock analysis. Codex’s@codex reviewon the pull request is a useful extra pass, not the sign-off.
The application code around the queries (types, service wiring) goes through the same gates as any other Codex change: type check, lint, tests, and review, as described in Codex testing.
What breaks when Codex does database work?
Section titled “What breaks when Codex does database work?”| Symptom | Likely cause | Recovery |
|---|---|---|
| Deploy fails with “CREATE INDEX CONCURRENTLY cannot run inside a transaction block” | drizzle-kit migrate applies all pending migrations in one transaction | Remove the CONCURRENTLY statement from drizzle/, run it with psql outside the migrator as its own deploy step, drop any INVALID index left behind with DROP INDEX CONCURRENTLY, and retry |
drizzle-kit check fails or a migration re-applies | Codex edited an applied migration or drizzle/meta/ by hand | Revert those files to main, change schema.ts only, and regenerate; keep the AGENTS.md rule that forbids it |
| Plan tests pass locally, the dashboard is slow in production | The seed is small or uniformly distributed, so the planner chose differently | Seed with production-like skew, run ANALYZE, and keep plan assertions in the cloud proof run |
| Codex “fixes” every slow query with a new index | The prompt rewarded speed only | Require each index to cite the query it serves and its plan before and after, and ask which existing index it makes redundant |
| Dashboard totals are off for users outside UTC | Day boundaries computed in UTC | Pass the user’s time zone into every query, and keep the 23:30 UTC order in the differential fixture |
| Two worktree sessions fail with “relation already exists” or see each other’s test rows | Every worktree points at the same local DATABASE_URL, so parallel migrations and fixtures collide | Create one database per worktree (createdb app_<branch>) and set DATABASE_URL in that worktree’s environment |
| The cloud task reports “connection refused” | PostgreSQL started in setup is not running in the agent phase | Keep the pg_isready and service postgresql start rule in AGENTS.md, and check the environment variable holds the socket URL |
| The MCP server fails to start or shows no tools | uvx missing, DATABASE_URI not exported, or a misspelled name in enabled_tools | Run uvx postgres-mcp --help directly, export the variable before starting codex, and read the real tool names with /mcp verbose |
Where to go next with Codex database work
Section titled “Where to go next with Codex database work”- Set up the server once, properly: database MCP servers and securing MCP servers.
- Plan migrations that span releases: database migration patterns and SQL optimization patterns.
- Ship the change with the application: deployment automation with Codex.
- Profile what the audit finds: Codex performance optimization.
- Give every agent task its own database: ephemeral environments.
- The same workflow in the other tools: database work in Claude Code and schema design in Cursor.