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 issuesdata-ai
license: Apache-2.0 NOT for unrelated tasks outside this domain.
development
Use when designing caching strategies (cache-aside, write-through, write-behind), implementing distributed locks, building rate limiters, leaderboards, real-time streams (XADD/consumer groups), pub/sub, or tuning eviction policies. Triggers: thundering-herd on cache miss, dogpile on key expiry, Redlock vs SET-NX-PX choice, sliding-window rate limiter, hot-key on a single cluster slot, big-key blowup, MULTI/EXEC across slots, KEYS in production. NOT for Redis Cluster operations/admin (different domain), embedded KV (SQLite, leveldb), in-process LRU caches, or Memcached.
tools
Drawing the `'use client'` boundary correctly in React Server Components apps (Next.js App Router, RSC frameworks) — leaf-pushing, slot composition, serialization rules, and environment poisoning prevention. Grounded in react.dev and Next.js 16 docs.
development
Use when designing rate limiting for an API, choosing between token bucket / sliding window / leaky bucket / fixed window, implementing it in Redis, deciding edge (Cloudflare/Upstash) vs origin enforcement, sizing per-user vs per-IP vs per-endpoint quotas, returning the right 429 response with Retry-After, or fixing the boundary-burst bug in fixed-window limiters. Triggers: 429 too many requests, INCR + EXPIRE, ZADD + ZREMRANGEBYSCORE + ZCARD, X-RateLimit-Remaining header, Cloudflare WAF rate limiting rules, Upstash @upstash/ratelimit, leaky bucket shaping vs policing, distributed rate limiter consistency. NOT for DDoS mitigation specifically (different scale), CAPTCHA / bot management, full WAF design, or per-user quota billing.