Skip to content

Migrations

Migrations without an ORM: commit .sql files to a directory and wire it into the database resource with migrationsDir. Each deploy applies the pending files — there is nothing else to run.

-- migrations/0001_init.sql
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE
);
-- migrations/0002_posts.sql
CREATE TABLE posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id),
title TEXT NOT NULL
);
const db = yield* Cloudflare.D1.Database("app-db", {
migrationsDir: "./migrations",
});

The same prop exists on Neon.Branch and Planetscale.PostgresBranch.

  • The directory is scanned recursively for .sql files.
  • Files are ordered by numeric prefix (0001_init.sql, 0002_posts.sql), then by name.
  • Each file is content-hashed and applied files are recorded, so a deploy only runs what’s new — an unchanged directory is a noop.
  • Application is ordered and stops on the first failure; the failed file is retried on the next deploy.

Each database documents its own mechanics — tracking table, transactionality, and quirks:

Database Mechanics
Cloudflare D1 D1 migrations — wrangler-compatible tracking table, migrationsTable for drizzle compatibility
Neon Neon migrations — applied transactionally on the branch
PlanetScale PlanetScale migrations — the same contract on Postgres and MySQL branches

Generated migrations target the same contract

Section titled “Generated migrations target the same contract”

Drizzle.Schema emits into the same shape: its out output is a migrations directory, so migrationsDir: schema.out orders generation before application in one deploy. Any tool that writes ordered .sql files into a directory already works.