ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

OmniRoute 可插拔持久化边界 ADR:从同步 SqliteAdapter 到域仓库契约的架构决策解析

OmniRoute 可插拔持久化边界 ADR:从同步 SqliteAdapter 到域仓库契约的架构决策解析 OmniRoute 可插拔持久化边界 ADR:从同步 SqliteAdapter 到域仓库契约的架构决策解析【免费下载链接】OmniRouteNever stop coding. Free MIT AI gateway: one endpoint, 352 providers (150 free), 1200 models Kimi, Claude, GPT, Gemini, GLM, DeepSeek, MiniMax. Works with Claude Code, Codex, Cursor, OpenCode, Cline Copilot. Quota-aware auto-fallback, RTKCaveman compression saves 15-95% tokens, MCP/A2A, Desktop/PWA. Built by 550 contributors项目地址: https://gitcode.com/GitHub_Trending/om/OmniRoute本文基于 OmniRoute 仓库中的架构决策记录(ADR)《Pluggable persistence boundary》(波兰语译文位于 persistence-backend-boundary.md,英文原版见 persistence-backend-boundary.md),系统解析该项目如何为多数据库后端规划域仓库 内部异步后端两级持久化边界:它为什么不在现有同步 SQLite 适配器上硬塞 PostgreSQL,如何划定可移植仓库的允许/禁止面,以及用何种一致性测试与交付序列保证迁移过程可回滚、可审查。读完后,你将能够理解一个嵌入式 SQLite 系统在引入外部数据库前应做的边界设计,并能对照仓库源码核实该决策的现状证据。文档定位与状态这份 ADR 的关键元信息如下:状态:Proposed(提案中)——在 maintainer 批准前,不启动任何运行时改造;跟踪问题:#8075;范围:仅涉及持久化架构。该决策本身不引入、也不选择任何外部数据库,不新增数据库依赖、环境变量、schema 或迁移文件。需要特别强调的是一条非目标边界:直到 maintainer 对文末开放问题作出裁定之前,该文档只是一份提案,不隐含任何运行时重构。这决定了本文所有未来形态描述都属于设计意图,而非仓库当前已实现的能力。背景:SQLite 形态的持久化现状ADR 的第一部分用耦合清单说明问题所在。以下逐条对照当前仓库源码核实。1. 同步的 SqliteAdapter 契约OmniRoute 的领域持久化函数集中在src/lib/db/目录(该目录包含数百个领域模块,如apiKeys.ts、combos.ts、providers.ts等),而所有模块共享的数据库连接由 core.ts 返回,其类型是定义在 types.ts 中的同步SqliteAdapter接口:export interface SqliteAdapter { readonly driver: better-sqlite3 | node:sqlite | bun:sqlite | sql.js; readonly open: boolean; readonly name: string; readonly inTransaction?: boolean; prepare(sql: string): PreparedStatement; exec(sql: string): void; pragma(pragmaStr: string, options?: { simple?: boolean }): unknown; /** 在 DEFERRED 事务中执行 fn */ transactionT(fn: (...args: unknown[]) T): (...args: unknown[]) T; /** 在 IMMEDIATE 事务中执行 fn(立即获取写锁) */ immediate(fn: () void): void; /** 原生备份或 file-copy 回退 */ backup(destination: string): Promisevoid; checkpoint(mode?: string): void; close(): void; readonly raw: unknown; }(见 types.ts,PreparedStatement提供同步的run/get/all方法。)从源码结构看,这个接口虽然横跨四个 SQLite 运行时(better-sqlite3、node:sqlite、bun:sqlite、sql.js),但其表面完全是 SQLite 形状的:同步 prepared statements、pragma、deferred/immediate 双模式事务、原生备份/文件复制备份、WAL checkpoint 以及本地数据库句柄(raw)。ADR 的判断是:这些是嵌入式 SQLite 部署的合理属性,应当保留,但不应强迫 PostgreSQL 或 MySQL 去模拟这套 API。2. 启动路径拥有 SQLite 文件生命周期core.ts 解析数据目录并定位storage.sqlite:export const DATA_DIR resolveWritableDataDir({ isCloud }); const LEGACY_DATA_DIR isCloud ? null : getLegacyDotDataDir(); export const SQLITE_FILE isCloud ? null : path.join(DATA_DIR, storage.sqlite); const JSON_DB_FILE isCloud ? null : path.join(DATA_DIR, db.json); export const DB_BACKUPS_DIR isCloud ? null : path.join(DATA_DIR, db_backups);同一模块还维护着一个进程级全局适配器,负责 WAL checkpoint(见 walMaintenance 的导入与startWalMaintenance/runCheckpointNow调用)、重建数据库时删除 SQLite 伴生文件(WAL、shm 等),并在恢复流程中快照/保留关键表。核心表清单CRITICAL_DB_TABLES直接写在 core.ts 中,包括key_value、provider_connections、provider_nodes、combos、api_keys、proxy_registry、webhooks等,每张表带maxRows上限(5,000~10,000 行)。这正是 ADR 所指的启动与恢复路径拥有 SQLite 文件生命周期的具体形态——业务语义与文件操作耦合在同一个模块里。3. 驱动选择不是外部后端抽象driverFactory.ts 负责在受支持的 SQLite 运行时之间做驱动级联选择。从源码可以看到它通过字面量switch分支加载驱动(L109-L120 的requireSqliteDriver只接受bun:sqlite、better-sqlite3、node:sqlite,其他一律抛出Unsupported SQLite driver module),并配有 Windows 原生插件挂起守护(子进程有界超时探测 better-sqlite3 是否可加载,避免 ABI 不匹配的 native addon 在DllMain中卡死整个进程)。ADR 对此的定性很关键:这套级联解决的是哪个 SQLite 运行时可用,而不是外部后端抽象。把 PostgreSQL 塞进这条级联,等于让一个为同步 SQLite 运行时设计的兼容层去承接一个异步网络数据库——这正是被否决的替代方案之一(见下文)。4. Schema 演进同样耦合migrationRunner.ts 从migrations目录按序应用编号 SQL 文件,其模块头注释说明了机制:命名规范NNN_description.sql(如001_initial_schema.sql);已应用版本记录在schema_migrations表;每个文件的全部迁移在单个事务中执行(全有或全无);安全措施包括:迁移前备份、批量迁移检测(已有库上待执行迁移数超过阈值即中止,防止迁移跟踪表被误删后从头重放)、迁移重命名告警。Runner 内部还会探测sqlite_master与PRAGMA table_info、检测可选的 FTS5 支持、处理遗留版本槽位与重编号兼容(见 migrationRunner 子模块 中的LEGACY_VERSION_SLOT_MIGRATIONS、OPTIONAL_FTS5_MIGRATION_VERSIONS等常量)。这些机制全部建立在 SQLite 专属元数据之上,是 ADR 中外部后端不能假定 SQLite SQL 文件可移植这一条规则的直接依据。5. 运维模块直接使用 SQLite 语义backup.ts:基于SQLITE_FILE的文件复制备份、DB_BACKUPS_DIR保留策略(环境变量DB_BACKUP_MAX_FILES/DB_BACKUP_RETENTION_DAYS覆盖 → 持久化 UI 值 → 默认值),并有 60 分钟节流防止高变更场景下每次调用都复制整个数据库文件;optimizationSettings.ts:直接操作 page-size、cache-size、auto-vacuum 等PRAGMA参数,配合VACUUM语义;此外还有vacuumScheduler.ts、recovery.ts、memoryVec.ts(sqlite-vec 集成)等模块。ADR 的结论是:这些是正确且应当保留的嵌入式 SQLite 特性,但必须被隔离在 SQLite 自己的实现与运维接口之后,而不是成为可移植层的一部分。决策:两级持久化边界ADR 的核心决定是为可移植的持久状态建立两级边界:第一级:域仓库契约(Domain repository contracts)定义业务与路由代码所需的持久化操作。调用方只依赖领域行为与领域数据,不依赖 SQL 文本、prepared statement、数据库文件或方言对象。第二级:内部异步后端契约(internal async backend contract)支撑仓库实现,提供:事务上下文、health/readiness、迁移协调、后端能力声明(capabilities)与分类错误。值得注意的一个工程取舍是:ADR刻意不冻结具体的 TypeScript API 表面——精确的接口将随第一个实现 PR 提出,并由一致性测试验证。这是一种以测试固化契约、而非以文档固化契约的写法,避免 ADR 变成一份过早的 API 规格。后端选型顺序被明确排定:SQLite 保持默认实现。现有驱动级联与同步SqliteAdapter留在 SQLite 仓库实现之后,各领域以小的垂直切片(vertical slice)逐步迁移;任何用户都不需要配置外部服务;PostgreSQL 是第一个外部实现,且前提是先对 SQLite 证明仓库边界;MySQL 作为同一套一致性测试套件下的对等实现跟进,而不是第二份业务逻辑分叉。仓库现状佐证:垂直切片已经开始落地当前仓库中已存在 src/lib/db/repositories/ 目录,包含三个文件:routingConfigRepositories.ts、sqliteComboRepository.ts、sqliteModelComboMappingRepository.ts。从源码结构看,这与 ADR 交付序列中第 2、3 步(引入首批域仓库契约、将现有 SQLite 实现适配到契约之后)相吻合,且 ADR 点名的候选域(provider connections、API keys、combos、routing configuration)正是这批文件覆盖的方向。边界规则可移植仓库的允许面一个可移植仓库可以暴露:领域读写;显式的原子操作,以及事务作用域内的仓库访问;当并发语义属于领域本身时,compare/update 或 lease 操作;后端中立的分页、排序与约束错误。后端 health、readiness 与迁移协调归属于内部后端/运维契约,不属于单个域仓库——这避免了每个仓库都携带一份健康检查样板。可移植仓库的禁止面可移植仓库不得暴露:禁止项原因(结合源码理解)prepare、get、all、run或裸驱动句柄会泄露同步 SQLite 方言与驱动对象PRAGMA、WAL checkpoint 模式、VACUUM、page/cache 调优SQLite 专属运维语义(当前散落在 optimizationSettings 等模块中)SQLite 文件路径、伴生文件、文件复制备份外部后端没有数据库文件概念lastInsertRowid作为跨后端领域契约依赖 SQLite rowid 的隐式 ID 语义无法移植(对照 types.ts 中RunResult.lastInsertRowid正是当前领域代码广泛使用的返回值)FTS5 或sqlite-vec语法全文/向量检索属于能力项,不属于可移植面供普通业务代码使用的通用方言逃生舱逃生舱一旦存在就会被用滥,边界形同虚设后端能力面SQLite 专属维护保持在 SQLite 自己的实现与运维接口之后,包括:运行时驱动选择、WAL checkpoint 与关闭行为、page-size/cache-size/auto-vacuum 设置、数据库文件备份/恢复、SQLite schema 内省、FTS5 与sqlite-vec集成。对等规则是:外部后端无义务模仿这些能力。仓库必须三选一:使用可移植能力、提供带文档行为的后端专属实现、或明确报告该能力不可用。这条规则防止了兼容性 shim 慢慢渗漏的经典劣化路径。事务与迁移模型事务:契约面向可观察保证,而非 SQL 模式仓库 API 定义原子业务操作,调用方不选择SQL 事务模式(deferred/immediate 由 SQLite 实现内部决定)。每个操作必须定义其可观察的并发保证:受保护的不变量、冲突检测、重试分类、幂等性预期、事务上下文传播。实现可以使用不同的事务与隔离机制,前提是这些可观察保证等价;SQLite 内部可以继续用当前的 deferred/immediate 行为,只要满足操作契约即可。迁移:显式的所有权外部后端要求显式的迁移所有权,防止多个应用副本竞争同一 schema 变更。后端之间的迁移历史可以共享逻辑里程碑,但 SQLite 的 SQL 文件不被假定为可移植、更不假定可复用于其他方言——这与 migrationRunner 深度依赖sqlite_master/PRAGMA table_info的现状一致。跨后端一致性语义ADR 要求一致性测试覆盖行为而非仅仅仓库方法签名。每个迁移后的域必须定义并验证九个维度:时间戳的时区、精度与序列化;NULL排序、collation 与大小写敏感性预期;JSON 表示与比较行为;整数、小数与货币精度;稳定排序与分页时的确定性平局裁决(tie-breaker);不依赖 SQLite row ID 的 ID 生成;唯一性与外键违规的分类;no-op、compare/update 与 delete 操作的 affected-row 行为;并发写入的结果、可重试冲突与幂等重试。收尾条款同样重要:如果一个域无法表达等价的可观察语义,它就不算可移植,必须保持后端专属,直到该契约被设计出来。这为先做、再谈移植提供了清晰的判据,而不是模糊的尽量兼容。兼容性要求任何遵循该 ADR 的实现必须保持以下属性(逐条对应仓库现状):SQLite 保持零配置默认;现有 SQLite 文件与迁移历史保持可读;npm、Electron、Docker 与受限运行时的 SQLite 回退保持当前启动路径(对应 driverFactory 的四运行时级联与 electron/ 桌面的零服务启动模型);存储的 provider 凭据继续使用现有的应用层加密行为(见 encryption.ts 的migrateLegacyEncryptedString等机制,由 core.ts 导入);仓库迁移不得悄悄改变路由、配额、API 密钥或审计语义;备份与恢复行为按后端分别文档化,而不是一律宣称通用;纯 SQLite 的干净安装不加载、也不要求任何外部数据库驱动。交付序列ADR 将实施拆成七步,每步都是独立的、可审查的 PR,且后一步不能作为提前合入前一步未证明的抽象的理由:发布可复现的 SQLite 耦合清单(coupling inventory),作为独立审查工件;引入首批域仓库契约与一致性测试;在不改变默认配置的前提下,将现有 SQLite 实现适配到契约之后;经 maintainer 批准后,为一个受限的 control-plane 切片加入 PostgreSQL 作为第一个外部实现;只有存在并发写入与迁移所有权测试后,才扩展共享状态;在宣传可切换数据库之前,先加入离线、已验证的 SQLite-to-external 迁移路径;让 MySQL 对已证明的仓库与后端契约进行实现。首个实现切片的准入条件首个运行时切片应在耦合清单评审后选定。候选域是 provider connections、API keys、combos 与路由配置——因为它们的基表在 core.ts 的CRITICAL_DB_TABLES中可见——但 ADR 明确不批准任何表清单或迁移 PR。切片必须包含:保持 SQLite 行为不变的行为保留测试;仓库一致性测试;显式的事务边界;对存储凭据的加密与脱敏(redaction)验证;默认启动配置零变更。被否决的替代方案ADR 完整记录了五个被否决的方向及其理由,这是判断架构取舍的典型样本:替代方案否决理由在SqliteAdapter之下加 PostgreSQLSqliteAdapter是 SQLite 运行时的兼容层,暴露 SQLite 专属操作;模拟该表面会把同步与方言假设泄漏进新后端向所有域暴露通用 query/execute API作为主边界会集中连接处理,但 SQL 方言、事务与表的耦合仍留在业务模块中;低层后端原语可以存在于仓库实现内部,但不作为应用面向的持久化 API先重写全部持久化、再验证一个切片当前持久化面很宽(文件生命周期、恢复、搜索、运维设置),垂直切片提供可审查的行为与回滚边界以外部数据库取代 SQLite 默认嵌入式与桌面部署依赖当前零服务启动模型,外部后端是 opt-in用 Redis 作为持久权威Redis 适合显式临时的协调、缓存或计数器,不能替代此处定义的持久仓库契约(仓库中 Redis 仅用于此类场景,见 REDIS_PRODUCTION_CONFIG.md)影响:收益与成本收益:业务代码获得独立于数据库方言的稳定持久化接缝;外部后端定义抽象之前,SQLite 行为先被测试固化;PostgreSQL 与 MySQL 共享契约与测试,而非复制领域逻辑;SQLite 专属能力保持一等公民地位,而不是沦为渗漏的兼容 shim;多副本迁移与事务行为成为显式设计关切。成本与风险:仓库抽取需要增量迁移调用点;异步边界可能沿当前同步的服务代码向上传播;跨后端语义需要超越 SQL 语法兼容性的测试;备份、搜索、向量存储与维护仍按能力项单独处理;同时运行多个持久化实现会增加 CI 与运维支持成本。非目标与开放问题该 ADR 明确不做以下事情:不新增数据库依赖/环境变量/schema/迁移;不改动存活的 SQLite 单例与驱动级联;不承诺某个版本提供 PostgreSQL 或 MySQL 支持;不使 FTS5、sqlite-vec、备份文件或 SQLite 维护变得可移植;不在共享状态与协调测试存在前定义 active-active 就绪度;不批准对src/lib/db/的一次性重写。留给 maintainer 批准的五个开放问题:仓库 内部异步后端边界是否是首选方向,还是外部持久化应放在独立的 control-plane 服务之后?在 SQLite 一致性证明之后,PostgreSQL 是否可接受为第一个外部实现?哪个域应作为首个受限仓库切片?首个多副本里程碑中哪些状态必须共享、哪些保持节点本地?被中断或回滚的仓库迁移需要多长的兼容窗口?小结这份 ADR 的价值不在于将来支持 PostgreSQL,而在于它把一个嵌入式 SQLite 系统的耦合面显式化:同步适配器表面、文件生命周期、迁移内省、运维 PRAGMA,全部被清点并划出边界。通过域仓库契约只暴露领域操作 内部后端契约承接 health/迁移/能力的两级设计,配合九个维度的行为一致性测试与七步可回滚交付序列,它把换数据库从一个一次性重写风险,转化为一组独立可审查、独立可回滚的工程步骤。对维护类似多运行时 SQLite 架构的开发者而言,其模板意义——先固化默认实现的测试、再冻结契约、最后才引入外部实现——与具体数据库选型同样重要。【免费下载链接】OmniRouteNever stop coding. Free MIT AI gateway: one endpoint, 352 providers (150 free), 1200 models Kimi, Claude, GPT, Gemini, GLM, DeepSeek, MiniMax. Works with Claude Code, Codex, Cursor, OpenCode, Cline Copilot. Quota-aware auto-fallback, RTKCaveman compression saves 15-95% tokens, MCP/A2A, Desktop/PWA. Built by 550 contributors项目地址: https://gitcode.com/GitHub_Trending/om/OmniRoute创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表