Files
cfw-autumn/scripts/db
2026-05-20 10:29:01 +01:00
..
2026-05-20 10:29:01 +01:00
2026-05-20 10:29:01 +01:00
2026-05-18 20:48:51 +01:00
2026-04-27 14:01:26 +01:00
2026-05-18 20:48:51 +01:00
2026-02-05 17:31:55 -08:00

bun db — Drizzle Migration CLI

Unified entrypoint for everything migration-related. Subcommands route through scripts/db/index.ts, which auto-wraps DB-touching subcommands in infisical run so DATABASE_URL is injected from the right secret environment.

Surface

bun db help                                   # print usage
bun db generate                               # write a new migration .sql from schema changes
bun db migrate       [--env=dev|staging|prod] # apply pending migrations to the target DB
bun db migrate:dry   [--env=dev|staging|prod] # preview pending SQL + safety checks, don't apply
bun db mark-applied  [--env=dev|staging|prod] # seed drizzle.__drizzle_migrations on an existing DB
bun db rebase                                 # auto-resolve a local migration that collided with origin/dev

migrate and migrate:dry also run a safety check that refuses to apply any pending migration containing CREATE INDEX, DROP INDEX, or REINDEX without CONCURRENTLY. Those DDL statements take an ACCESS EXCLUSIVE lock and can block reads/writes on busy tables. To get through the check: either rewrite the SQL with CONCURRENTLY and apply manually + mark-applied, or add .concurrently() to the index in your schema and regenerate.

--env defaults to dev. generate and rebase never touch a DB and don't take --env.


Day-to-day workflow

# 1. Make a schema change in shared/db/**.ts or shared/models/**.ts
# 2. Generate the migration
bun db generate

# 3. Review the new shared/drizzle/{N}_*.sql and updated meta/.
#    Commit both with the code change in your PR.

# 4. Apply locally
bun db migrate

If your branch's migration collides with one that landed on origin/dev first (CI will tell you via duplicate-idx errors), pull and run:

git pull origin dev
bun db rebase     # auto-renumbers your migration, regenerates against the new baseline

bun db rebase is idempotent and bails out cleanly if drizzle-kit would need interactive input (e.g. column rename detection) — see the "Edge cases" section below.


Bootstrapping a new DB

bun db migrate won't run against a DB that has the tables but no drizzle.__drizzle_migrations tracking row — it'd try to CREATE TABLE on tables that already exist and fail. Seed the tracking table first:

bun db mark-applied                  # local dev DB
bun db mark-applied --env=staging    # if/when staging is wired up — see below
bun db mark-applied --env=prod       # if/when prod is wired up — see below

Idempotent. Re-running is safe.


Status: staging & prod are NOT wired into the CLI yet

Right now, staging and prod migrations are still applied manually (paste the .sql into TablePlus per env). The --env=staging|prod plumbing exists in the code and works, but we haven't:

  1. Run bun db mark-applied --env=staging / --env=prod to seed the tracking table on those DBs.
  2. Switched the deploy pipeline to call bun db migrate --env=… automatically.

Until both happen, do not run bun db migrate --env=staging or --env=prod — without the mark-applied step, it will try to re-create existing tables and fail. (Failing safely, but failing.)


Upgrade path: moving staging / prod into the CLI

When you're ready to flip from "TablePlus paste" to automated migrations on a shared env, the sequence is:

One-time per env

  1. Confirm the env's secret name in infisical. The CLI assumes:

    • dev → infisical env dev, ENV_FILE=.env
    • staging → infisical env staging, ENV_FILE=.env.staging
    • prod → infisical env prod, ENV_FILE=.env.prod

    This mapping lives in scripts/db/helpers/env.ts. If your infisical env names diverge, update there.

  2. Confirm DATABASE_URL in that infisical env points at the right DB. A wrong URL here means migrations apply to the wrong DB. Run:

    bun db mark-applied --env=<env>
    

    The first log line prints the host parsed from DATABASE_URL (e.g. host=staging-pg.foo.com:5432) — verify it before letting the script run further. Hit Ctrl-C if it's wrong; mark-applied does its writes after the log line.

  3. Run mark-applied. Same command as step 2. It seeds drizzle.__drizzle_migrations with rows for every migration currently in shared/drizzle/meta/_journal.json, so db migrate will skip them.

  4. Verify migrate is a clean no-op. This proves the seeding worked:

    bun db migrate --env=<env>
    

    Expected: drizzle-kit reports "applied successfully" with no DDL run, and drizzle.__drizzle_migrations has the same row count as before.

Ongoing — moving the apply step from TablePlus to the CLI / CI

Once the bootstrap above is done for an env, you have two ways to apply new migrations:

  • Manual command, same shape as dev:

    bun db migrate --env=staging
    bun db migrate --env=prod
    
  • CI-based — call the same command from a deploy step. Two things to wire up in the pipeline:

    1. The CI runner needs infisical installed and authenticated with credentials that can read the relevant env. Existing workflows that use infisical run --env=… are the precedent; copy that auth setup.
    2. Add a step like:
      - name: Apply DB migrations
        run: bun db migrate --env=prod
      
      after the build step and before traffic is shifted to the new revision. If the migration fails, the deploy should halt — bun db migrate exits non-zero on any DDL failure.

    Recommendation: start with staging-only CI migration first, run it for a week or two, and only then enable prod. Prod migration failures during a deploy are high-blast-radius and you want to be confident the pipeline works before you bet a deploy on it.


File layout

scripts/db/
├── README.md              # this file
├── index.ts               # subcommand dispatch
├── commands/
│   ├── help.ts
│   ├── generate.ts        # passthrough to `bun -F @autumn/shared db:generate`
│   ├── migrate.ts         # passthrough to `bun -F @autumn/shared db:migrate`
│   ├── markApplied.ts     # seeds drizzle.__drizzle_migrations
│   └── rebase.ts          # auto-resolves duplicate-idx conflicts
├── helpers/
│   ├── env.ts             # --env parsing + infisical wrap + DATABASE_URL host extraction
│   ├── paths.ts           # canonical paths to shared/drizzle/ and meta/
│   └── spawn.ts           # thin child_process.spawn wrapper
└── pull.ts                # unrelated — customer data pull (legacy)

shared/package.json still owns the implementation of db:generate and db:migrate (which are what the CLI shells out to under the hood). The unified bun db interface lives at the repo root.


Edge cases

bun db rebase bailed with "drizzle-kit needs interactive input"

This happens when the regeneration would prompt for a rename decision (column renamed, enum value reordered, etc.). The script restored your original files from a temp backup and exited non-zero. Recovery:

  1. bun db generate directly in a terminal — answer the prompts.
  2. Drizzle will produce a new migration. If it has a duplicate idx with origin/dev, manually rename the .sql file and update the idx + tag in meta/_journal.json.
  3. Re-run bun db rebase to confirm the journal is now clean.

This is rare. ~95% of migrations (ADD COLUMN, DROP COLUMN, CREATE INDEX) auto-resolve cleanly.

CI says "migration idx N already exists"

Someone else's PR with the same migration idx merged first. Run bun db rebase on your branch, push, re-run CI.

"I deleted shared/drizzle/ by accident"

Pull from origin/dev to get it back. Your in-flight schema changes in TS are still intact; run bun db generate and it'll regenerate the migration.