d1-migration — Claude Code skill for Cloudflare D1 migrations
Walks Cloudflare D1 migrations through Drizzle: generate the SQL, read it for traps, apply to local and remote, unstick partial runs.
npx skills add jezweb/claude-skills --skill d1-migration -a claude-codeWhat d1-migration does
The skill lays out a migration order for D1. pnpm db:generate drops a new .sql file into drizzle/, and the next step is to actually read it. Drizzle sometimes turns a small schema change into a full table recreation: CREATE TABLE my_table_new, an INSERT INTO ... SELECT that pulls the new column from the old table, then DROP TABLE and RENAME. That insert fails, because the old table has no such column. When you are only adding columns, edit the file down to ALTER TABLE my_table ADD COLUMN before applying anything. Worth checking every generated file.
Applying happens twice: pnpm db:migrate:local and pnpm db:migrate:remote. Local-only runs are the usual source of "works here, breaks in production". Verification uses npx wrangler d1 execute DB_NAME --local --command "PRAGMA table_info(my_table)", then the same with --remote.
Stuck migrations get their own section. If a column landed but no row was written to d1_migrations, wrangler retries the migration and trips over the duplicate. The fix is a manual insert into d1_migrations, then a normal run for the rest. The skill also flags D1's parameter limit: large multi-row inserts fail quietly, so it batches them 10 rows at a time. New projects follow a different order — deploy the Worker, run npx wrangler d1 create, copy database_id into wrangler.jsonc, redeploy, then migrate.
What it helps with
- generate a SQL migration from a Drizzle schema
- catch table recreation in generated SQL before it runs
- apply a migration to both local and remote D1
- repair a migration that applied halfway and got stuck
- chunk a large insert around D1's parameter limit
Example prompts for the agent
- generate the migration and show me the SQL before applying it
- the migration is stuck: the column exists but d1_migrations has no row for it
- set up D1 for a new project and run migrations locally and remotely
How to install d1-migration
1Skills CLI (Vercel)
npx skills add jezweb/claude-skills --skill d1-migration -a claude-code2Manually: put the files in place
~/.claude/skills/d1-migration/SKILL.mdCopy the whole skill folder; for one project use .claude/skills/d1-migration/
1Skills CLI (Vercel)
npx skills add jezweb/claude-skills --skill d1-migration -a codex2Manually: put the files in place
.agents/skills/d1-migration/SKILL.mdFor all projects use ~/.agents/skills/
1Skills CLI (Vercel)
npx skills add jezweb/claude-skills --skill d1-migration -a cursor2Manually: put the files in place
.cursor/skills/d1-migration/SKILL.mdCursor also reads .agents/skills/ and .claude/skills/
1GitHub CLI
gh skill install jezweb/claude-skills d1-migration2Manually: put the files in place
.github/skills/d1-migration/SKILL.md1Skills CLI (Vercel)
npx skills add jezweb/claude-skills --skill d1-migration -a gemini-cli2Platform CLI
gemini skills install https://github.com/jezweb/claude-skills --path archive/plugins/cloudflare/skills/d1-migrationManually: put the files in place
.devin/skills/d1-migration/SKILL.mdThe legacy .windsurf/skills/ path still works
Skills CLI (Vercel)
npx skills add jezweb/claude-skills --skill d1-migration -a opencodeClaude.ai
Zip the folder and upload it in Settings → Capabilities → Skills
Step by step, with commands for each platform: How to install skills in Claude Code: five ways
Questions about d1-migration
How do I install d1-migration in Claude?
Run npx skills add jezweb/claude-skills --skill d1-migration -a claude-code. The skill lands in ~/.claude/skills/d1-migration/SKILL.md.
What does d1-migration do?
It guides Cloudflare D1 migrations with Drizzle ORM: generate, inspect the SQL, apply to local and remote, verify with wrangler d1 execute.
Why does a D1 migration get stuck?
The column was added but never recorded in d1_migrations, so wrangler retries and hits the duplicate. Insert the row into d1_migrations manually, then run the remaining migrations.
Source text
SKILL.md · MIT
---
name: d1-migration
description: "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."
compatibility: claude-code-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 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:
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
© jezweb · MIT · jezweb/claude-skills