Skip to content

Migrations

Migrations are versioned TypeScript files that build and evolve your D1 schema. Each file exports an up (apply) and down (revert) function that receive a Schema instance — no db argument, no raw SQL execution. The CLI tracks which migrations have run in a _migrations table.

The full migration DSL — every column type, constraint, foreign key, and modifier — lives in the Schema Builder reference. This page covers the day-to-day workflow; reach for that page when you need the exact method signatures.

Workflow at a glance

sh
bunx d1-eloquent make:migration create_users_table   # 1. scaffold
# …edit the generated file…
bunx d1-eloquent migrate                              # 2. apply pending
bunx d1-eloquent status                               # 3. inspect state
bunx d1-eloquent rollback                             # 4. revert last batch
bunx d1-eloquent fresh                                # 5. drop all + re-run

The binding name is read from wrangler.jsonc automatically and defaults to local D1 — no --db/--local flags needed.

1. Scaffold a migration

sh
bunx d1-eloquent make:migration create_users_table

This writes src/database/migrations/TIMESTAMP_create_users_table.ts with commented stubs. One migration per domain entity — keep them small and focused rather than monolithic. See the Generators reference for all make:* commands.

2. Write the schema

ts
// src/database/migrations/20260101000000_create_users_table.ts
import type { TMigration } from '@orphnet/d1-eloquent/cli'
import { Schema } from '@orphnet/d1-eloquent/cli'

const migration: TMigration = {
  name: '20260101000000_create_users_table',

  up: (schema: Schema) => {
    schema.createTable('users', (t) => {
      t.id()                              // TEXT PRIMARY KEY (UUID)
      t.text('name')
      t.text('email', { unique: true })
      t.boolean('is_admin').default(false)
      t.text('settings').nullable()       // JSON stored as TEXT
      t.timestamps()                      // created_at / updated_at
      t.softDeletes()                     // deleted_at (omit if not soft-deleting)
    })
  },

  down: (schema: Schema) => {
    schema.dropTable('users')
  },
}

export default migration

Foreign keys, composite keys, indexes, check constraints, and altering existing tables are all covered in the Schema Builder reference.

Keep down a true inverse

Whatever up creates, down should undo — so rollback and fresh stay clean. For a createTable, that's a matching dropTable; for an addColumn alter, drop the column.

3. Apply migrations

sh
bunx d1-eloquent migrate

Runs every pending migration in timestamp order and records each in _migrations:

Running: 20260101000000_create_users_table
✓ Applied 1 migration

See the migrate reference for remote application and dry-run options.

4. Roll back

sh
bunx d1-eloquent rollback

Reverts the most recent batch by calling each migration's down. See the rollback reference.

5. Fresh rebuild

sh
bunx d1-eloquent fresh        # drop all tables, re-run every migration
bunx d1-eloquent fresh --seed # …then run seeders

fresh drops tables in FK-safe topological order, then re-applies the full migration history — ideal for resetting local dev. See the fresh reference.

Generate migrations from your models

generate diffs each model against the migrations already on disk and writes a reconciling migration for you — no hand-writing ALTER TABLE. It's pure static analysis: it reads your model files and migration files and never touches a database. The emitted file is your review gate before migrate applies it.

sh
bunx d1-eloquent generate            # dry run — print the diff for every model
bunx d1-eloquent generate Post       # just one model
bunx d1-eloquent generate Post --write   # emit the migration file
bunx d1-eloquent generate Post --write --name=add_pinned_to_posts   # custom name

--name=<name> overrides the derived create_<table> / update_<table> name segment (the timestamp prefix is always kept). It only applies when exactly one migration file is emitted - a multi-table run keeps the derived per-table names.

The desired schema is derived from the model itself: its attribute-type fields (the TS type/interface passed to BaseModel<…>), refined by static casts (boolean → INTEGER, json/array → JSON, date → TEXT, …), plus the conventional created_at/updated_at (when timestamps) and deleted_at (when softDeletes). That's diffed against the columns your migrations already declare for the table.

ts
// You add two fields to the model…
interface PostAttrs {
  // …existing fields…
  pinned?: boolean          // optional → nullable; boolean cast → INTEGER
  subtitle: string | null
}
sh
bunx d1-eloquent generate Post --write
# ~ Post (alter "posts")
#     + pinned    INTEGER nullable
#     + subtitle  TEXT
# ✓ Wrote database/migrations/20260707201912_update_posts.ts
ts
// …and get a ready-to-run migration (up adds, down reverses):
up:   (schema) => schema.table("posts", (t) => {
  t.addInteger("pinned", { nullable: true })
  t.addText("subtitle")
}),
down: (schema) => schema.table("posts", (t) => {
  t.dropColumn("pinned")
  t.dropColumn("subtitle")
}),

A brand-new model (no migration for its table yet) generates a full createTable instead of an alter.

Review before you migrate

generate emits a file — it does not apply anything. Always read it first.

  • Dropped columns are destructive and are annotated with a ⚠️ comment. Removing a field from a model makes generate propose a dropColumn that deletes that column's data.
  • SQLite can't ALTER a column's type in place, so a storage-class change (e.g. TEXT → INTEGER) is reported as a warning, never auto-emitted — recreate the table manually if you need it.
  • Declared-column read-back honours prior alter migrations: columns added via t.addText()/addInteger()/… and removed via t.dropColumn() in a migration's up() are tracked, so generate round-trips its own output and won't re-propose an already-added column.

New migrations are written to wherever your existing migrations live (falling back to src/database/migrations for a greenfield project).

Next steps

Released under the MIT License.