D1 Migration

by jezweb64965d9d9fc7No license1K starsListed Oct 8, 2026Updated Oct 8, 2026Repository updated 3 days ago

Cloudflare D1 migration workflow: generate with Drizzle, inspect SQL for gotchas, apply to local and remote, fix stuck migrations, handle partial failures. Use when running migrations, fixing migration errors, or setting up D1 schemas.

AI-generated overview

Guided workflow for Cloudflare D1 database migrations with Drizzle ORM, covering generation, inspection, application and repair.

What it does
This skill provides a step-by-step procedure for running Cloudflare D1 migrations generated by Drizzle ORM. It explains how to generate migration SQL, inspect it for destructive table-recreation patterns, apply migrations to both local and remote databases, and verify results with PRAGMA queries. It also covers diagnosing and manually recording stuck or partially applied migrations, batching bulk inserts to avoid D1 parameter limits, and setting up a D1 database for a new project.
When to use it
Use it when running D1 migrations, when a migration errors or appears stuck, or when setting up D1 schemas for a new project. It is also relevant when generated Drizzle SQL needs review before being applied.
Requirements
Requires a Cloudflare D1 project with Drizzle ORM, the wrangler CLI, and package scripts such as db:generate and db:migrate. Network access and Cloudflare credentials are needed for remote migrations. Ships no scripts; it is instructions only.

D1 Migration Workflow

Guided workflow for Cloudflare D1 database migrations using Drizzle ORM.

Standard Migration Flow

1. Generate Migration

bash
pnpm db:generate

This creates a new .sql file in drizzle/ (or your configured migrations directory).

2. Inspect the SQL (CRITICAL)

Always read the generated SQL before applying. Drizzle sometimes generates destructive migrations for simple schema changes.

Red Flag: Table Recreation

If you see this pattern, the migration will likely fail:

sql
CREATE TABLE `my_table_new` (...);INSERT INTO `my_table_new` SELECT ..., `new_column`, ... FROM `my_table`;--                                      ^^^ This column doesn't exist in old table!DROP TABLE `my_table`;ALTER TABLE `my_table_new` RENAME TO `my_table`;

Cause: Changing a column's default value in Drizzle schema triggers full table recreation. The INSERT SELECT references the new column from the old table.

Fix: If you're only adding new columns (no type/constraint changes on existing columns), simplify to:

sql
ALTER TABLE `my_table` ADD COLUMN `new_column` TEXT DEFAULT 'value';

Edit the .sql file directly before applying.

3. Apply to Local

bash
pnpm db:migrate:local# or: npx wrangler d1 migrations apply DB_NAME --local

4. Apply to Remote

bash
pnpm db:migrate:remote# or: npx wrangler d1 migrations apply DB_NAME --remote

Always apply to BOTH local and remote before testing. Local-only migrations cause confusing "works locally, breaks in production" issues.

5. Verify

bash
# Check localnpx wrangler d1 execute DB_NAME --local --command "PRAGMA table_info(my_table)"
# Check remotenpx wrangler d1 execute DB_NAME --remote --command "PRAGMA table_info(my_table)"

Fixing Stuck Migrations

When a migration partially applied (e.g. column was added but migration wasn't recorded), wrangler retries it and fails on the duplicate column.

Symptoms: pnpm db:migrate errors on a migration that looks like it should be done. PRAGMA table_info shows the column exists.

Diagnosis

bash
# 1. Verify the column/table existsnpx wrangler d1 execute DB_NAME --remote \  --command "PRAGMA table_info(my_table)"
# 2. Check what migrations are recordednpx wrangler d1 execute DB_NAME --remote \  --command "SELECT * FROM d1_migrations ORDER BY id"

Fix

bash
# 3. Manually record the stuck migrationnpx wrangler d1 execute DB_NAME --remote \  --command "INSERT INTO d1_migrations (name, applied_at) VALUES ('0013_my_migration.sql', datetime('now'))"
# 4. Run remaining migrations normallypnpm db:migrate

Prevention

  • CREATE TABLE IF NOT EXISTS — safe to re-run
  • ALTER TABLE ADD COLUMN — SQLite has no IF NOT EXISTS variant; check column existence first or use try/catch in application code
  • Always inspect generated SQL before applying (Step 2 above)

Bulk Insert Batching

D1's parameter limit causes silent failures with large multi-row INSERTs. Batch into chunks:

typescript
const BATCH_SIZE = 10;for (let i = 0; i < allRows.length; i += BATCH_SIZE) {  const batch = allRows.slice(i, i + BATCH_SIZE);  await db.insert(myTable).values(batch);}

Why: D1 fails when rows x columns exceeds ~100-150 parameters.

Column Naming

ContextConventionExample
Drizzle schemacamelCasecaseNumber: text('case_number')
Raw SQL queriessnake_caseUPDATE cases SET case_number = ?
API responsesMatch SQL aliasesSELECT case_number FROM cases

New Project Setup

When creating a D1 database for a new project, follow this order:

  1. Deploy Worker first — npm run build && npx wrangler deploy
  2. Create D1 database — npx wrangler d1 create project-name-db
  3. Copy database_id to wrangler.jsonc d1_databases binding
  4. Redeploy — npx wrangler deploy
  5. Run migrations — apply to both local and remote

Source and attribution

Source:jezweb/claude-skillsinarchive/plugins/cloudflare/skills/d1-migrationat commit64965d9

License: No license

Content belongs to its original authors. SourceWeft indexes it from a public repository.

Report or request removal