35 KiB
ADR-0004:SQLite/PostgreSQL Repository、驱动与 Migration 策略
- 状态:Proposed
- 日期:2026-07-18
- 决策者:QingLong Maintainers
- 关联 RFC:QL-RFC-0001
- 前置决策:ADR-0001、ADR-0002
- 后续细化:ADR-0037
官方参考:
- Node.js 24 node:sqlite
- Drizzle Node SQLite
- Drizzle PostgreSQL
- Drizzle migration generate
- SQLite WAL
- SQLite Online Backup API
- PostgreSQL SELECT locking
1. 决策摘要
QingLong 3.0 的领域层只依赖 Repository port,不依赖 Sequelize、Drizzle、SQLite 或 PostgreSQL API。
最终持久化组合为:
| Profile | 数据库 | Query/Schema 工具 | Driver |
|---|---|---|---|
| edge | SQLite | Drizzle typed schema | Node.js node:sqlite |
| standalone | SQLite | Drizzle typed schema | Node.js node:sqlite |
| cluster-control | PostgreSQL | Drizzle typed schema | node-postgres |
| worker | 无控制面数据库 | 本地有界 spool 另行决定 | 不适用 |
新代码不得把 Drizzle query object 或 Sequelize Model 暴露到 service/domain 层。SQLite 与 PostgreSQL adapter 可以使用不同的 SQL 并发算法,但必须通过同一 Repository contract suite,产生相同的领域状态与错误语义。
迁移采用“生成、审查、提交、由应用 runner 执行”的方式:
- Drizzle Kit 只在开发/CI 中生成或校验 SQL。
- 生产环境不执行 drizzle-kit push。
- SQLite 与 PostgreSQL migration 都必须进入版本控制、具有稳定 ID 和 checksum。
- 应用继续使用单一 SchemaMigrations 历史表,不同时维护一套不可见的 Drizzle runtime migration 历史。
- 2.x SQLite 使用 baseline + incremental migration 原地升级;PostgreSQL 3.0 首版从空库 baseline 创建,不声称可以直接打开 2.x SQLite 文件。
- migration 前必须完成一致性备份,失败时停止启动,不能吞掉 schema error。
为避免“大爆炸”式替换,允许一个明确临时的 SequelizeRunRepository adapter:
- PR-1/PR-2 使用现有 Sequelize connection 实现 Run Repository,只存在于 adapter 层。
- Shadow Runtime 稳定后,按数据域迁移旧 Sequelize 调用。
- DrizzleSQLiteRunRepository 通过同一 contract suite 后替换临时 adapter。
- keyv.sqlite 完成迁移后,才能移除 sqlite3、Sequelize 和 @keyv/sqlite。
- 临时 adapter 不得成为插件 API,也不得新增领域层对 Sequelize 的依赖。
当前官方 Node 24 文档仍把 node:sqlite 标为 Stability 1.2 Release candidate,Drizzle 的 node:sqlite 文档也仍建议 RC 包。因此依赖必须锁定精确版本,并在 Alpha 期保留 Legacy adapter 回退;不能只依据“内置模块”三个字直接删除现有驱动。ADR-0063 已把 Drizzle RC 限制为 SQLite adapter 的开发期 schema 工具,edge/standalone production graph 不安装它。
2026-07-18 的包元数据审计进一步确认:drizzle-orm@0.45.2 稳定版没有导出 node-sqlite,该导出存在于 1.0.0-rc.4;@keyv/sqlite@4.0.8 稳定版仍依赖 sqlite3,而 6.0.0-beta.4 虽要求 Node >=22.18.0,仍直接依赖 better-sqlite3。因此本 ADR 接受目标组合,但不授权当前直接升级这些预览依赖。
2. 当前状态
2.1 主数据库
主数据库位于:
data/db/database.sqlite
当前 back/data/index.ts 创建 Sequelize 6 SQLite connection:
- sqlite3 实现来自 @whyour/sqlite3。
- pool max 5、min 2。
- SQLITE_BUSY 最多重试 10 次。
- transactionType 使用 IMMEDIATE。
- 各 service 直接调用 Model.findAll/findOne/create/update/destroy 等 API。
- 显式业务事务目前较少,但 Run 状态机将显著增加事务要求。
2.2 第二个 SQLite 数据库
back/shared/store.ts 通过 @keyv/sqlite 打开:
data/db/keyv.sqlite
其中至少保存:
- apps
- authInfo
- lang
因此仅迁移主数据库不能移除 sqlite3 原生依赖,也不能得到单文件一致备份。SQLite 官方说明,在 WAL 模式下涉及多个 ATTACH 数据库的事务只保证每个数据库单独原子,不保证多个数据库作为整体原子。
2.3 启动与迁移
现有启动顺序为:
model sync
-> explicit SchemaMigrations runner
-> initData
-> API/scheduler
PR-0 已将吞掉所有 ALTER 错误的逻辑替换为带 checksum 的显式 runner。当前 runner 仍使用 Sequelize QueryInterface,这是 2.x 兼容引导实现,不是 3.0 最终数据库边界。
2.4 多架构与低资源约束
当前镜像覆盖 amd64、arm/v6、arm/v7、arm64、ppc64le、s390x、386 等架构。最终采用 node:sqlite 虽可消除 npm native addon,但仍必须证明:
- 每个承诺架构的固定 Node 24 运行时实际包含 node:sqlite。
- Alpine/musl 与 Debian/glibc 上数据库行为一致。
- 旧宿主 seccomp、文件系统和 SQLite VFS 行为通过测试。
- DatabaseSync 的同步查询不会让低性能路由设备出现不可接受的 event-loop stall。
- Node 运行时镜像本身不会因升级而失去原有架构。
“没有 node-gyp”不等于“自动获得多架构兼容”。
2.5 旧库结构审计
对当前工作区 database.sqlite 的只读 introspection 发现,实际数据库可能同时包含:
- 当前 Sequelize model 管理的核心表。
- 3.0 孵化 migration 新增的 Run、Attempt、Event 与取消派发表。
- 历史版本、分支或外部扩展留下但当前 model 未声明的表,例如 Scenario、User、CronLog 类结构。
- 当前 model 未声明的兼容列,例如重复命名风格的 pinned 字段或 userId。
这说明“根据当前 model 或 Drizzle schema 重建整库”会有真实数据丢失风险。迁移必须使用 ownership manifest:只修改明确声明为 QingLong 3.0 所有的对象,unknown table/column/index 默认保留并由诊断报告。CI fixture 必须包含未知表和未知列,证明增量 migration 不会删除它们。
3. 目标
- 让 Run 状态机在 SQLite 与 PostgreSQL 上保持相同领域语义。
- 为 edge 保留单文件、低内存、零外部服务部署。
- 为 cluster-control 提供多副本 claim、锁和一致性能力。
- 通过 typed schema、受限动态查询和 reviewable migration 降低维护风险。
- 原地升级已有 2.x database.sqlite,不重命名历史表和字段。
- 最终移除 Sequelize、sqlite3 与 @keyv/sqlite 的运行时依赖。
- 在迁移期间保持每一步可测试、可观测、可回退。
- 数据库故障不能造成重复任务、状态和事件分离或静默 schema 漂移。
4. 非目标
- 不设计一个模拟 Sequelize Model API 的兼容 facade。
- 不强求 SQLite 与 PostgreSQL 使用完全相同的 SQL。
- 不允许 cluster-control 多副本共享 SQLite/NFS 文件。
- 不在 3.0 首版提供任意 SQLite 到 PostgreSQL 的在线双向复制。
- 不使用数据库触发器承载 Run 领域状态机。
- 不让插件获得任意 SQL connection。
- 不用 drizzle-kit push 修改生产数据库。
- 不把 database.sqlite 和 keyv.sqlite 的普通文件复制称为一致性在线备份。
5. 分层边界
建议目录边界:
back/runtime/domain/
Run state, commands, errors
back/runtime/ports/
RunRepository and transaction contracts
back/db/schema/sqlite/
Drizzle SQLite schema
back/db/schema/postgres/
Drizzle PostgreSQL schema
back/db/adapters/sqlite/
node:sqlite connections and repositories
back/db/adapters/postgres/
pg pool and repositories
back/db/adapters/legacy-sequelize/
temporary transition adapters
back/db/migrations/
reviewed manifests and dialect SQL
领域层使用 camelCase 与明确类型,数据库层负责 snake_case、JSON、boolean、timestamp 和 error 映射。
禁止:
- service 直接 import sqliteTable、pgTable、Op 或 Model。
- Repository 返回 ORM entity/proxy。
- API route 接收排序字段后直接拼 SQL identifier。
- adapter 以 any 绕过字段映射。
- 为“少改代码”实现 findAll/update 风格的通用仓储。
6. 为什么选择 Drizzle
选择 Drizzle 的目的不是隐藏 SQL,而是获得:
- TypeScript schema 与查询字段类型。
- SQLite/PostgreSQL 方言支持。
- 可生成并审查的 SQL migration。
- 明确表达索引、约束和字段映射。
- 避免继续把领域对象继承自 ORM Model。
官方文档确认 Drizzle 原生支持 node:sqlite,也支持 node-postgres。当前两条文档仍使用 RC 安装版本,因此必须:
- package.json 和 lockfile 锁定经过验证的精确版本。
- 禁止使用不受控的 latest、next 或浮动 rc tag。
- 每次升级重新生成 schema diff,并运行两个方言的 contract/migration suite。
- 在依赖进入 Stable 前保留版本升级评审项。
Drizzle 是 adapter 工具,不是 Repository contract。若未来替换 Drizzle,领域层不应变化。
6.1 依赖稳定性门槛
当前版本事实不能被文档示例替代:
| 包 | 2026-07-18 审计结果 | 决策 |
|---|---|---|
drizzle-orm@0.45.2 |
stable,但无 node-sqlite export |
不可用于目标 adapter |
drizzle-orm@1.0.0-rc.4 |
有 node-sqlite driver/session/migrator export |
只允许锁定 exact version 的不可达 spike,接受前不得接生产流量 |
@keyv/sqlite@4.0.8 |
依赖 sqlite3,Keyv peer 为 5.x |
迁移窗口继续保留,不宣称已移除 native addon |
@keyv/sqlite@6.0.0-beta.4 |
依赖 better-sqlite3,Keyv peer 为 6 beta |
不作为移除 native addon 的路径,也不直接升级生产数据 |
进入实现时必须重新查询并记录版本状态;若 stable 已支持 node-sqlite,优先重新评审 stable。若仍只能使用 RC,spike 必须同时锁定 ORM、Kit 和 Node patch,lockfile 不允许浮动 tag。
7. SQLite Adapter
7.1 连接模型
edge/standalone 只允许 ql-core 主进程持有控制面数据库写连接:
- 使用一个 DatabaseSync connection。
- enableForeignKeyConstraints 为 true。
- enableDoubleQuotedStringLiterals 为 false。
- allowExtension 为 false。
- allowUnknownNamedParameters 为 false。
- defensive 为 true。
- busy timeout 使用显式、可配置、有限值。
- 关闭时先停止新请求、完成有界事务,再关闭连接。
Node.js 官方文档说明 DatabaseSync 的 API 全部同步执行。初始实现允许在主线程使用,但必须满足:
- 请求路径只执行有索引、结果有上限的短查询。
- migration、backup、integrity_check、VACUUM 和大批量清理不在服务请求中执行。
- 慢查询与 event-loop lag 有指标。
- edge 基准不通过时,必须把 SQLite adapter 移入专用 Worker Thread;不能通过放宽延迟门禁掩盖阻塞。
专用 Worker Thread 不是默认值,因为它会增加低内存设备的常驻开销和 RPC 复杂度。
7.2 写事务
SQLite 写命令使用短 BEGIN IMMEDIATE 事务:
- 读取 Run 与 version。
- 验证状态转换。
- 条件更新 Run/version/event_sequence。
- 插入 RunEvent。
- 必要时更新 Legacy projection。
- 立即提交。
事务中禁止:
- spawn。
- 网络请求。
- 文件上传。
- 等待 Worker。
- 模型调用。
- 大日志写入。
- 无界循环或分页扫描。
SQLITE_BUSY 只对明确可重试、尚未产生外部副作用的事务进行有界退避。达到上限后返回统一 RepositoryBusyError,由调用者决定重试,不在 adapter 内无限等待。
7.3 Queue claim
SQLite 不模拟 PostgreSQL SKIP LOCKED。单 ql-core writer 使用:
- BEGIN IMMEDIATE。
- 以 priority、queued_at_ms、id 的确定顺序选择一个或有界批次 queued Run。
- 以 status + version 条件更新为 dispatching。
- 创建 Attempt/Event。
- COMMIT。
SQLite Profile 不支持多个控制面副本同时 claim。同一部署若检测到第二个 ql-core writer,启动必须失败。
7.4 Journal mode
不全局硬编码 WAL:
- edge 初始默认沿用 rollback journal,减少额外文件与 checkpoint 不确定性。
- standalone 可以显式启用 WAL,并在本地文件系统基准通过后成为建议值。
- 网络文件系统、共享卷或无法确认 VFS shared-memory 能力时禁止 WAL。
- cluster-control 不使用 SQLite。
SQLite 官方说明 WAL 支持读写并发,但不能用于网络文件系统,仍只有一个 writer,并需要管理 checkpoint 与 WAL 增长。任何默认值变化都必须测量:
- 空闲和任务运行时写入量。
- WAL/shm 峰值。
- checkpoint 延迟。
- 断电恢复。
- 闪存写放大。
- SQLITE_BUSY 比例。
7.5 时间和整数
- 时间统一存 epoch milliseconds。
- 当前可预见的 epoch milliseconds 与事件 sequence 必须处于 JavaScript safe integer 范围。
- adapter 显式决定 number/BigInt 转换,禁止依赖驱动隐式行为。
- 若字段可能超过 safe integer,领域类型必须使用 bigint 或字符串,不静默截断。
8. PostgreSQL Adapter
8.1 Driver 与 pool
cluster-control 使用 Drizzle node-postgres adapter 与 pg Pool:
- pool 大小由 Profile 资源预算和副本数共同决定。
- 每个请求/事务必须有 timeout。
- edge 构建不得初始化或连接 PostgreSQL。
- 连接失败不得自动降级到本地 SQLite。
- schema 使用 PostgreSQL 原生 boolean、jsonb 和 bigint,但映射到同一领域类型。
选择 node-postgres 而非 postgres.js 的首要原因是显式 pool 生命周期、成熟生态和按 query 配置类型解析。最终精确版本同样必须锁定。
8.2 Queue claim
多副本 claim 使用 PostgreSQL 行锁语义:
- 候选查询具有完整、唯一的 ORDER BY。
- 使用 FOR UPDATE SKIP LOCKED 获取有界批次。
- 在同一事务创建 Attempt、更新 Run、递增 version/sequence、追加 Event。
- Worker lease/fencing 仍按 ADR-0009 实现,不能只依赖数据库行锁。
- 隔离级别与 serialization/deadlock 错误映射为有限重试。
PostgreSQL 官方文档明确指出 SKIP LOCKED 会给出不一致视图,不适合通用查询,但适合多个消费者访问 queue-like table。本项目只在 claim 端口内使用,不用于普通 Run 列表。
8.3 Migration lock
cluster-control 多副本启动时只能有一个 migration leader:
- 通过 PostgreSQL advisory lock 或部署级 migration Job 获得排他权。
- 非 leader 等待有界时间并重新检查 schema version。
- migration 失败时所有新版本副本保持 not-ready。
- 不允许多个副本同时自动执行 DDL。
具体 advisory lock key、超时和部署 Job 由实现 ADR/PR 决定。
9. 跨方言语义契约
9.1 必须相同
SQLite 与 PostgreSQL 必须对以下行为给出相同结果:
- Run/Attempt/Event 创建。
- 合法和非法状态转换。
- version CAS 冲突。
- Event sequence 单调和唯一。
- dedupe key 幂等。
- terminal state 不可覆盖。
- cancel 与 exit 并发。
- idempotency key。
- 分页顺序。
- 错误码和可重试分类。
- task snapshot 与 Secret 规则。
9.2 允许不同
以下实现可以按方言不同:
- claim locking SQL。
- busy/deadlock/serialization retry。
- JSON 存储为 TEXT/JSON 或 jsonb。
- boolean 存储为 integer 或 boolean。
- migration DDL。
- pool/connection 数。
- journal/checkpoint。
- 索引实现细节。
9.3 统一错误
Repository adapter 至少映射:
RepositoryBusyError
VersionConflictError
DuplicateIdempotencyKeyError
ConstraintViolationError
MigrationChecksumError
DatabaseUnavailableError
SerializationRetryExhaustedError
领域服务不得解析 SQLite 文本错误或 PostgreSQL SQLSTATE 来决定状态机行为。
10. Repository Transaction Contract
RunRepository 的 mutation API 必须强制事务上下文。目标形态:
repository.transaction(async (tx) => {
const run = await tx.findRunForUpdate(runId);
const decision = transition(run, command);
await tx.compareAndSetRun(decision);
await tx.appendEvent(decision.event);
await tx.updateLegacyProjection(decision.projection);
});
要求:
- appendEvent 不提供绕过 transaction 的 public mutation API。
- compareAndSetRun 必须携带 expectedVersion。
- event sequence 在事务内分配。
- adapter 不能自行决定合法状态转换。
- transaction callback 不能泄漏到事务结束后使用。
- 所有 adapter 运行同一 contract suite。
- 只读列表有最大 limit 和稳定 cursor,不提供任意 offset 全表扫描作为默认 API。
11. Schema 与 Migration
11.1 Source of truth
TypeScript Drizzle schema 是目标结构的 typed source;提交到仓库的 migration SQL/manifest 是生产变更的审计事实。两者必须由 CI 检查一致,不能只保留其中一个。
SQLite 与 PostgreSQL 分别维护 schema 文件和生成配置,因为:
- 自增、boolean、JSON、partial index 和锁语义不同。
- 强行共享一个方言 schema 会隐藏真实差异。
- 领域 contract,而不是相同 DDL,保证可移植性。
11.2 生成和审查
每次 schema 变更:
- 修改两个方言 schema。
- 使用锁定版本的 Drizzle Kit generate 或 custom migration 生成候选。
- Maintainer 审查 SQL、锁范围、表重建、索引和数据转换。
- 为旧版 fixture 增加 migration test。
- 运行空库、升级、重复执行、失败恢复和 downgrade-read 测试。
- 将 migration 与 checksum 提交。
- 应用 runner 执行,不在生产调用 Kit CLI。
Drizzle 官方支持生成普通或 custom migration,也允许外部 runner 执行生成结果。QingLong 使用这一模式,不使用 push。
11.3 Baseline
2.x SQLite:
- 保留现有表名和字段名。
- 使用 introspection 区分已有 baseline 与待增量字段。
- SchemaMigrations 记录已确认的 baseline/incremental migration。
- 不对已有生产库执行全量 CREATE 或 blind push。
- baseline manifest 只声明项目拥有的 required table/column/index;未知对象不参与 destructive diff,默认保留。
- 旧库异常必须明确报错并提供修复说明,不能 catch 后继续。
PostgreSQL:
- 3.0 首版只支持空库 baseline 或受支持的 3.x migration。
- SQLite 到 PostgreSQL 的导入是独立、可校验、可回滚的离线工具,不是应用启动隐式行为。
11.4 Checksum
- migration ID 永久唯一。
- checksum 覆盖该 migration stream 的方言 SQL、数据转换代码和 manifest;ADR-0037 冻结现有 SQLite
0001–0024,PostgreSQL 使用独立pg-*stream,通过 schema contract version 表达跨方言逻辑兼容,禁止为追加另一方言内容而修改已应用 checksum。 - 已应用 migration 内容不得修改;修复使用新 migration。
- 当前未发布的 0001/0002 可以在 next Alpha 前调整,一旦预发布即冻结。
- checksum mismatch 阻止启动并输出 migration ID,不自动覆盖数据库记录。
11.5 Schema drift
启动时只验证 migration history 和必要 capability,不做全库昂贵 diff。CI/诊断命令负责完整 schema drift 检查。
next 的正式 Local 3.0 只读诊断必须显式选择数据库与部署 Profile:
pnpm audit:schema:ql3 -- --database=/opt/qinglong3/qinglong3.sqlite --profile=edge
它在 Node 24 中复用 @qinglong/local-sqlite/readiness-inspection,以 defensive、query-only
node:sqlite 验证正式 migration checksum/history、capability、required schema、foreign key、
quick-check 和 repository integrity,同时要求 edge=DELETE、standalone=WAL。数据库必须是
当前 UID 的 canonical 0600 regular file;结果不含路径或业务数据,也不执行自动修复。
历史 back/migrations ownership drift 工具保留为显式 legacy/Shadow 命令:
pnpm audit:legacy-schema:ql3 -- --database=/absolute/legacy.sqlite --json
它不再默认打开 data/db/database.sqlite。报告依据 legacy ownership manifest 区分 missing owned、
unmanaged 和 unknown objects;--fail-on-drift 可让 CI 严格拒绝未知对象。该报告不能作为
fresh/adopted 3.0 readiness 证据。
生产禁止:
- sync alter。
- drizzle-kit push。
- 自动 drop/recreate。
- 忽略未知列或约束错误。
- 在没有备份时执行破坏性重建。
11.6 单一 migration 历史
Drizzle schema 是 typed query/schema source,Drizzle Kit 是开发期候选 SQL 生成器;现有应用 runner 是生产 migration authority。两者不能各自写一套互不知情的历史表:
- Kit 生成的 SQL 先经过人工审查和兼容 fixture 验证。
- 审查后的方言 SQL、数据转换和 ownership manifest 共同计算 checksum。
- 应用 runner 以现有
SchemaMigrations记录稳定 ID、checksum 和应用时间。 - 已有
0001至0005继续属于同一线性历史,不重新标记为 Drizzle baseline,也不静默跳过。 - 运行时不得调用 Kit CLI;诊断可比较 schema,但默认不执行修复。
12. Legacy Sequelize 迁移
12.1 临时 Run adapter
为了先验证 Run Runtime,允许实现 SequelizeRunRepository:
- 位于 legacy-sequelize adapter 目录。
- 使用现有 sequelize connection 和 transaction。
- 返回纯 RunRecord/Attempt/Event 类型。
- 实现 CAS、sequence、dedupe 和错误映射。
- 通过 SQLite RunRepository contract suite。
- 默认只服务 off/shadow 阶段。
- 文件头和 tracking issue 明确删除条件。
它不能:
- 让 Runtime service import Sequelize。
- 暴露 Model。
- 成为 Package/plugin SDK。
- 为新功能扩展通用 Sequelize 基础设施。
- 阻止后续 Drizzle adapter 替换。
这不是最终方向,而是避免在 Run Runtime 验证前一次性迁移全部旧 CRUD 的风险隔离层。
12.2 数据域迁移顺序
建议顺序:
- Run/RunAttempt/RunEvent。
- App/System/Auth/CronView。
- Dependence/Env/Subscription。
- Crontab/RunningInstance/CronStats。
- Keyv store。
- 删除 Sequelize/sqlite3/@keyv/sqlite。
每一组:
- 建立 Repository port 或明确 service query。
- 增加旧库 fixture 与 contract test。
- 切换读取。
- 切换写入。
- 观察一个版本窗口。
- 删除旧调用。
禁止构造一个兼容 Model.findAll/Op 的新 facade。
13. Keyv 数据迁移
keyv.sqlite 必须进入迁移范围,否则:
- native sqlite3 依赖仍存在。
- 备份仍跨两个数据库。
- Auth/App/Language 状态与主库无法原子更新。
目标:
- authInfo 进入明确的 auth/system typed table。
- apps 以现有 Apps 表或新的 typed projection 为事实源。
- lang 进入系统设置表。
- 通用短期 KV 如仍需要,使用主 database.sqlite 的 KeyValueStore 表,带 namespace、version、updated_at 和可选 expires_at。
迁移步骤:
- 只读解析 keyv.sqlite。
- 校验 key、JSON shape 和主库冲突。
- 在主库事务写入。
- 写 migration marker 与数据摘要。
- read-through 验证一个窗口。
- 停止写 keyv.sqlite。
- 备份并保留旧文件一个弃用周期。
- 移除 @keyv/sqlite。
在两个数据库并存期间,升级备份 manifest 必须同时列出两个文件,并在短暂停写窗口获取一致版本;不能声称普通并行复制是原子快照。
@keyv/sqlite v6 beta 当前仍引入 better-sqlite3,且其 namespace、TTL 和表结构能力相对现有 v4 有变化,因此不使用“直接升级 v6”替代上述数据迁移。若未来稳定版提供纯 node:sqlite 路径,也必须先在 key/value serialization、namespace、TTL、并发和旧 keyv(key,value) 表 fixture 上通过兼容测试,不能自动接受 schema migration。
14. Backup 与 Restore
14.1 SQLite
升级前使用 SQLite Online Backup API,而不是直接 cp 正在写入的文件。Node 24 node:sqlite 提供 backup(sourceDb, path),返回 Promise 并支持分批进度。
流程:
- 拒绝新的 mutation,等待有界事务完成。
- 在线备份到同文件系统临时路径。
- 对备份执行 quick_check/integrity_check。
- 写 manifest:源版本、migration IDs、大小、hash、时间、Node/SQLite 版本。
next 当前增加了两个仅在 Node 24 执行的兼容 spike:Online Backup/restore 测试使用分批 backup() 复制 WAL 源库,在备份过程中从同一连接追加事实,随后对备份执行 integrity_check、restore、quick_check、行数与 SHA-256 校验;schema 测试先由当前 Sequelize runner 执行 0002 至 0005,再用启用 defensive、关闭 extension 且 read-only 的 DatabaseSync 打开同一文件,核对表列、完整性和拒绝写入。二者已在 macOS arm64 的临时 Node 24.18.0 进程实跑通过,Node 20/22 测试明确 skip;CI 和 Linux 多架构未实际跑通前不能把它记为发布能力。该 spike 也不替代正式的停写协调、双数据库 manifest、权限和容量检查。
5. fsync 文件与目录后原子 rename。
6. 运行 migration。
7. 启动后执行核心读写 smoke test。
8. 失败时停止服务并给出 restore 命令,不在运行中覆盖原库。
备份保留策略考虑小容量路由设备:
- 先检查可用空间。
- 峰值空间预算至少包含原库、WAL/journal、临时备份和 migration 重建。
- 超出预算时拒绝升级并明确原因。
- 自动保留数量有上限。
14.2 PostgreSQL
应用不自行复制 PostgreSQL 数据目录。集群部署依赖:
- 平台 snapshot、pg_dump/pg_restore 或托管备份。
- migration 前 backup precondition hook。
- 恢复演练和 RPO/RTO 由部署文档定义。
14.3 Restore 兼容
forward migration 默认不自动 down。应用回滚版本必须能:
- 忽略新增表/可空列。
- 在 schema capability 不兼容时拒绝启动。
- 通过备份恢复到旧 schema,而不是尝试逆向猜测数据转换。
15. 资源与性能门禁
15.1 edge/standalone
至少测量:
- 打开 connection 后额外 RSS。
- 空闲 event-loop lag。
- Run create + two Event transaction p50/p95/p99。
- 1、10 个并发 API 请求下的 stall。
- 100、1000、10000 个 Run 查询。
- migration 峰值 RSS、时间、临时磁盘。
- backup 吞吐和 API 延迟。
- rollback journal 与 WAL 的写入量。
- 意外断电/kill -9 后恢复。
- SQLITE_BUSY 与 retry 次数。
所有 query 必须有 statement timeout 或可证明的 bounded input。SQLite 同步 API 的 p99 超出预算时,评估 Worker Thread,而不是增加并发 connection。
next 提供 pnpm benchmark:db:node-sqlite:在 Node 24 上使用 rollback journal、synchronous=FULL 和短 BEGIN IMMEDIATE 事务写入一条 Run 与两条 Event,报告 transaction p50/p95/p99、最大同步 batch、RSS、文件大小和 integrity_check。它只测 runtime/host 边界,不是生产手写 SQL adapter,也不能替代固定路由设备基准。
15.2 cluster-control
至少测量:
- 多副本 claim 吞吐。
- 重复 claim 为零。
- lock wait、deadlock、serialization retry。
- pool saturation。
- primary/replica failover。
- migration leader 竞争。
- 连接断开时的 lost/lease 协调。
16. Security
- 所有值使用参数绑定。
- 动态排序字段通过 schema-column allowlist。
- SQLite extension 永久默认关闭。
- defensive mode 开启。
- 数据库文件与备份权限不宽于现有 data 目录。
- PostgreSQL 使用最小权限账号;runtime 与 migration 账号可分离。
- callback token hash、Secret ref 与加密材料不进入普通 query log。
- 生产默认不记录完整 SQL 参数。
- 插件只能调用受 Policy 控制的领域 API,不能获取 db handle。
- migration SQL 属于受审代码,禁止远程 Package 注入。
17. Rollout
Stage A:当前基线
- 显式 SchemaMigrations runner。
- 0001 Legacy columns。
- 0002 Run schema。
- Sequelize 仍为唯一 active driver。
Stage B:Run Runtime 验证
- 实现临时 SequelizeRunRepository。
- 完成纯状态机与 Repository contract。
- 只启用 manual shadow。
- 不引入第二个 SQLite connection。
Stage C:Drizzle/node:sqlite spike
- 锁定 Node 24 和 Drizzle 精确版本。
- 建立 Drizzle SQLite schema。
- 在 fixture copy 上验证与 0001 至 0005 schema 完全兼容,并证明未知表、列和索引保持不变。
- 完成 Node 24 多架构、seccomp、WAL、backup、event-loop 基准。
- 通过显式 test factory 注入,不修改生产默认 driver,不接生产流量,也不对同一 live database 启用双写。
next 的 ADR-0063 已完成一个面向全新 3.0 数据库的独立 @qinglong/local-sqlite vertical slice:Node 24 DatabaseSync、typed schema、reviewed migration、readiness、完整 RunRepository contract 与 edge/standalone storage-only 组合均已通过本机测试。该实现没有接管 legacy database.sqlite,也不满足本 Stage 对旧 fixture、backup、全架构和物理 edge 基准的全部要求,因此仍属于 Incubating,而不是 Stage D 生产切换。
Stage D:SQLite adapter 切换
- DrizzleSQLiteRunRepository 通过相同 contract suite。
- 小范围 Shadow 切换。
- 按数据域迁移旧查询。
- 同一进程禁止长期并存两个写 connection;切换必须明确 connection owner。
Stage E:PostgreSQL cluster
- 按 ADR-0037 建立独立
pg-*PostgreSQL schema/migration stream,不修改 SQLite migration checksum。 - DrizzlePostgresRunRepository 通过相同 contract。
- 多副本 claim/lease/fencing 测试通过。
- cluster-control Profile 才能启用。
Stage F:移除 Legacy DB stack
只有同时满足:
- 所有 Sequelize import 清零。
- keyv.sqlite 数据迁移完成。
- sqlite3/@keyv/sqlite 无运行时调用。
- 支持架构 Node 24 镜像与恢复测试通过。
- 至少一个预发布观察窗口无阻塞问题。
才移除依赖。
18. 进入 Drizzle 实现的门禁
- Maintainer 接受本 ADR。
- 选择并锁定 exact Drizzle ORM/Kit 版本。
- 明确 Node 24 exact patch,记录 node:sqlite stability 状态。
- node:sqlite 在全部承诺架构上启动和读写通过。
- database.sqlite fixture introspection 完成。
- 0001/0002 与 Drizzle schema diff 为零或有已审查解释。
- backup/restore spike 通过。
- DatabaseSync event-loop benchmark 在 edge 预算内。
- 临时 Sequelize adapter 的删除计划和 owner 明确。
- 不使用 drizzle-kit push。
- ownership manifest 与包含未知表/列/索引的 fixture preservation test 通过。
- D-14/D-16 的正式支持架构冲突已经由 Maintainers 选择并记录,不能用本机单架构测试代替。
19. 被拒绝的方案
19.1 直接把 sqlite3 换成 better-sqlite3
Sequelize 6 与 @keyv/sqlite 依赖 sqlite3 风格 API,driver-only 替换不能解决 ORM/Keyv 迁移,也保留 native addon 的 ABI/架构风险。
19.2 只迁移 Run 表,永久保留 Sequelize
这会形成两套长期数据访问范式,继续携带 sqlite3 和 Keyv 原生依赖,违背 typed schema 与多架构目标。
19.3 先写一个 ORM 兼容 facade
模拟 findAll、Op 和 Model 会把旧抽象永久复制到新系统,动态查询仍难以约束,后续维护更差。
19.4 所有 Profile 都用 PostgreSQL
会破坏路由、NAS 和单机用户的零外部服务部署路径。
19.5 cluster 多副本共享 SQLite 卷
WAL 不能跨网络文件系统提供所需 shared memory 语义,SQLite 也只有一个 writer,不满足控制面多副本一致 claim。
19.6 SQLite 与 PostgreSQL 强制共享一份 DDL
两种数据库的 JSON、boolean、索引、锁和 migration 行为不同。共享领域 contract,分别维护方言 schema 更诚实、更可测。
19.7 生产 drizzle-kit push
push 会直接比较并修改 live schema,不符合已有用户库需要的 review、backup、checksum 和可重复升级要求。
19.8 直接 cp 活跃 SQLite 文件
活跃数据库可能处于写入/WAL 状态,普通复制不能替代 Online Backup API 和一致性检查。
19.9 同时打开 Sequelize sqlite3 与 node:sqlite 长期双写
两个连接栈会增加锁竞争、SQLite 版本差异和状态不一致风险。过渡期优先使用同一 Sequelize connection;最终以明确 owner 切换到 node:sqlite。
19.10 直接升级到 @keyv/sqlite v6 beta
当前 beta 仍直接依赖 better-sqlite3,不能消除原生 addon 风险,同时带来 Keyv major、schema 和序列化迁移。它不满足“低风险移除 sqlite3”的目标。
20. 影响
正面
- 新 Runtime 不再绑定 ORM。
- edge 与 cluster 共享领域语义但不伪装 SQL 相同。
- 依赖替换可按数据域推进。
- 现有用户库、Keyv 和多架构风险都进入正式计划。
- backup 与 migration 成为发布门禁。
- 临时 adapter 允许更早验证 Run 状态机。
负面
- 迁移期存在额外 adapter 和 contract tests。
- SQLite/PostgreSQL 需要维护两份 schema/migration。
- node:sqlite 与 Drizzle 当前仍有 RC 风险。
- DatabaseSync 需要严密 event-loop 性能门禁。
- 移除 sqlite3 必须等 Keyv 和全部 Legacy CRUD 完成,周期较长。
21. 验证矩阵
| 场景 | SQLite | PostgreSQL |
|---|---|---|
| 空库 migration | 必须 | 必须 |
| 2.x 原地升级 | 必须 | 不适用 |
| migration 重复执行 | 必须 | 必须 |
| checksum mismatch | 必须失败 | 必须失败 |
| Run/Event 原子事务 | 必须 | 必须 |
| CAS 并发冲突 | 必须 | 必须 |
| dedupe callback | 必须 | 必须 |
| queue claim | 单 writer | 多副本 SKIP LOCKED |
| cancel/exit race | 必须 | 必须 |
| backup/restore | Online Backup | 平台/pg 工具 |
| 断电/进程崩溃 | 必须 | failover |
| edge 资源预算 | 必须 | 不适用 |
| 多架构镜像 | 必须 | adapter/client 必须 |
22. 接受标准
- 接受 Repository port 为领域唯一数据库边界。
- 接受 edge/standalone 使用 SQLite、cluster-control 使用 PostgreSQL。
- 接受最终 SQLite 为 Drizzle + node:sqlite,PostgreSQL 为 Drizzle + node-postgres。
- 接受方言 schema/SQL 分开、领域 contract 统一。
- 接受生产禁用 drizzle-kit push。
- 接受 baseline + incremental migration 与单一 SchemaMigrations 历史。
- 接受 keyv.sqlite 必须迁移后才能移除 sqlite3。
- 接受临时 SequelizeRunRepository 只作为受限过渡 adapter。
- 接受 DatabaseSync 同步阻塞、多架构和 RC 状态属于发布门禁。
- 接受 SQLite Online Backup 与 PostgreSQL 外部备份分别实现。
- 接受 ownership manifest 与 unknown-object preservation 是旧库原地升级的强制边界。
- 接受当前不以
@keyv/sqlitev6 beta 替代正式 Keyv 数据迁移。