全部文章

指南 / 2026-09-24

以后从 Cloudflare D1 迁到 Postgres:哪些能交给 AI 翻译,哪些不能

用 D1(SQLite)起步,并不意味着永远被锁死。把 Drizzle 应用切到 Postgres,大部分是机械活,AI 做得很好;真正的工作是那些悄悄变掉的行为,以及你已有的数据。一份迁移清单。

以后从 Cloudflare D1 迁到 Postgres:哪些能交给 AI 翻译,哪些不能

在 Cloudflare D1 上做 SaaS,最常见的担心是:万一以后 SQLite 不够用了怎么办? 这个担心很合理:D1 单库上限 10 GB,没有交互式事务,也没有 Postgres 的各种扩展。ShipKit 为什么还是从 D1 起步我们单独写过。这篇讲的是退路:如果真有一天要把 Drizzle 应用从 D1 迁到 Postgres,要做些什么。

先说清楚:我们还没有发布过 Postgres 版的 ShipKit。 下面是我们会照着做的清单,依据是模板的真实代码。简短的回答是:代码是容易的那一半,难的那一半是不报任何错就悄悄变掉的行为,以及你已经有的数据。

AI 做得好的部分

用 Drizzle 的话,schema 就是 TypeScript,所以换数据库一开始就是个翻译活。只要代码库足够规整,Claude Code、Codex 这类 AI 就能稳定完成。ShipKit 就是这样:数据库客户端只在一个文件里创建,每个功能的表都在自己的 schema.ts 里,所有表遵循同一套约定。机械性的步骤有:

  • Schema。 把每个 schema 文件从 sqlite-core 改成 pg-core:sqliteTable 改成 pgTable,每一列换成对应的 Postgres 类型。
  • 默认值。 像 cast(unixepoch('subsecond') * 1000 as integer) 这样的 SQLite 默认值改成 now()。
  • 日期函数。 原生 SQL 里用到 SQLite 的 date(x / 1000, 'unixepoch') 的地方,改成 date_trunc('day', x)。ShipKit 的统计模块里恰好只有一个这样的函数。
  • 事务。 D1 没有交互式事务,所以需要多条写入一起成功的代码用的是 batch();到了 Postgres,这些都可以改成真正的 db.transaction(),算是升级。
  • 登录。 把 Better Auth 的 Drizzle 适配器指向 pg,重新生成它的表。
  • 迁移文件。 不要去翻译旧的 SQLite 迁移文件,直接从新 schema 生成一个全新的 Postgres 基线。
  • 连接。 如果还在 Workers 上,通过 Cloudflare Hyperdrive 连接,它包含在 Workers 套餐里;不在 Workers 上,就用普通的带连接池的客户端。

这些加起来是 AI 几个小时的工作量,中途出的小错大部分会被 tsc 抓住。

能编译通过、但结果是错的部分

下面这些改动不会产生类型错误,往往也不会让测试失败。正因为有它们,"AI 翻译完了"不等于"能用了"。

LIKE 开始区分大小写。 SQLite 的 LIKE 对英文字母不区分大小写,Postgres 的区分。ShipKit 的后台搜索用的就是 LIKE,直接翻译过去以后,搜 "john" 就找不到 "John" 了。不报错,只是结果变少。每一处 LIKE 都要做个决定:改成 ILIKE,或者两边都套上 lower()。

NULL 排到了另一头。 升序排列时,SQLite 把 NULL 放在最前,Postgres 放在最后(降序反过来)。按"最近登录时间"或"付款时间"排序的列表,顺序会悄悄变掉。凡是顺序重要的地方,都要显式写上 NULLS FIRST/LAST。

类型变严格了。 SQLite 没有真正的布尔和时间戳类型:ShipKit 把时间戳存成整数毫秒,把布尔存成 0/1,Drizzle 的 mode 选项替你的代码把这些细节藏了起来。到了 Postgres,你会想用原生的 timestamptz 和 boolean,然后要检查原生 SQL 里每一处对这些值做比较、分组或运算的地方。

SQLite 能容忍脏数据,Postgres 不能。 SQLite 的类型亲和性允许一个整数列里存着文本值,Postgres 会直接拒绝。这种问题你会在导入数据时发现,而不是在迁移代码时。

本地开发的方式变了。 用 D1 时,本地开发什么都不用配。换成 Postgres,本地要跑一个数据库(Docker,或者托管服务上的一个分支),种子数据、e2e 测试和 CI 里重置数据的方式也都要重新搭。

如果已经有用户:迁移数据

新项目没有数据,到上一步就结束了。线上产品的话,这一步决定了切换顺不顺利。

  1. 导出。 wrangler d1 export <db> --remote --output=dump.sql 会把数据库导出成 SQL。Cloudflare 文档提醒了两点:导出过程中会阻塞这个数据库的其他请求;非常大的整数可能丢失精度,因为数值要经过 JavaScript 的数字类型。
  2. 转换。 导出的是 SQLite 语法和 SQLite 的值:整数毫秒要转成时间戳,0/1 要转成布尔。写一个小脚本读取导出文件、按 Postgres 的类型写出每一行,比指望通用转换工具猜中你的约定要稳妥。
  3. 导入并核对。 导入 Postgres 后,逐表核对行数,并且把两边的金额列加总对一下。订单总额对不上,就说明哪里转错了。
  4. 切换。 把应用切到只读或维护模式,做最后一次导出导入,切换连接,然后盯着错误日志。

转换脚本和核对查询都可以让 AI 来写,但必须有人读一遍、亲手核对总数。

那到底要多久?

  • 还没有线上数据: 一两天左右:翻译,加上把上面那份"悄悄变掉的行为"清单过一遍,再重跑一遍测试。
  • 线上产品: 几天,大部分时间花在数据迁移和核对上,而不是代码。

这就是退路的真实成本:不小,但有边界。这也是为什么 ShipKit 只做一种数据库,而没有试图两边都支持:一套干净的代码,比两套都维护得半吊子的代码更容易迁移。如果你已经确定需要 Postgres,那就直接从 Postgres 模板起步,我们的 TanStack Start 模板对比里列了好几个。

如果 D1 能撑过你的第一年(对大多数新 SaaS 来说都能),ShipKit 把剩下的都给你准备好了:登录、支付、后台管理、国际化,以及防止 AI 把它们改坏的规则和测试。

更多文章