
LobeHub 数据库迁移实战指南Drizzle Schema 规范、三类上线策略与幂等化加固【免费下载链接】lobehub LobeHub is your Chief Agent Operator, organizing your agents into 7×24 operations by hiring, scheduling, and reporting on your entire AI team.项目地址: https://gitcode.com/GitHub_Trending/lo/lobehub导读本文以 LobeHub 仓库根工作区包lobehub/lobehub沉淀的数据库迁移规范 SKILL.md 为主线系统讲解该团队在 PostgreSQL Drizzle 下从「Schema 怎么写」到「迁移怎么发」的完整方法论先约定 Schema 层避免枚举陷阱再按变更风险把上线路径划分为「常规迁移 / 在线建索引 / 数据回填」三类并给出drizzle-kit generate的标准四步产物加固流程。读完你不仅能照做生成安全、可重放、幂等的迁移文件还能理解仓库中 150 个历史迁移为何普遍采用IF NOT EXISTS、DROP CONSTRAINT IF EXISTS ADD这类防御性写法。为什么需要一份迁移指南LobeHub 的数据库模型非常庞大且持续演进在 packages/database/migrations 下已经积累到0158_file_upload_reservations.sql每个迁移都配套一份快照与 journal 记录。仓库将所有表结构集中在 packages/database/src/schemas并由根目录 drizzle.config.ts 统一驱动生成export default { dbCredentials: { url: connectionString }, // 取自 .env 的 DATABASE_URL dialect: postgresql, out: ./packages/database/migrations, // 迁移 SQL 输出目录 schema: ./packages/database/src/schemas, // Schema 源目录 strict: true, } satisfies Config;在此基础上团队把数据库上线策略、Schema 约定、生成工作流沉淀为一份面向 AI Agent 的 Skill 文档覆盖以下高频场景数据库上线策略选择、Drizzle 迁移生成、在线索引创建、数据回填、迁移再生成、变基冲突、幂等 SQL 审查与迁移重命名。下面的内容即对这份指南的完整展开并结合仓库实际文件逐一佐证。Schema 约定在生成迁移之前先把 Schema「写对」规范的第一个原则很明确Schema 层的写法会影响未来每一次迁移的复杂度而不只是当前这条 SQL。因此在生成任何迁移之前就要遵守两条约定。增长型取值域不要用 pg enum也不要硬编码取值列表对于取值集合会持续扩张的列资源类型、状态、provider 等使用普通text列 .$typeUnionType()来承接类型// ✅ Good — 类型定义在 lobechat/types列本身仍是普通 text resourceType: text(resource_type).$typeTransferResourceType().notNull(), // ❌ Bad — pg enum每新增一个取值都需要一次 ALTER TYPE ... ADD VALUE 迁移 resourceType: resourceTypeEnum(resource_type).notNull(), // ❌ Avoid — 取值列表被硬编码进 schema 文件 resourceType: text(resource_type, { enum: TRANSFER_RESOURCE_TYPES }).notNull(),这样做的收益是以pgEnum定义的列每新增一个枚举值都要产生ALTER TYPE ... ADD VALUE的 DDL即便是 Drizzle 的text(col, { enum: [...] })简写也会把完整取值列表写死在 schema 文件里。而采用.$type()后新增取值的「上线」只是一次纯类型变更——连迁移都不需要。仓库实践中可以找到同源佐证例如 user.ts 中大量布尔、jsonb 列直接依赖lobechat/types提供的联合类型UserAgentOnboarding、UserOnboarding列定义保持精简类型则集中在类型包里管理。领域常量留在类型包Schema 文件只负责表结构在新写或改动 schemas 下的 schema 文件时共享的领域字面量数组、联合类型与 option 接口应归属lobechat/types每个领域一个模块并从其index.ts统一 re-export。Schema通过.$type()与消费方router 侧z.enum(...)、service、UI都从类型包导入形成单一事实来源。同时规范也划清了边界这条规则只针对领域常量。表对象、推断出的行类型、Drizzle relation 对象、以及 zod 的 insert/select schema如insertAgentSchema等本就是 schema 文件的职责应当留在原地。仓库中像 resourcePermission.ts 这样「历史遗留」地直接导出了领域常量的文件被冻结豁免grandfathered只在其下次被真正改动时顺带迁移不做批量清扫避免大范围无价值 diff。上线前先分类三种 rollout 路径规范要求在生成或修改迁移之前先把每一次数据库变更归入三种 rollout 路径之一。这不只是流程洁癖——它决定了你的 SQL 是「跟随部署自动执行」还是需要「手工前置操作」。前提先在真实的 Dev 数据库上验证假设一个重要的纪律是不要在假设上选路。类似「这个迁移可能很慢」「装这些触发器可能阻塞部署」这类推断不能作为选择手工生产步骤、延迟安装路径或专门回填的依据。应当先用项目批准的数据库访问工具对目标库分类该 Skill 文档提示了bun -epg直连的 Dev 库访问模式绝不直接读取含密钥的.env文件度量真实操作或最接近的安全等价物——例如在临时名称下创建同定义的探针索引或在会被回滚的事务里临时安装触发器记录被测试的 SQL/操作、代表性行数与表大小、耗时和清理验证结果保证探针可逆度量完成后删除全部临时数据库对象把单次 Dev 结果当作观测到的 Dev 规模证据而不是生产行为证明——如果生产规模、负载、缓存状态与锁竞争存在实质性差异必须显式说明并给生产结论打上 inference 标签。Rollout 决策必须同时结合仓库的部署事实与上述度量结果不应仅为了防范未经验证的性能担忧而凭空增加运维表、延迟激活路径或手工发布步骤。这恰好与仓库的部署方式呼应根 package.json 中build:vercel会执行build:raw db:migrate而db:migrate指向 scripts/migrateServerDB 的迁移执行脚本——部署管线已经在自动应用迁移因此凡管线已覆盖的迁移就不必再要求发布时冗余手工执行。路径 1常规 Drizzle 迁移适用于可安全随部署执行的 schema 变更例如创建小表或新增可空列更新 Drizzle schema执行bun run db:generate按下文各步骤审查与加固生成的产物。在把某个手工迁移命令列入 release 步骤前先检查目标仓库的构建与部署脚本若其部署管线已自动应用迁移就不要额外要求一次冗余的手工执行。路径 2在线索引创建Online Index Creation在大表或高频访问表上创建索引通常的CREATE INDEX会阻塞写入与查询长事务还可能拖垮部署。处理方式是把建索引拆成两步部署前先在目标库的 SQL 编辑器中手工执行CONCURRENTLY版本CREATE INDEX CONCURRENTLY IF NOT EXISTS table_column_idx ON table USING btree (column);迁移文件里保留幂等的非CONCURRENTLY版本CREATE INDEX IF NOT EXISTS table_column_idx ON table USING btree (column);手工在线操作避免了阻塞线上流量等部署真正跑到这条迁移时IF NOT EXISTS使它成为一个 no-op而新建或自托管数据库仍可通过正常的迁移重放收敛到同一结构。注意CREATE INDEX CONCURRENTLY不能放在事务内执行这是 PostgreSQL 的硬性限制。路径 3数据回填Data Backfill回填与历史数据对账必须作为独立的、幂等的脚本运行而不是塞进 Drizzle 迁移里。schema 变更保留在 Drizzle 中但逐行或分批的数据处理挪到单独脚本避免阻塞部署。执行时机按兼容性决定部署前运行当新代码或新约束要求存量行立即具备新字段形态时部署后运行当应用能同时安全处理新旧两种行形态、回填可渐进收敛时。回填脚本本身应满足可断点续跑、可安全重试、按有界批次处理、可观测。可选的清理或激进式对账应视为 optional而不是 release blocker。开发期 Schema 反复变更删草稿、重新生成而不是手改 SQL功能开发期间 schema 会频繁变动。规范给出的处理原则非常明确在迁移尚未发布时就改动 schema不要手改既有迁移 SQL 去追新的 schema 形状。正确做法是删除本分支新增的草稿迁移产物SQL 文件 对应 snapshot 对应 journal entry再跑一次生成器并重新走一遍常规迁移审查。例如分支草稿迁移叫0110_add_verify_tables_and_ai_infra_id当前仓库确实存在该编号见 0110_add_verify_tables_and_ai_infra_id.sql# 1. 删除草稿 SQL 及其 snapshot rm packages/database/migrations/0110_add_verify_tables_and_ai_infra_id.sql rm packages/database/migrations/meta/0110_snapshot.json # 2. 从 journal 的 entries 数组移除对应的 0110 条目 # packages/database/migrations/meta/_journal.json # 3. 基于当前 schema 重新生成 bun run db:generate这套动作确保生成的 SQL、snapshot、journal 与实际 schema 始终对齐。手工编辑 SQL 仅保留给 review 期的加固动作比如补幂等子句、自定义扩展 SQL、有意义的文件名/tag 更新。合并开发期的中间态迁移发布前如果功能分支累积了多条开发期迁移尽量合并为一条——生产环境没必要重放每个中间草稿形态迁移越少部署风险越低。例如本分支新增了0110、0111、0112# 1. 删除本分支新增的全部草稿 SQL 与 snapshot rm packages/database/migrations/011{0,1,2}_*.sql rm packages/database/migrations/meta/011{0,1,2}_snapshot.json # 2. 从 _journal.json 的 entries 数组移除 0110/0111/0112 条目 # 3. 重新生成一条覆盖完整 schema delta 的迁移 bun run db:generate不要为「未发布的自己」做兼容迁移还有一条容易踩坑的规则不要做与同一分支早期开发版本兼容的迁移。迁移未发布就没有生产历史需要保留本地/开发库直接用什么 SQL 简单修什么 SQL删草稿表、重命名列、删草稿行然后从当前 schema 重新生成分支迁移即可。例如早前草稿建过signup_attempt_id后来你改名为user_signup_log_id——不要在迁移里加ALTER ... RENAME兼容只需按项目 Dev 库访问模式直接修库再重新生成# 以最简单 SQL 将 Dev 库修正为新 schema set -a source .env set a bun -e import pg from pg; const client new pg.Client({ connectionString: process.env.DATABASE_URL }); await client.connect(); await client.query(ALTER TABLE user_signup_logs DROP COLUMN signup_attempt_id); await client.end(); # 重新生成使迁移只反映最终形态 bun run db:generate一旦迁移已进入生产或目标默认分支就把它视为不可变需要变更时追加新的后续迁移而不是重写历史。变基冲突保留上游删除分支迁移重新生成当 rebase 在迁移文件上发生冲突时处理方式与开发期重生成一脉相承保留 upstream/默认分支的迁移移除当前功能分支引入的所有迁移完成 rebase基于 rebase 后的 schema 重新生成该分支的迁移。这样可以避免合并两个相互独立的 snapshot、或手工拼接 journal entries 的混乱局面。背后逻辑与前面一致——既然分支迁移还没发布就没有合并价值重新生成永远是最干净的收敛方式。标准生成工作流四步走下面的「Step 1 ~ Step 4」是仓库对db:generate产物的完整加固流程任何一条迁移都应走完。Step 1生成迁移bun run db:generate根据根 package.jsondb:generate实际展开为drizzle-kit generate npm run workflow:dbml即先生成迁移再通过 scripts/dbmlWorkflow 刷新 DBML 文档。它生成packages/database/migrations/0046_meaningless_file_name.sql一个新编号的迁移 SQL并更新packages/database/migrations/meta/_journal.jsonpackages/database/src/core/migrations.jsondocs/development/database-schema.dbml其中 journal 与 snapshot 的对应关系可从 meta 目录 直接看到每个编号一个0xxx_snapshot.json外加一份统一的_journal.json。补充一点仓库核验结论文档记录的src/core/migrations.json属于构建/部署期产物在当前源码树中并不常驻仓库 core 目录 下仅见db-adaptor.ts、getTestDB.ts、web-server.ts其生成/消费由部署流程中的 scripts/migrateServerDB 承担而db:generate脚本本体只由drizzle-kit generate与workflow:dbml组成。写作迁移时以「SQL snapshot journal」三件套为审查重点即可。自定义迁移Custom Migrations对于不涉及 Drizzle schema 变更的迁移如启用 PostgreSQL 扩展使用--custom标志bunx drizzle-kit generate --custom --nameenable_pg_search这会生成一个空 SQL 文件并正确更新_journal.json与 snapshot。随后编辑生成的 SQL 文件加入自定义 SQL-- Custom SQL migration file, put your code below! -- CREATE EXTENSION IF NOT EXISTS pg_search;仓库中的 0090_enable_pg_search.sql 正是这种模式的落地实例后续的全文检索 BM25 索引如 0093_add_bm25_indexes_with_icu.sql也在同一条线上演进。核心纪律绝不手工创建迁移文件或手改_journal.json——始终通过drizzle-kit generate确保 journal entries 与 snapshots 的正确性。Step 2优化迁移 SQL 文件名把自动生成的「无语义」文件名改为有意义的描述性名称0046_meaningless_file_name.sql→0046_user_add_avatar_column.sql仓库迁移目录是很好的示范0158_file_upload_reservations.sql、0143_resource_transfer_requests.sql这类命名让 150 条迁移的历史一目了然。Step 3使用幂等子句防御性编程始终使用防御性子句让迁移可以安全重跑。下表是规范的核心对照仓库迁移同样逐条遵守DDL 操作✅ 幂等写法❌ 非幂等写法CREATE TABLECREATE TABLE IF NOT EXISTS裸CREATE TABLE新增列ADD COLUMN IF NOT EXISTS裸ADD COLUMN删除列DROP COLUMN IF EXISTS裸DROP COLUMN外键约束DROP CONSTRAINT IF EXISTSADD CONSTRAINT裸ADD CONSTRAINTDROP TABLEDROP TABLE IF EXISTS裸DROP TABLE普通索引CREATE INDEX IF NOT EXISTS裸CREATE INDEX唯一索引CREATE UNIQUE INDEX IF NOT EXISTS ... USING btree裸CREATE UNIQUE INDEXCREATE TABLE-- ✅ Good CREATE TABLE IF NOT EXISTS agent_eval_runs ( id text PRIMARY KEY NOT NULL, name text, created_at timestamp with time zone DEFAULT now() NOT NULL ); -- ❌ Bad CREATE TABLE agent_eval_runs (...);ALTER TABLE - 列-- ✅ Good ALTER TABLE users ADD COLUMN IF NOT EXISTS avatar text; ALTER TABLE posts DROP COLUMN IF EXISTS deprecated_field; -- ❌ Bad ALTER TABLE users ADD COLUMN avatar text;ALTER TABLE - 外键约束PostgreSQL没有ADD CONSTRAINT IF NOT EXISTS因此幂等姿势必须是「先 DROP 再 ADD」-- ✅ Good: Drop first, then add (idempotent) ALTER TABLE agent_eval_datasets DROP CONSTRAINT IF EXISTS agent_eval_datasets_user_id_users_id_fk; ALTER TABLE agent_eval_datasets ADD CONSTRAINT agent_eval_datasets_user_id_users_id_fk FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE cascade ON UPDATE no action; -- ❌ Bad: Will fail if constraint already exists ALTER TABLE agent_eval_datasets ADD CONSTRAINT agent_eval_datasets_user_id_users_id_fk FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE cascade ON UPDATE no action;DROP TABLE / INDEX-- ✅ Good DROP TABLE IF EXISTS old_table; CREATE INDEX IF NOT EXISTS users_email_idx ON users (email); CREATE UNIQUE INDEX IF NOT EXISTS users_email_unique ON users USING btree (email); -- ❌ Bad DROP TABLE old_table; CREATE INDEX users_email_idx ON users (email);仓库中最新的迁移 0158_file_upload_reservations.sql 就是这套风格的整体呈现CREATE TABLE IF NOT EXISTS建表 → 对每个外键先DROP CONSTRAINT IF EXISTS再ADD CONSTRAINT→ 多个CREATE INDEX IF NOT EXISTS最后甚至用带WHERE status IN (active,cleaning)的CREATE UNIQUE INDEX IF NOT EXISTS部分索引表达「活跃/清理中文件路径唯一」的业务约束——既有语义又能安全重放。Step 4更新 Journal Tag完成 Step 2 的重命名后同步更新 packages/database/migrations/meta/_journal.json 中的tag字段使其与新的文件名一致不含.sql后缀。这一步保证 journal、snapshot 与 SQL 文件三者对齐是后续drizzle-kit增量比较与 CI 检查的前提。一份可复用的迁移 Checklist综合全文一条合规的 LobeHub 迁移应满足Schema 层增长型取值域用text(col).$typeUnionType()取值/联合类型定义在lobechat/typesschema 文件不导出领域常量策略层先按仓库批准的工具在 Dev 库实测再归类为「常规迁移 / 在线建索引 / 数据回填」之一部署管线已自动跑迁移时不得要求冗余手工执行生成层一律bun run db:generate或--custom生成空文件后补 SQL绝不手写文件或手改 journal加固层重命名为语义化文件名、补齐IF NOT EXISTS/DROP ... IF EXISTS幂等子句、同步 journaltag发布前合并同分支多条开发迁移、以「删草稿→重新生成」处理 schema 反复变更、上线后把迁移当不可变对象只做追加。这套方法论的最终目标是让 150 条累积迁移在「新建库重放、老库增量、回滚恢复」三种场景下都能正确收敛同时把大表索引、回填这类高风险操作从部署窗口里安全地拆出去。【免费下载链接】lobehub LobeHub is your Chief Agent Operator, organizing your agents into 7×24 operations by hiring, scheduling, and reporting on your entire AI team.项目地址: https://gitcode.com/GitHub_Trending/lo/lobehub创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考