8.1 SQLite 的 ALTER TABLE 局限
SQLite 的 ALTER 能力历来较弱:
- ✅
ADD COLUMN(新增列,带默认值) - ✅
RENAME TABLE、RENAME COLUMN(3.25+) - ✅
DROP COLUMN(3.35+) - ❌ 修改列类型 / 约束
- ❌ 加/改外键(要重建表)
"改列类型"的标准做法是 12 步重建(官方文档有模板):
PRAGMA foreign_keys = OFF;
BEGIN;
CREATE TABLE new_users (...);
INSERT INTO new_users SELECT ... FROM users;
DROP TABLE users;
ALTER TABLE new_users RENAME TO users;
COMMIT;
PRAGMA foreign_keys = ON;
好消息:drizzle-kit 自动生成这种重建 SQL。
8.2 两条路线
- A. 纯手写 SQL 迁移
- 自己写
migrations/0001_init.sql、0002_add_column.sql;用一个驱动(migrate命令)顺序 apply。控制感最强。 - B. drizzle-kit 自动生成
- 改 TS schema →
drizzle-kit generate产出 SQL →drizzle-kit migrate应用。省心但偶尔要手改(复杂重命名)。
8.3 drizzle-kit 工作流(推荐)
# 1. 改 schema.ts
# 2. 生成迁移
pnpm drizzle-kit generate --name add_posts_published
# 产出:
# db/migrations/0002_add_posts_published.sql
# db/migrations/meta/_journal.json
# 3. 本地验证
pnpm drizzle-kit migrate
# 应用到 drizzle.config.ts 里配的库
# 4. 检查远端 schema
turso db shell my-app .schema
meta/_journal.json
drizzle-kit 把每次生成的迁移编号和 checksum 记下来——不要改这个文件,它是"已应用"的真相来源。
8.4 CI 里自动 migrate
# .github/workflows/deploy.yml
- name: Apply DB migrations
env:
TURSO_DATABASE_URL: ${{ secrets.TURSO_URL }}
TURSO_AUTH_TOKEN: ${{ secrets.TURSO_TOKEN }}
run: |
pnpm install
pnpm drizzle-kit migrate
- name: Deploy app
run: wrangler deploy
重要次序:先 migrate,再 deploy 新代码。反过来新代码访问旧 schema 会 500。
8.5 迁移原子性
drizzle-kit migrate 把每条迁移包在 BEGIN ... COMMIT 里。SQLite 事务是原子的——要么全改完、要么回滚。失败的迁移不会留下半成品 schema。
但"表重建"类迁移如果中断,SQLite 可能残留 new_users 临时表——下次重跑前需手工清。生产库 migrate 前务必 dump 备份。
8.6 长表的在线迁移
大表加列默认值——单条 ALTER TABLE ADD COLUMN x INT DEFAULT 0 在 SQLite 3.35+ 是O(1)(记在 schema 头部,读时按需填),不会锁表扫描。
但"改列类型"这种 12 步重建会把整个表 COPY——对于百万行级别可能要分钟级锁。实务方案:
- 新版加新列
x2,先写双写(应用层同时写x和x2) - 后台迁移把旧数据填到
x2 - 应用切成读
x2 - 下一版删掉
x
8.7 多租户:DB-per-tenant
Turso 的一个杀手级用法——每个客户一个独立数据库:
- 免费档就有 500 个 db,付费无上限
- 每个库独立备份、可以按客户区域选 primary
- 出事互不影响,GDPR 删数据"删一个 db"即可
- 挑战:schema 改动要 fan-out 应用到所有 tenant db
批量迁移脚本
import { $ } from 'bun';
import { createClient } from '@libsql/client';
import { migrate } from 'drizzle-orm/libsql/migrator';
import { drizzle } from 'drizzle-orm/libsql';
const { stdout } = await $`turso db list --json`;
const dbs = JSON.parse(stdout).filter((d: any) => d.name.startsWith('tenant_'));
for (const info of dbs) {
console.log(`migrating ${info.name}...`);
const client = createClient({
url: info.url,
authToken: process.env.ADMIN_TOKEN, // 组织级 token
});
const db = drizzle(client);
await migrate(db, { migrationsFolder: './db/migrations' });
}
注意 rate limit——一次别启动几百个并发客户端,分批来(比如 Promise.all 加 p-limit 10)。
8.8 向后兼容的发布流程
避免"部署期间半新半旧":
- 加而不破:先加新列/新表、不删旧的;两套代码都能跑
- 部署新应用
- 验证新应用稳定运行一段时间
- 再发一版迁移,删掉旧列/旧表
8.9 常见迁移坑清单
- ✘ 新增非空列且无默认值 → SQLite 拒绝。必须给
DEFAULT或先 NULL 再 update 再约束 - ✘ 改主键 → 必须 12 步重建
- ✘ 视图/触发器依赖 → 重建前要 DROP VIEW、之后 CREATE 回
- ✘ 生产直接跑
drizzle-kit push(会产生不受版本控制的改动)——禁用 - ✘ 嵌入式副本的本地文件落后于 schema → 重启前先
sync()
小结
Turso/libSQL schema 迁移:drizzle-kit 自动生成 SQL + CI 里 migrate,覆盖 90% 场景;复杂重建与改主键需要手写。多租户 DB-per-tenant 是 Turso 特色玩法——迁移要 fan-out,记得做速率限制。下一章讲备份与时间旅行。