ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Trigger.dev 数据库迁移安全指南:幂等、无锁与 CI 强制规则

Trigger.dev 数据库迁移安全指南:幂等、无锁与 CI 强制规则 Trigger.dev 数据库迁移安全指南幂等、无锁与 CI 强制规则【免费下载链接】trigger.devTrigger.dev – build and deploy durable AI agents and workflows项目地址: https://gitcode.com/gh_mirrors/tr/trigger.dev本指南系统梳理 Trigger.dev 仓库中 .claude/rules/database-safety.md 定义的生产级 Postgres 迁移规范如何用CREATE INDEX CONCURRENTLY避免加索引锁表、如何保证每个语句在部分应用后重跑依然幂等、如何在 CI 中用guard:migrations脚本自动拦截违规 SQL并配合 migrationSafetyGuard.core.ts 源码与真实迁移文件讲清每条规则的底层原因。读完你既能按规则写出可安全重跑的迁移脚本也能理解这套规则为何被强制实施。为什么 Trigger.dev 需要一套迁移安全规范Trigger.dev 的核心业务库如 internal-packages/database/prisma/migrations 与 internal-packages/run-ops-database/prisma/migrations运行在共享生产 Postgres 之上。生产环境里任何锁表操作都可能直接阻塞正在执行的 durable workflow 与 AI agent 任务写入而一次迁移失败后的部分应用状态又必须能靠重跑修复。因此规范确立了两条底线不能长时间锁表对已存在的生产表建索引必须使用CONCURRENTLY避免普通CREATE INDEX的SHARE锁阻塞整张表的写入必须幂等每一条语句都能在部分应用后安全重跑——已经执行过的部分必须是 no-op而不是报错或产生重复数据。这两条底线对应了后文的全部具体规则并且由 CI 脚本自动强制而不是靠人工自觉。规则一现有表加索引必须 CONCURRENTLY且一文件一语句对已存在的表添加索引规范要求CREATE INDEX CONCURRENTLY IF NOT EXISTS TaskRun_parentTaskRunId_idx ON TaskRun(parentTaskRunId);必须满足三个条件使用CONCURRENTLY、带IF NOT EXISTS、独占一个迁移文件一个文件里只能有这一条语句。后两个条件都有明确的 Postgres 机制原因。IF NOT EXISTS保证重跑时不再重复建索引而独占文件是因为Postgres 会把多语句迁移脚本放进同一个隐式事务执行而CREATE INDEX CONCURRENTLY在事务内是非法的——只要同一文件里还有其他语句重跑就会直接报错。仓库中有大量符合此模式的实例例如 add_parent_task_run_id_index_concurrently/migration.sql 全文件仅一条语句add_batch_task_run_created_at_dashboard_index/migration.sql 同样以单条CREATE INDEX CONCURRENTLY IF NOT EXISTS收尾。规则二新建表上的索引不需要 CONCURRENTLY但仍需 IF NOT EXISTS如果索引所在的表与索引在同一次迁移中创建同一个CREATE TABLE之后就不需要CONCURRENTLY——因为表尚不存在不可能有并发写入在等锁。但IF NOT EXISTS仍然必须保留以保证整份迁移重跑时索引创建是 no-op。这类表上由 Prisma 生成的约束如主键、唯一约束规范允许用一条语句内的先删后加替代 DO blockALTER TABLE ... DROP CONSTRAINT IF EXISTS name, ADD CONSTRAINT name ...;需要注意这个替代写法只适用于同文件新建的表。正如 migrationSafetyGuard.core.ts 中的实现所示guard 只有在ctx.createdTables.has(tableKey(table))该表在本文件被创建且约束名出现在同文件的DROP CONSTRAINT IF EXISTS中时才放行这条ADD CONSTRAINT对既有表这么写会每次重跑都在锁下重建约束属于违规。规则三现有表加新列拆成两个迁移文件当需要在已存在的表上新增一列并为其建索引时禁止在同一个迁移里一步完成。正确做法是拆分第一个迁移ALTER TABLE ... ADD COLUMN IF NOT EXISTS col ...单独成文第二个迁移CREATE INDEX CONCURRENTLY IF NOT EXISTS ...单独成文遵循规则一。拆分的理由与规则一完全一致索引必须CONCURRENTLY构建而CONCURRENTLY不能与其他语句同处一个隐式事务。仓库中的 add_project_id_to_task_schedule_instance/migration.sql 展示了先ADD COLUMN IF NOT EXISTS、再在后续迁移里补索引的拆分流派。规则四Prisma 生成迁移后必须清理多余行用 Prisma 生成迁移后diff 常常夹带与本次意图无关的噪音语句。规范明确要求删除这些多余行_BackgroundWorkerToBackgroundWorkerFile_BackgroundWorkerToTaskQueue_TaskRunToTaskRunTag_WaitpointRunConnections_completedWaitpointsSecretStore_key_idx以及无关的 TaskRun 索引这些大多是历史遗留的多对多关系表或冗余索引的重复声明保留它们会让迁移文件变得不可信也可能触发 guard 的无关规则。规则五全语句幂等——没有 IF NOT EXISTS 的语句用 DO block 保护这是整套规范的灵魂。每个语句都必须能幂等重跑具体到不同语句类型语句类型幂等写法CREATE TABLECREATE TABLE IF NOT EXISTSADD COLUMNADD COLUMN IF NOT EXISTSALTER TYPE ... ADD VALUEADD VALUE IF NOT EXISTSDROP ...DROP ... IF EXISTSDROP INDEXDROP INDEX CONCURRENTLY IF EXISTSINSERT数据加ON CONFLICT麻烦的是Postgres 对CREATE TYPE、ADD CONSTRAINT、RENAME这类语句没有IF NOT EXISTS语法无法直接写成幂等形式。规范的解决方案是把它们包进DO $$ ... $$块用目录表先查后建DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conname TaskScheduleInstance_projectId_fkey ) THEN ALTER TABLE public.TaskScheduleInstance ADD CONSTRAINT TaskScheduleInstance_projectId_fkey FOREIGN KEY (projectId) REFERENCES public.Project (id) ON DELETE CASCADE ON UPDATE CASCADE; END IF; END $$;上面的示例直接取自 add_project_id_to_task_schedule_instance/migration.sql。目录表按对象类型选择类型查pg_type、约束查pg_constraint、列查pg_attribute。guard 也接受第二种写法——在块内捕获重复错误DO $$ BEGIN CREATE TYPE TaskQueueConcurrencyVersion AS ENUM (V1, V2); EXCEPTION WHEN duplicate_object THEN NULL; END $$;见 add_task_queue_concurrency_version_and_role/migration.sql同文件还演示了ADD COLUMN IF NOT EXISTS与DEFAULT的组合。guard 的源码对EXCEPTION分支有细致检查条件必须匹配duplicate_*/undefined_*/unique_violation类且不重新RAISE见 migrationSafetyGuard.core.ts并且同一个 EXCEPTION 块内不能塞多条依赖该 handler 的 DDL——否则第一次语句报错跳到 handler 后后续语句根本不会执行。此外 guard 还强制 DO block 内的 DDL 也要按普通规则自检body 必须用$$ ... $$字符串常量包裹E-string 无法解码会报do-body-unsupportedDO 块里不允许出现CONCURRENTLY对既有表的CREATE INDEX在任何嵌套深度都会被拦截migrationSafetyGuard.core.ts。CI 强制与逃生舱guard:migrations 与 migration-guard: allow以上规则不是纸面文档而是由 CI 自动执行的。运行方式pnpm --filter webapp run guard:migrations该脚本定义在 apps/webapp/package.json固定形如guard:migrations: tsx ./scripts/migrationSafetyGuard.ts --cutoff 20260918--cutoff后的日期就是强制生效的截止线只检查时间戳不早于该值的迁移之前的迁移视为已应用、不再追溯。CLI 还支持其他用法见 migrationSafetyGuard.tspnpm --filter webapp run guard:migrations -- --all # 审计全部历史迁移 pnpm --filter webapp run guard:migrations -- --cutoff 20260101 # 从指定日期开始审计 pnpm --filter webapp run guard:migrations -- path/to/migration.sql ... # 只查指定文件脚本会扫描internal-packages/database/prisma/migrations与internal-packages/run-ops-database/prisma/migrations两个目录下以YYYYMMDDHHMMSS_命名的迁移文件夹逐个解析其migration.sql并输出违规规则实现在 migrationSafetyGuard.core.ts 的checkMigration/checkStatement/checkDoBlock等纯函数中有违规即非零退出CI 随之失败。其 SQL 解析器会正确跳过字符串、引号标识符、$...$dollar-quoted 字面量、E...转义串与两类注释避免误报。对于确实无法遵守规则的语句例如 Postgres 无法并发删除分区索引的场景规范提供了逃生舱——在语句上一行写注释-- migration-guard: allow 分区索引无法 CONCURRENTLY 删除Postgres 会拒绝 DROP INDEX IF EXISTS PartitionedIdx;逃生舱不是免责金牌原因必须填写一条没有抑制任何违规的 allow 指令本身就是违规allow-unused防止过期逃生舱残留。guard 源码中allow-reason-required、allow-unused两条规则实现了这一闭环migrationSafetyGuard.core.ts。两条硬性红线禁止删列删表、只面向 V2 引擎没有明确审批绝不删除列或表DROP COLUMN/DROP TABLE属于破坏性操作会摧毁生产数据必须走人工审批流程新代码只面向RunEngineVersion.V2新增代码不得再为 V1 引擎兼容分支保证演进方向单一、降低迁移复杂度。小结一套可落地的迁移安全 Checklist给当前仓库贡献迁移时按此清单自查现有表加索引 → 单独文件CREATE INDEX CONCURRENTLY IF NOT EXISTS新建表 索引 → 同一文件可省CONCURRENTLY但必须IF NOT EXISTSPrisma 生成的约束可用DROP CONSTRAINT IF EXISTS ..., ADD CONSTRAINT ...单语句替代 DO block现有表加列再建索引 → 先ADD COLUMN IF NOT EXISTS一文件再CONCURRENTLY索引一文件生成迁移后删除_BackgroundWorkerToBackgroundWorkerFile、_TaskRunToTaskRunTag、SecretStore_key_idx等噪音行CREATE TYPE/ADD CONSTRAINT/RENAME用带目录检查或EXCEPTION WHEN duplicate_object的 DO block 包裹数据INSERT加ON CONFLICT提交前跑pnpm --filter webapp run guard:migrations必要时用带原因的-- migration-guard: allow reason逃生绝不删除列/表新代码只面向 V2。在 Trigger.dev 这样的高并发任务编排系统中一次锁表或不可重跑的迁移就可能拖垮整条运行流水线。这套规则的价值正在于把安全从口头约定变成了机器可校验的代码让每次prisma migrate都有据可依、可审计、可重放。【免费下载链接】trigger.devTrigger.dev – build and deploy durable AI agents and workflows项目地址: https://gitcode.com/gh_mirrors/tr/trigger.dev创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进