skills/drizzle-migrations/SKILL.md
Manage database schema with Drizzle ORM and SQLite migrations. Use when adding tables, modifying columns, creating indexes, or running migrations. Activates for database schema changes, migration generation, and Drizzle query patterns.
npx skillsauth add curiositech/windags-skills drizzle-migrationsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Manage database schema changes using Drizzle ORM with SQLite through decision-driven migration strategies.
Is this a production database?
├─ YES → Always use generate + migrate workflow
│ ├─ Breaking change (column removal, type change)?
│ │ ├─ YES → Multi-step migration with data preservation
│ │ └─ NO → Standard migration generation
│ └─ Performance impact (large table, complex index)?
│ ├─ YES → Plan maintenance window, test on replica
│ └─ NO → Standard migration during low traffic
└─ NO (development) → Can use push for speed
├─ Breaking existing local data?
│ ├─ YES → Backup first, consider fresh DB
│ └─ NO → Safe to push directly
└─ Collaborating with team?
├─ YES → Use migrations for consistency
└─ NO → Push is acceptable
| Data Type | Use | Drizzle Type | Index Recommended |
|-----------|-----|--------------|------------------|
| User ID | Primary/Foreign keys | text('id').primaryKey() | Auto on PK/FK |
| Email | Unique identifiers | text('email').unique() | YES |
| Timestamps | Created/updated times | text('created_at').default(sql\CURRENT_TIMESTAMP`)| If filtered | | JSON Data | Flexible schemas |text('data', { mode: 'json' })| NO (use computed) | | Booleans | Status flags |integer('active', { mode: 'boolean' })| If filtered | | Counters | Stats, quantities |integer('count').default(0)` | If aggregated |
Migration fails to apply?
├─ Schema conflict detected
│ ├─ Column exists → Check if type matches expected
│ ├─ Table exists → Verify schema matches or rename
│ └─ Index exists → Check if definition identical
├─ Foreign key violation
│ ├─ Referenced table missing → Create dependency first
│ ├─ Referenced column missing → Fix reference or add column
│ └─ Circular reference → Break into separate migrations
└─ Data type mismatch
├─ Incompatible conversion → Create transformation migration
├─ NULL constraint violation → Add default or migrate data
└─ Unique constraint violation → Clean duplicate data first
Symptoms: Using npm run db:push on production database
Detection: If environment is production AND using push command
Fix:
npm run db:generate/drizzle folderSymptoms: Query performance degrades after adding foreign keys Detection: Foreign key columns without corresponding indexes Fix:
// Add index for FK columns
export const checkIns = sqliteTable('check_ins', {
userId: text('user_id').notNull().references(() => users.id),
}, (table) => ({
userIdIdx: index('idx_checkins_user_id').on(table.userId), // Add this
}));
Symptoms: Data loss during column rename or type changes
Detection: Migration contains DROP COLUMN or incompatible type changes
Fix:
Symptoms: Tables with 15+ columns, deeply nested JSON in text fields Detection: Single table handling multiple concerns Fix:
Symptoms: Accidental bulk deletes or updates
Detection: DELETE or UPDATE statements without .where() clause
Fix:
// WRONG: Deletes all records
await db.delete(users);
// RIGHT: Targeted deletion
await db.delete(users).where(eq(users.id, userId));
Scenario: Add user preferences table to existing user system
Step 1: Analyze Impact
Step 2: Schema Definition
// src/db/schema.ts
export const userPreferences = sqliteTable('user_preferences', {
id: text('id').primaryKey(),
userId: text('user_id').notNull().references(() => users.id, {
onDelete: 'cascade', // Delete prefs when user deleted
}),
theme: text('theme', { enum: ['light', 'dark', 'auto'] }).default('auto'),
notifications: integer('notifications', { mode: 'boolean' }).default(true),
timezone: text('timezone').default('UTC'),
updatedAt: text('updated_at').notNull().default(sql`CURRENT_TIMESTAMP`),
}, (table) => ({
userIdIdx: index('idx_user_preferences_user_id').on(table.userId),
}));
export const userPreferencesRelations = relations(userPreferences, ({ one }) => ({
user: one(users, {
fields: [userPreferences.userId],
references: [users.id],
}),
}));
export const usersRelations = relations(users, ({ one, many }) => ({
preferences: one(userPreferences), // Add this line
checkIns: many(checkIns),
}));
Step 3: Generate and Review
npm run db:generate
Step 4: Verify Generated SQL
-- drizzle/0001_create_user_preferences.sql
CREATE TABLE `user_preferences` (
`id` text PRIMARY KEY NOT NULL,
`user_id` text NOT NULL,
`theme` text DEFAULT 'auto',
`notifications` integer DEFAULT true,
`timezone` text DEFAULT 'UTC',
`updated_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
);
CREATE INDEX `idx_user_preferences_user_id` ON `user_preferences` (`user_id`);
What Expert Catches: FK cascade behavior is correct, index is present, defaults are appropriate What Novice Misses: Would forget the index, might not consider cascade behavior
npm run db:generate)/drizzle folderonDelete behaviornpm run db:migrate)❌ DO NOT use this skill for:
supabase-admin skill instead✅ Instead delegate to:
supabase-admin for PostgreSQL schema managementdatabase-design for entity relationship modelingsql-optimization for query performance issuestools
Building resilient distributed systems with circuit breakers, retries with full-jitter exponential backoff, retry budgets (per-request 3-attempt + per-client 10% ratio per Google SRE), deadline propagation, and the cascading-failure math (4 layers × 3 retries = 64x amplification). Grounded in Resilience4j, Microsoft Cloud Patterns, AWS Architecture Blog (Marc Brooker), and Google SRE Book.
testing
Designing HTTP cache headers that work correctly across browsers, CDNs, and shared proxies — `Cache-Control` directives per RFC 9111, `stale-while-revalidate` and `stale-if-error` per RFC 5861, the Vary header for varying responses, and surrogate keys for tag-based purging. Grounded in IETF RFCs and Cloudflare/Fastly docs.
development
Use when designing or fixing a Content Security Policy on a real site, choosing between nonce-based and hash-based CSP, adding strict-dynamic, debugging "Refused to execute inline script" errors, deploying CSP in report-only mode first, configuring report-to / report-uri, or auditing an existing policy for unsafe-inline / unsafe-eval / wildcards. Triggers: "CSP blocks legitimate inline script", strict-dynamic, nonce-{RANDOM}, sha256-{HASH}, object-src none, base-uri none, frame-ancestors, Trusted Types, X-Content-Security-Policy obsolete, report-only vs enforced. NOT for general HTTP security headers (HSTS, COOP/COEP), Trusted Types deep dive, CORS configuration, or building a WAF.
tools
Choosing and operating an HTTP API versioning strategy that doesn't break clients — Stripe's date-based pinned versions, the Deprecation/Sunset header pair (RFC 9745 + RFC 8594), URI vs header vs media-type approaches, and the version-transformer pattern. Grounded in Stripe's published architecture and IETF RFCs.