mcpField
D
drizzle-migrations

drizzle-migrations

github.com/bobmatnyc数据与数据库781.3k 次安装15 周前更新
代码仓库

关于 drizzle-migrations

面向 TypeScript/JavaScript 项目的 Drizzle ORM 数据库迁移技能,适合需要管理 schema 变更的开发者。它以 SQL 迁移为唯一变更起点,并强调快照和 CI 检查以控制 schema 漂移。

主要功能

  1. 01以 SQL 迁移优先管理 schema
  2. 02从迁移生成 TypeScript 定义
  3. 03使用快照检测 schema 漂移
  4. 04按领域组织 schema
  5. 05在 CI 与暂存环境验证变更

适用场景

  1. 01为数据库新增表或列
  2. 02在多个环境间保持 schema 一致
  3. 03检查并处理迁移导致的 schema 漂移

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/bobmatnyc/claude-mpm-skills --skill drizzle-migrations

安装后在 Claude Code 中输入/drizzle-migrations确认已加载。

附带文件metadata.json

详细介绍

Drizzle ORM 数据库迁移(TypeScript)

使用 Drizzle ORM 为 TypeScript/JavaScript 项目采用迁移优先的数据库开发工作流。

何时使用此技能

适用于以下情况:

  • 在 TypeScript/JavaScript 项目中使用 Drizzle ORM
  • 需要创建或修改数据库架构
  • 希望采用迁移优先的开发工作流
  • 设置新的数据库表或列
  • 需要确保各环境中的架构一致

核心原则:迁移优先开发

关键规则:架构变更始终从迁移开始,绝不先编写代码。

为什么要迁移优先?
  • ✅ SQL 迁移是唯一可信来源
  • ✅ 防止各环境之间出现架构漂移
  • ✅ 支持回滚和版本管理
  • ✅ 促使明确做出架构设计决策
  • ✅ 从迁移生成 TypeScript 类型
  • ✅ CI/CD 可以验证架构变更
反面模式(代码优先)

❌ 错误:先编写 TypeScript 架构

// DON'T DO THIS FIRST
export const users = pgTable('users', {
  id: uuid('id').primaryKey(),
  email: text('email').notNull(),
});
正确模式(迁移优先)

✅ 正确:先编写 SQL 迁移

-- drizzle/0001_add_users_table.sql
CREATE TABLE users (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  email TEXT NOT NULL UNIQUE,
  created_at TIMESTAMP DEFAULT NOW()
);

完整迁移工作流

第 1 步:在 SQL 迁移中设计架构

创建描述清晰的 SQL 迁移文件:

-- drizzle/0001_create_school_calendars.sql
CREATE TABLE school_calendars (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  school_id UUID NOT NULL REFERENCES schools(id) ON DELETE CASCADE,
  start_date DATE NOT NULL,
  end_date DATE NOT NULL,
  academic_year TEXT NOT NULL,
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW()
);

-- Add indexes for query performance
CREATE INDEX idx_school_calendars_school_id ON school_calendars(school_id);
CREATE INDEX idx_school_calendars_academic_year ON school_calendars(academic_year);

-- Add constraints
ALTER TABLE school_calendars
  ADD CONSTRAINT check_date_range
  CHECK (end_date > start_date);

命名约定:

  • 使用连续编号:0001_、0002_ 等
  • 使用描述性名称:create_school_calendars、add_user_roles
  • 格式:XXXX_descriptive_name.sql
第 2 步:生成 TypeScript 定义

Drizzle Kit 从 SQL 生成 TypeScript 类型:

# Generate TypeScript schema and snapshots
pnpm drizzle-kit generate

# Or using npm
npm run db:generate

生成内容:

  1. TypeScript 架构文件(如果使用 drizzle-kit push)
  2. drizzle/meta/XXXX_snapshot.json 中的快照文件
  3. 迁移元数据
第 3 步:创建架构快照

快照可用于检测架构漂移:

// drizzle/meta/0001_snapshot.json (auto-generated)
{
  "version": "5",
  "dialect": "postgresql",
  "tables": {
    "school_calendars": {
      "name": "school_calendars",
      "columns": {
        "id": {
          "name": "id",
          "type": "uuid",
          "primaryKey": true,
          "notNull": true,
          "default": "gen_random_uuid()"
        },
        "school_id": {
          "name": "school_id",
          "type": "uuid",
          "notNull": true
        }
      }
    }
  }
}

将快照纳入版本控制:

  • ✅ 将快照提交到 git
  • ✅ 支持在 CI 中检测漂移
  • ✅ 记录架构历史
第 4 步:实现 TypeScript 架构

现在编写与 SQL 迁移一致的 TypeScript 架构:

// src/lib/db/schema/school/calendar.ts
import { pgTable, uuid, date, text, timestamp } from 'drizzle-orm/pg-core';
import { schools } from './school';

export const schoolCalendars = pgTable('school_calendars', {
  id: uuid('id').primaryKey().defaultRandom(),
  schoolId: uuid('school_id')
    .notNull()
    .references(() => schools.id, { onDelete: 'cascade' }),
  startDate: date('start_date').notNull(),
  endDate: date('end_date').notNull(),
  academicYear: text('academic_year').notNull(),
  createdAt: timestamp('created_at').defaultNow(),
  updatedAt: timestamp('updated_at').defaultNow(),
});

// Type inference
export type SchoolCalendar = typeof schoolCalendars.$inferSelect;
export type NewSchoolCalendar = typeof schoolCalendars.$inferInsert;

要点:

  • 列名与 SQL 完全一致:school_id → 'school_id'
  • TypeScript 属性名使用 camelCase:schoolId
  • 在 SQL 中而非 TypeScript 中定义约束和索引
  • 外键引用其他表
第 5 步:按领域组织架构

以便于维护的方式组织架构:

src/lib/db/schema/
├── index.ts              # Export all schemas
├── school/
│   ├── index.ts
│   ├── district.ts
│   ├── holiday.ts
│   ├── school.ts
│   └── calendar.ts
├── providers.ts
├── cart.ts
└── users.ts

index.ts(导出全部内容):

// src/lib/db/schema/index.ts
export * from './school';
export * from './providers';
export * from './cart';
export * from './users';

school/index.ts:

// src/lib/db/schema/school/index.ts
export * from './district';
export * from './holiday';
export * from './school';
export * from './calendar';
第 6 步:在 CI 中添加质量检查

在 CI/CD 中验证架构一致性:

# .github/workflows/quality.yml
name: Quality Checks

on:
  pull_request:
    branches: [main, develop]
  push:
    branches: [main]

jobs:
  quality:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'pnpm'

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Check database schema drift
        run: pnpm drizzle-kit check

      - name: Verify migrations (dry-run)
        run: pnpm drizzle-kit push --dry-run
        env:
          DATABASE_URL: ${{ secrets.STAGING_DATABASE_URL }}

      - name: Run type checking
        run: pnpm tsc --noEmit

      - name: Lint code
        run: pnpm lint

CI 检查说明:

  • drizzle-kit check:验证快照与架构相符
  • drizzle-kit push --dry-run:测试迁移,但不实际应用
  • 类型检查:确保 TypeScript 可以编译
  • Lint:执行代码风格规范
第 7 步:在预发布环境测试

上线生产环境前,先在预发布环境测试迁移:

# 1. Run migration on staging
STAGING_DATABASE_URL="..." pnpm drizzle-kit push

# 2. Verify schema
pnpm drizzle-kit check

# 3. Test affected API routes
curl https://staging.example.com/api/schools/calendars

# 4. Check for data integrity issues
# Run queries to verify data looks correct

# 5. Monitor logs for errors
# Check application logs for migration-related errors

预发布检查清单:

  • 迁移运行无错误
  • 架构漂移检查通过
  • 使用新架构的 API 路由运行正常
  • 不存在数据完整性问题
  • 应用程序日志无错误
  • 查询性能可接受

常见迁移模式

添加列
-- drizzle/0005_add_user_phone.sql
ALTER TABLE users
ADD COLUMN phone TEXT;

-- Add index if querying by phone
CREATE INDEX idx_users_phone ON users(phone);

TypeScript:

export const users = pgTable('users', {
  id: uuid('id').primaryKey(),
  email: text('email').notNull(),
  phone: text('phone'), // New column
});
创建连接表
-- drizzle/0006_create_provider_specialties.sql
CREATE TABLE provider_specialties (
  provider_id UUID NOT NULL REFERENCES providers(id) ON DELETE CASCADE,
  specialty_id UUID NOT NULL REFERENCES specialties(id) ON DELETE CASCADE,
  PRIMARY KEY (provider_id, specialty_id)
);

CREATE INDEX idx_provider_specialties_provider ON provider_specialties(provider_id);
CREATE INDEX idx_provider_specialties_specialty ON provider_specialties(specialty_id);

TypeScript:

export const providerSpecialties = pgTable('provider_specialties', {
  providerId: uuid('provider_id')
    .notNull()
    .references(() => providers.id, { onDelete: 'cascade' }),
  specialtyId: uuid('specialty_id')
    .notNull()
    .references(() => specialties.id, { onDelete: 'cascade' }),
}, (table) => ({
  pk: primaryKey(table.providerId, table.specialtyId),
}));
修改列类型
-- drizzle/0007_change_price_to_decimal.sql
ALTER TABLE services
ALTER COLUMN price TYPE DECIMAL(10, 2);

TypeScript:

import { decimal } from 'drizzle-orm/pg-core';

export const services = pgTable('services', {
  id: uuid('id').primaryKey(),
  name: text('name').notNull(),
  price: decimal('price', { precision: 10, scale: 2 }).notNull(),
});
添加约束
-- drizzle/0008_add_email_constraint.sql
ALTER TABLE users
ADD CONSTRAINT users_email_unique UNIQUE (email);

ALTER TABLE users
ADD CONSTRAINT users_email_format CHECK (email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}$');

配置

drizzle.config.ts
import type { Config } from 'drizzle-kit';

export default {
  schema: './src/lib/db/schema/index.ts',
  out: './drizzle',
  driver: 'pg',
  dbCredentials: {
    connectionString: process.env.DATABASE_URL!,
  },
} satisfies Config;
package.json 脚本
{
  "scripts": {
    "db:generate": "drizzle-kit generate:pg",
    "db:push": "drizzle-kit push:pg",
    "db:studio": "drizzle-kit studio",
    "db:check": "drizzle-kit check:pg",
    "db:up": "drizzle-kit up:pg"
  }
}

迁移测试工作流

本地测试
# 1. Create migration
echo "CREATE TABLE test (...)" > drizzle/0009_test.sql

# 2. Generate TypeScript
pnpm db:generate

# 3. Push to local database
pnpm db:push

# 4. Verify schema
pnpm db:check

# 5. Test in application
pnpm dev
# Manually test affected features

# 6. Run tests
pnpm test
回滚策略
-- drizzle/0010_add_feature.sql (up migration)
CREATE TABLE new_feature (...);

-- drizzle/0010_add_feature_down.sql (down migration)
DROP TABLE new_feature;

执行回滚:

# Manually run down migration
psql $DATABASE_URL -f drizzle/0010_add_feature_down.sql

最佳实践

应该做
  • ✅ 优先编写 SQL 迁移
  • ✅ 使用描述性迁移名称
  • ✅ 为外键添加索引
  • ✅ 在迁移中加入约束
  • ✅ 在生产环境上线前于预发布环境测试迁移
  • ✅ 将快照提交到版本控制
  • ✅ 按领域组织架构
  • ✅ 在 CI 中使用 drizzle-kit check
禁忌
  • ❌ 切勿在 SQL 迁移之前编写 TypeScript 架构
  • ❌ 不要跳过预发布环境测试
  • ❌ 不要修改旧迁移(请创建新迁移)
  • ❌ 别忘了添加索引
  • ❌ 不要在生产环境中使用 drizzle-kit push(请使用规范的迁移)
  • ❌ 没有快照时,不要提交生成的文件

故障排查

检测到架构漂移

错误:Schema drift detected

解决方案:

# Check what changed
pnpm drizzle-kit check

# Regenerate snapshots
pnpm drizzle-kit generate

# Review changes and commit
git add drizzle/meta/
git commit -m "Update schema snapshots"
迁移在预发布环境中失败

错误:迁移因数据约束冲突而失败

解决方案:

  1. 回滚迁移
  2. 创建数据迁移脚本
  3. 先运行数据迁移
  4. 然后运行架构迁移
-- First: Migrate data
UPDATE users SET status = 'active' WHERE status IS NULL;

-- Then: Add constraint
ALTER TABLE users
ALTER COLUMN status SET NOT NULL;
TypeScript 类型不同步

错误:TypeScript 类型与数据库不匹配

解决方案:

# Regenerate everything
pnpm db:generate
pnpm tsc --noEmit

# If still broken, check schema files
# Ensure column names match SQL exactly

相关技能

  • universal-data-database-migration - 通用迁移模式
  • toolchains-typescript-data-drizzle - Drizzle ORM 使用模式
  • toolchains-typescript-core - TypeScript 最佳实践
  • universal-debugging-verification-before-completion - 验证工作流

评论

支持 Markdown