返回博客

Drizzle ORM 的沉默丢字段:schema 漂移让写操作无声消失

某天排查一个工作流任务系统:连续几十次失败的任务运行,runs.error 列全是 NULL。代码里的 catch 块明明把错误信息传进去了,console.log(dbPatch) 也打出了正确的值,手动 UPDATE runs SET error='test' WHERE id=1 也写进去了。那条错误信息究竟去了哪里?

排查花了半个小时,最终发现根因只有四个字:schema 漂移

现象

  • runs.error 列在 DB 中确实存在(SHOW COLUMNS FROM runs 可以看到)
  • 代码 catch 块构建 { status: 'failed', error: msg, finishedAt: now() } 传给 patch helper
  • Helper 调用 Drizzle 的 client.update(runs).set(dbPatch).where(...) 后返回成功
  • 查 DB:status 写进去了,errorfinished_at 永远是 NULL
  • 没有任何异常、没有任何警告日志

根因

Drizzle ORM 的 update(table).set(patch) 在生成 SQL 时,只会把 patch 对象里同时出现在 drizzle schema 定义中的字段塞进 SET 子句。patch 里多余的 key 直接被忽略,不抛错,不警告。

这个行为是 Drizzle 有意为之的"宽容更新"设计——但它让 schema 漂移彻底隐形。

在这个案例中,runs.errorruns.finished_at 两列是几个月前通过原始 ALTER TABLE 手工加进去的,但 schema.ts 里的 mysqlTable('runs', { ... }) 块从未更新过。结果:

// schema.ts(漂移状态)
runs: mysqlTable('runs', {
  id: int('id').primaryKey(),
  status: varchar('status', { length: 32 }),
  // error 和 startedAt 根本不在这里!
})
-- Drizzle 实际生成的 SQL(error 和 finished_at 被静默丢弃)
UPDATE runs SET status = 'failed' WHERE id = ?
坑:手工 ALTER 加的列,Drizzle 不认识就当不存在

直接对数据库执行 ALTER TABLE 添加列,然后在代码里往 patch 对象里放了这个 key —— Drizzle 不会告诉你它忽略了这个 key。update().set() 顺利执行,返回值看起来没问题,但那个列的值在 DB 里永远不会变。

这个坑最难察觉的地方:手动 SQL 写入没问题(证明列可写)、代码不报错(证明逻辑通了)、测试通过(断言通常只看 status 之类的主要字段)。三个"正常"叠在一起,让人自然怀疑是编码、字符集、事务等其他问题。

正解

两步确认漂移

# 步骤 1:确认 DB 有这列
mysql -e "SHOW COLUMNS FROM runs" | grep error
 
# 步骤 2:确认 drizzle schema 没有这列
grep -A 30 "runs: mysqlTable" src/storage/schema.ts | grep error

步骤 1 有、步骤 2 没有 → 漂移确认,只需补 schema 声明,不需要新建 migration(列已经在 DB 里了)。

修复

// schema.ts(修复后)
runs: mysqlTable('runs', {
  id: int('id').primaryKey(),
  status: varchar('status', { length: 32 }),
  error: text('error'),               // 补上:之前被静默丢弃
  startedAt: timestamp('started_at'), // 补上:之前被静默丢弃
  finishedAt: timestamp('finished_at'),
})

如果项目同时支持多个 driver(MySQL + SQLite + PostgreSQL),每个 schema 文件都要补。Drizzle 的静默丢弃行为在所有 driver 中一致。

正解:只补 schema 声明,列已存在不需要 migration

mysqlTable / sqliteTable / pgTable 的列定义里加上缺失的列即可。Drizzle 从此会把这个 key 纳入 SET 子句,后续写入立即生效。历史行已经是 NULL 了,无法自动回填——那是丢失的数据。

长期预防

  • 所有 schema 变更走 drizzle-kit generate,禁止裸 ALTER TABLE
  • 每个 patch helper 写单测时断言目标列的值,不只断言"helper 没抛错"
  • 有条件的话,在应用启动时对比 SHOW COLUMNS 和 drizzle schema 的列集合,不一致就拒绝启动

一句话外卖

字段值永远是 NULL 但手动 SQL 能写进去?先查 drizzle schema,不是查代码逻辑。