
D1 Migration
Run and recover Cloudflare D1 schema migrations generated by Drizzle without destructive surprises or stuck remote state.
Overview
D1 Migration is an agent skill for the Build phase that guides Cloudflare D1 schema migrations with Drizzle, including SQL inspection, apply steps, and recovery from stuck or partial failures.
Install
npx skills add https://github.com/jezweb/claude-skills --skill d1-migrationWhat is this skill?
- Standard flow: pnpm db:generate → inspect SQL → apply local then remote
- Red-flag detection for Drizzle table-recreation migrations that break INSERT SELECT
- Manual SQL simplification pattern (ALTER ADD COLUMN) when only adding columns
- Remote stuck-migration and partial-failure recovery steps
- Explicit compatibility note: Claude Code–oriented workflow commands
- Mandatory pre-apply SQL inspection step before any D1 apply
- Documents table-recreation red-flag pattern with CREATE TABLE `*_new`
Adoption & trust: 776 installs on skills.sh; 841 GitHub stars; 3/3 security scanners passed (skills.sh audits).
What problem does it solve?
You generated a Drizzle migration for D1 but do not know if the SQL will recreate tables destructively or how to fix a failed remote apply without corrupting production data.
Who is it for?
Indie builders shipping on Cloudflare Workers/Pages with Drizzle-managed D1 who want a checklist before every `db:generate` and `wrangler d1 migrations apply`.
Skip if: Teams not using Cloudflare D1 or Drizzle, or projects that only need one-off SQL in the dashboard with no versioned migration folder.
When should I use this skill?
Running migrations, fixing migration errors, or setting up D1 schemas with Drizzle on Cloudflare.
What do I get? / Deliverables
You get a reviewed migration file, successful local and remote applies when appropriate, and concrete fixes when Drizzle emits unsafe recreation SQL or migrations hang in Wrangler state.
- Reviewed and possibly hand-edited migration `.sql` in drizzle/
- Successful local and/or remote D1 migration apply
- Recovery steps executed for stuck or partial migration failures
Recommended Skills
Journey fit
Schema changes and migration SQL live in the backend/data layer during product construction, before production ship cadence. D1 tables, Drizzle generate/apply, and Worker bindings are core backend persistence work for Cloudflare stacks.
How it compares
Use instead of applying Drizzle output blindly or ad-hoc SQL edits without the table-recreation red-flag review this workflow mandates.
Common Questions / FAQ
Who is d1-migration for?
Solo and indie developers building on Cloudflare with D1 and Drizzle who need reliable migration generation, pre-apply SQL review, and troubleshooting when remote applies fail.
When should I use d1-migration?
Use it when running new migrations, setting up D1 schemas, or fixing migration errors after `pnpm db:generate`, local apply, or remote Wrangler apply on your Worker-backed database.
Is d1-migration safe to install?
It is procedural documentation for your agent; review the Security Audits panel on this Prism page and treat migration commands as destructive-capable—always back up and test locally first.
SKILL.md
READMESKILL.md - D1 Migration
# 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 local npx wrangler d1 execute DB_NAME --local --command "PRAGMA table_info(my_table)" # Check remote npx 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 exists npx wrangler d1 execute DB_NAME --remote \ --command "PRAGMA table_info(my_table)" # 2. Check what migrations are recorded npx wrangler d1 execute DB_NAME --remote \ --command "SELECT * FROM d1_migrations ORDER BY id" ``` ### Fix ```bash # 3. Manually record the stuck migration npx 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 normally pnpm 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 | Context | Convention | Example | |---------|-----------|---------| | Drizzle schema | camelCase | `caseNumber: text('case_number')` | | Raw SQL queries | snake_case | `UPDATE cases SET case_number = ?` | | API responses | Match SQL aliases | `SELECT case_number FROM cases` | ## New Project Setup When creating a D1 database for a new project, follow this order: