ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

anarlog 中 @anlg/db 的 Drizzle 薄适配器设计:sqlite-proxy 契约、不变量与实现边界

anarlog 中 @anlg/db 的 Drizzle 薄适配器设计:sqlite-proxy 契约、不变量与实现边界 anarlog 中 anlg/db 的 Drizzle 薄适配器设计sqlite-proxy 契约、不变量与实现边界【免费下载链接】anarlogOpen source Granola AI Alternative项目地址: https://gitcode.com/GitHub_Trending/hy/anarlogpackages/db/AGENTS.md是 anarlog 仓库中定义anlg/db包职责边界的架构说明文档它规定了该包是“面向应用数据库 schema 的 Drizzle TypeScript 薄适配器”而不是通用 SQL 传输层。本文围绕该文档给出的三项核心约束——包角色、实现不变量Invariants、依赖方向——展开并结合packages/db的真实源码、测试用例与 Drizzle 运行时契约 逐条印证这些约束是如何落地的读完后你可以理解为什么 anarlog 要把“SQL 代理执行”与“命名行传输”严格分层以及如何在自己的项目中复用这套薄适配器模式。包的角色只做 Drizzle 适配不做传输AGENTS.md 对该包的角色定义非常明确共两点拥有应用数据库 schema 面向 Drizzle 的 TypeScript 适配器Owns the Drizzle-facing TypeScript adapter for the app database schema对外暴露createDb(...)并重新导出应用代码所使用的 Drizzle schema 与查询辅助函数ExposescreateDb(...)and re-exports the Drizzle schema/helpers used by app code。这一角色在 packages/db/package.json 中得到了印证包的exports只暴露两个入口——根入口./src/index.ts和 schema 子路径./src/schema.ts运行期依赖只有anlg/db-runtime提供代理客户端契约与drizzle-orm^0.44.2。开发依赖则包括drizzle-kit用于 introspect、typescript与vitest对应脚本introspect、typecheck、test。没有连接池、没有驱动、没有任何平台传输代码——这正是“薄适配器”定位的依赖层面证据。createDb(...)整个包的核心就十几行packages/db/src/index.ts 全部实现如下import { drizzle } from drizzle-orm/sqlite-proxy; import type { DrizzleProxyClient } from anlg/db-runtime; import * as schema from ./schema; export function createDb(client: DrizzleProxyClient) { return drizzle( async (sql, params, method) { try { return await client.executeProxy(sql, params, method); } catch (error) { console.error([drizzle-proxy], method, sql, error); throw error; } }, { schema }, ); } export * from ./schema; export { eq, and, or, desc, asc, sql, count, max, ne } from drizzle-orm;从实现可以看到三个要点只使用drizzle-orm/sqlite-proxy。Drizzle 的 sqlite-proxy 驱动模型是库内部生成 SQL 字符串和位置参数然后调用你提供的回调回调按method语义返回行数据。createDb把该回调直接转发给外部传入的DrizzleProxyClient.executeProxy自身没有任何 SQL 拼装或结果转换逻辑。错误只记录、不吞掉。回调中捕获异常后先console.error([drizzle-proxy], method, sql, error)打印方法名、原始 SQL 与错误对象再原样throw。这与文档中“createDb(...)must talk to a transport that implements the Drizzle sqlite-proxy contract directly”的不变量一致——错误处理是纯可观测性的附加不改变契约语义。重新导出 schema 与常用查询构造器。末尾的export * from ./schema与eq/and/or/desc/asc/sql/count/max/ne的再导出使应用代码只需依赖anlg/db一个包就能拿到表和查询辅助函数对应文档中“re-exports the Drizzle schema/helpers used by app code”的职责描述。DrizzleProxyClient契约代理层的唯一接口createDb的参数类型来自 packages/db-runtime/src/index.tsexport type ProxyQueryMethod run | all | get | values; export type ProxyQueryResult { rows: unknown[] }; export type DrizzleProxyClient { executeProxy( sql: string, params: unknown[], method: ProxyQueryMethod, ): PromiseProxyQueryResult; };这份契约定义了代理层与传输层之间的完整交互面输入是位置参数数组params与四种查询方法run写操作、all多行读、get单行读、values值集合与 SQLite 数据库 API 的语义一一对应输出{ rows: unknown[] }中的行是位置行positional rows即按 SELECT 列顺序排列的数组而不是带列名的对象。这一点是后文“不变量”章节的核心。同一个运行时包还定义了另外两份契约用于对比理解分层LiveQueryClient提供命名对象行的execute与带onData/onError回调的subscribe以及QueryEvent事件流与TransactionClientexecuteTransaction(statements)批量语句事务。anlg/db只消费其中的DrizzleProxyClientlive-query 与事务通道由其他层处理。不变量逐条解读为什么适配器“不能多做事”AGENTS.md 的 Invariants 一节是全文最核心的部分共五条约束逐条对照源码与测试可以看到它们各自的含义它是薄 Drizzle 适配器不是通用 SQL 传输层。src/index.ts中没有任何连接管理、SQL 解析或平台判断代码传输完全由调用方注入的client决定。createDb(...)必须与直接实现 Drizzle sqlite-proxy 契约的传输对话。也就是说executeProxy的实现方如 packages/db-tauri 面向 Tauri 命令通道的实现必须自己保证 proxy 语义适配器不会替它“补全”。Drizzle 的代理读写一律走executeProxy(sql, params, method)。见 index.ts 中唯一的调用点。executeProxy(...)必须按 SQL 的 select 顺序返回位置行这正是drizzle-orm/sqlite-proxy的预期。测试 packages/db/src/index.test.ts 中的maps proxy rows for findMany用例直接验证了这一点mock 返回的一行是[template-1, One, , 0, null, ...]这样的位置数组而db.select().from(templates)的解析结果是被 Drizzle 按 schema 列名映射后的命名对象{ id: template-1, title: One, ... }。映射发生在 Drizzle 层而非本包。本包不得解析 SQL也不得把命名对象行重映射成位置代理行命名对象行是下面一层通用 live-query 传输的职责。这两条划定了与LiveQueryClient其execute返回T[]命名行的责任边界两条通道互不越界代理通道只做字符串 位置参数 位置行的透传。从源码结构看第 5 条的边界是刻意设计anlg/db的导出面里没有LiveQueryClient的任何实现逻辑而 React 侧的响应式查询由独立的 packages/db-react 消费LiveQueryClientcreateUseLiveQuery/createUseDrizzleLiveQuery通过query.toSQL()拿到 SQL 与参数后再订阅三条层代理适配 / 命名行 live query / 事务各守其职。测试用例如何固化这些契约index.test.ts 用 6 个用例把不变量变成可执行的断言适合作为理解 proxy 语义的样本uses executeProxy for insertsdb.insert(templates).values(...)触发executeProxySQL 包含insert into templatesmethod 为runmaps proxy rows for findMany位置行数组被还原为与 schema 一致的命名对象含sectionsJson的JSON.parse还原[]→[]uses get mode for findFirstdb.query.templates.findFirst()使用get方法且rows直接是单行数组proxy 契约中 get 的返回形态passes all mode through to the proxy clientselect()使用all方法并断言 SQL 形如select id, title ...maps aggregate query rows from the proxy client聚合查询max(templates.pinOrder)的[[7]]行被映射为[{ maxOrder: 7 }]logs proxy errors and rethrowsexecuteProxy拒绝时console.error收到[[drizzle-proxy], all, 含 select 的 SQL, error]而查询本身 reject 出 Drizzle 的Failed query:错误——这正是上一节错误处理行为的回归验证。这些用例全部基于vi.fn()mock 的executeProxy没有任何真实数据库参与说明该包的契约验证完全在适配器边界内完成也再次印证“传输细节不属于本包”的依赖方向约束。依赖方向与边界不碰 Tauri/移动传输不碰初始化AGENTS.md 的 Dependency Direction 一节给出两条规则可以依赖anlg/db-runtime获取 proxy 客户端契约不得拥有 Tauri/移动端的传输细节也不得负责数据库初始化。对照 package.json依赖中只有anlg/db-runtimeworkspace:*和drizzle-orm没有任何tauri-apps/*、expo或驱动类依赖src 中不存在打开文件、执行迁移或建库的代码。Tauri 侧的传输实现位于独立的 packages/db-tauri 包它负责把executeProxy调用桥接到 Rust 侧——这是“初始化与传输归下层/旁层”的直观体现。另外值得注意 drizzle.config.tsexport default defineConfig({ dialect: sqlite, dbCredentials: { url: process.env.DB_PATH ?? ${process.env.HOME}/Library/Application Support/com.anthropic.char/app.db, }, out: ./src/generated, });它仅服务于pnpm introspect由 package.json 的introspect脚本调用drizzle-kit introspect从真实 SQLite 文件反向生成类型产物到./src/generated。这属于开发期工具链不影响运行时分层包运行时仍然只依赖注入的DrizzleProxyClient。Schema 侧./schema导出子路径的内容packages/db/src/schema.ts 是文档中“re-exports the Drizzle schema”所指的对象共约 750 行使用drizzle-orm/sqlite-core的sqliteTable定义应用表。几个可复用的模式UTC 时间戳默认值用 SQL 片段表达const currentTimestamp sql\(strftime(%Y-%m-%dT%H:%M:%fZ, now))作为createdAt/updatedAt的 Drizzledefault让默认值在数据库端生成布尔列统一用integer(..., { mode: boolean })存储例如templates.pinnedJSON 列用text(..., { mode: json })并在 schema 侧给 JSON 字符串默认值如iconJson的默认{type:icon,value:notebook-tabs,color:#9ca3af}、sectionsJson的默认[]软删除普遍采用可空的deletedAt文本列calendars、events、workspaces等索引通过表定义的第三个参数声明如index(idx_events_calendar_id).on(table.calendarId)、index(idx_events_started_at).on(table.startedAt)、index(idx_workspaces_owner_user_id).on(table.ownerUserId)。表中还包含trackingIdCalendar/trackingIdEvent之类的同步跟踪列从字段命名看它们服务于多端数据同步场景的远端标识对齐这是基于命名推断非文档明示。小结这套分层对开发者的启示anlg/db的 AGENTS.md 用约 20 行文本锁定了三条工程纪律——薄适配、位置行契约透传、依赖方向单向向下——而 index.ts、index.test.ts 与 db-runtime 契约 共同保证这些纪律可被编译期和测试期双重验证。对于需要在 Tauri、移动端等多传输面上共享同一套类型安全查询层的 SQLite 项目这套“适配器只转发、契约在运行时包、传输实现各自独立、测试只 mock 边界”的结构是一个可直接借鉴的参考实现。【免费下载链接】anarlogOpen source Granola AI Alternative项目地址: https://gitcode.com/GitHub_Trending/hy/anarlog创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进