# ADR-0004:SQLite/PostgreSQL Repository、驱动与 Migration 策略 - 状态:Proposed - 日期:2026-07-18 - 决策者:QingLong Maintainers - 关联 RFC:[QL-RFC-0001](../QINGLONG_3_0_ARCHITECTURE_RFC.md) - 前置决策:[ADR-0001](./ADR-0001-run-state-and-transaction-boundaries.md)、[ADR-0002](./ADR-0002-legacy-crontab-compatibility-and-shadow-write.md) - 后续细化:[ADR-0037](./ADR-0037-postgresql-cluster-control-storage-incubation.md) 官方参考: - [Node.js 24 node:sqlite](https://nodejs.org/download/release/latest-v24.x/docs/api/sqlite.html) - [Drizzle Node SQLite](https://orm.drizzle.team/docs/sqlite/connect-node-sqlite) - [Drizzle PostgreSQL](https://orm.drizzle.team/docs/get-started-postgresql) - [Drizzle migration generate](https://orm.drizzle.team/docs/drizzle-kit-generate) - [SQLite WAL](https://www.sqlite.org/wal.html) - [SQLite Online Backup API](https://www.sqlite.org/backup.html) - [PostgreSQL SELECT locking](https://www.postgresql.org/docs/current/sql-select.html) ## 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: 1. PR-1/PR-2 使用现有 Sequelize connection 实现 Run Repository,只存在于 adapter 层。 2. Shadow Runtime 稳定后,按数据域迁移旧 Sequelize 调用。 3. DrizzleSQLiteRunRepository 通过同一 contract suite 后替换临时 adapter。 4. keyv.sqlite 完成迁移后,才能移除 sqlite3、Sequelize 和 @keyv/sqlite。 5. 临时 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. 目标 1. 让 Run 状态机在 SQLite 与 PostgreSQL 上保持相同领域语义。 2. 为 edge 保留单文件、低内存、零外部服务部署。 3. 为 cluster-control 提供多副本 claim、锁和一致性能力。 4. 通过 typed schema、受限动态查询和 reviewable migration 降低维护风险。 5. 原地升级已有 2.x database.sqlite,不重命名历史表和字段。 6. 最终移除 Sequelize、sqlite3 与 @keyv/sqlite 的运行时依赖。 7. 在迁移期间保持每一步可测试、可观测、可回退。 8. 数据库故障不能造成重复任务、状态和事件分离或静默 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 事务: 1. 读取 Run 与 version。 2. 验证状态转换。 3. 条件更新 Run/version/event_sequence。 4. 插入 RunEvent。 5. 必要时更新 Legacy projection。 6. 立即提交。 事务中禁止: - spawn。 - 网络请求。 - 文件上传。 - 等待 Worker。 - 模型调用。 - 大日志写入。 - 无界循环或分页扫描。 SQLITE_BUSY 只对明确可重试、尚未产生外部副作用的事务进行有界退避。达到上限后返回统一 RepositoryBusyError,由调用者决定重试,不在 adapter 内无限等待。 ### 7.3 Queue claim SQLite 不模拟 PostgreSQL SKIP LOCKED。单 ql-core writer 使用: 1. BEGIN IMMEDIATE。 2. 以 priority、queued_at_ms、id 的确定顺序选择一个或有界批次 queued Run。 3. 以 status + version 条件更新为 dispatching。 4. 创建 Attempt/Event。 5. 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 变更: 1. 修改两个方言 schema。 2. 使用锁定版本的 Drizzle Kit generate 或 custom migration 生成候选。 3. Maintainer 审查 SQL、锁范围、表重建、索引和数据转换。 4. 为旧版 fixture 增加 migration test。 5. 运行空库、升级、重复执行、失败恢复和 downgrade-read 测试。 6. 将 migration 与 checksum 提交。 7. 应用 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。两者不能各自写一套互不知情的历史表: 1. Kit 生成的 SQL 先经过人工审查和兼容 fixture 验证。 2. 审查后的方言 SQL、数据转换和 ownership manifest 共同计算 checksum。 3. 应用 runner 以现有 `SchemaMigrations` 记录稳定 ID、checksum 和应用时间。 4. 已有 `0001` 至 `0005` 继续属于同一线性历史,不重新标记为 Drizzle baseline,也不静默跳过。 5. 运行时不得调用 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 数据域迁移顺序 建议顺序: 1. Run/RunAttempt/RunEvent。 2. App/System/Auth/CronView。 3. Dependence/Env/Subscription。 4. Crontab/RunningInstance/CronStats。 5. Keyv store。 6. 删除 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。 迁移步骤: 1. 只读解析 keyv.sqlite。 2. 校验 key、JSON shape 和主库冲突。 3. 在主库事务写入。 4. 写 migration marker 与数据摘要。 5. read-through 验证一个窗口。 6. 停止写 keyv.sqlite。 7. 备份并保留旧文件一个弃用周期。 8. 移除 @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 并支持分批进度。 流程: 1. 拒绝新的 mutation,等待有界事务完成。 2. 在线备份到同文件系统临时路径。 3. 对备份执行 quick_check/integrity_check。 4. 写 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 实现的门禁 1. Maintainer 接受本 ADR。 2. 选择并锁定 exact Drizzle ORM/Kit 版本。 3. 明确 Node 24 exact patch,记录 node:sqlite stability 状态。 4. node:sqlite 在全部承诺架构上启动和读写通过。 5. database.sqlite fixture introspection 完成。 6. 0001/0002 与 Drizzle schema diff 为零或有已审查解释。 7. backup/restore spike 通过。 8. DatabaseSync event-loop benchmark 在 edge 预算内。 9. 临时 Sequelize adapter 的删除计划和 owner 明确。 10. 不使用 drizzle-kit push。 11. ownership manifest 与包含未知表/列/索引的 fixture preservation test 通过。 12. 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/sqlite` v6 beta 替代正式 Keyv 数据迁移。