ARTICLE DETAIL

资讯详情

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

opencode 的 Effect Drizzle SQLite 适配层:vendoring `@opencode-ai/effect-drizzle-sqlite` 包的设计与实现解析

opencode 的 Effect Drizzle SQLite 适配层:vendoring `@opencode-ai/effect-drizzle-sqlite` 包的设计与实现解析 opencode 的 Effect Drizzle SQLite 适配层vendoringopencode-ai/effect-drizzle-sqlite包的设计与实现解析【免费下载链接】opencodeThe open source coding agent.项目地址: https://gitcode.com/GitHub_Trending/openc/opencodeopencode 仓库中除了 Drizzle ORM 常见的异步 Promise 适配路径外还存在一个面向 Effect 生态的 SQLite 适配层仓库在 specs/storage/effect-sqlite-package.md 中规划、并在 packages/effect-drizzle-sqlite 中落地了这个包。本文围绕这份规格文档展开讲清楚三件事为什么 opencode 要自己 vendor 一份 Drizzle Effect SQLite 适配器而不是等上游这个包的公开接口make/makeWithDefaults/DefaultServices/migrate如何逐字对齐 Drizzle 的 adapter 命名以及事务、回滚、嵌套 savepoint 与迁移表这些关键保证在源码与测试中是如何被实现的。读完本文你可以在自己的 Bun/Node 项目中以同样的方式构建 Effect Drizzle SQLite 数据访问层并理解 opencode 后续如何在其上实现存储包装层。一、设计目标一个纯粹的 Drizzle Effect SQLite 包规格文档对包的目标定位非常明确这是一个vendored 的 Drizzleeffect-sqlite适配层不是 opencode 的存储抽象。文档原文要求见 specs/storage/effect-sqlite-package.md包名固定为opencode-ai/effect-drizzle-sqlite风格上对齐 packages/http-recorder 这类小型 workspace 包packages/opencode会在内部消费它但包本身必须保持通用——任何 opencode 的路径、迁移、表结构、事务钩子、post-commit 行为或领域语言都不允许进入这个包公开表面应尽量贴近 Drizzle 的 Effect 适配器Think of it as a vendoreddrizzle-orm/effect-sqlitepackage surface, not as a new storage service API以便未来上游发布effect-sqlite后可以直接替换。这一约束同样写进了包内的 packages/effect-drizzle-sqlite/AGENTS.md运行时代码只允许依赖泛型的effect/unstable/sql/SqlClient具体客户端如effect/sql-sqlite-bun只应出现在测试与示例中除非包有意提供驱动特定的 helper。文档还特意解释了为什么先做适配器包而不是先做 SessionStorage 这类领域层SessionStorage是有用的领域接缝但它回答不了核心的适配器问题——如何让 Drizzle SQLite 在本仓库中 Effect 原生化。先把适配器 vendor 下来opencode 就能在其上自建存储包装层SessionStorage、MessageStorage、event store 与 projector 写入都可以共享同一套事务与迁移模型。这个决策把适配器正确性与领域存储设计两个问题解耦了。二、包的目录结构与导出面规格文档给出的包形态与最终落地的目录一致packages/effect-drizzle-sqlite/package.json声明了 4 个子导出——.、./effect-sqlite、./effect-sqlite/migrator、./sqlite-core/effectpackages/effect-drizzle-sqlite/src/index.ts入口导出src/effect-sqlite/*驱动、会话、迁移器src/sqlite-core/effect/*Effect 化的 SQLite 查询构建器select.ts/insert.ts/update.ts/delete.ts/raw.ts等packages/effect-drizzle-sqlite/test/sqlite.test.ts适配器级保证的集成测试。入口文件的导出与文档中规划的 Initial exports 完全一致// packages/effect-drizzle-sqlite/src/index.ts export { EffectLogger } from drizzle-orm/effect-core export * from ./effect-sqlite/driver export * from ./effect-sqlite/session export { migrate } from ./effect-sqlite/migrator export * as EffectDrizzleSqlite from .从 package.json 的依赖可以看到运行时只依赖drizzle-orm与effect两个 catalog 依赖effect/sql-sqlite-bun被放在 devDependencies 中——这正印证了 AGENTS.md 中具体 SQLite 客户端只进测试/示例的约定。测试脚本为bun test --timeout 30000 --only-failures类型检查使用tsgo --noEmit。三、公开表面make、makeWithDefaults与DefaultServices文档要求公开表面 mirror Drizzles Effect adapters并给出示例用法。packages/effect-drizzle-sqlite/src/effect-sqlite/driver.ts 的实现与文档中的 Public Surface 小节逐点对应// driver.ts 关键部分 export class EffectSQLiteDatabaseTRelations extends AnyRelations EmptyRelations extends SQLiteEffectDatabaseEffectSQLiteQueryEffectHKT, EffectSQLiteRunResult, TRelations { static override readonly [entityKind]: string EffectSQLiteDatabase } export type EffectDrizzleSQLiteConfigTRelations extends AnyRelations EmptyRelations Omit DrizzleConfigRecordstring, never, TRelations, cache | logger | schema export const DefaultServices Layer.merge(EffectCache.Default, EffectLogger.Default) export const make Effect.fn(SQLiteDrizzle.make)(function* ( config: EffectDrizzleSQLiteConfigTRelations {}, ) { const client yield* SqlClient // 泛型 Effect SqlClient 服务 const cache yield* EffectCache const logger yield* EffectLogger const dialect new SQLiteAsyncDialect() const session new EffectSQLiteSession(client, dialect, relations, { logger, cache, useJitMappers: jitCompatCheck(config.jit), }) const db new EffectSQLiteDatabase(dialect, session, relations) db.$client client db.$cache.invalidate cache.onMutate return db }) export const makeWithDefaults (config {}) make(config).pipe(Effect.provide(DefaultServices))这里有几个值得注意的实现细节make只依赖泛型SqlClient。文档注释写明Drizzle only depends on the genericSqlClient; install and provide a compatible SQLite provider such aseffect/sql-sqlite-node,effect/sql-sqlite-bun, or another package that exposesSqlClient. 这正是文档中SQLite clients come from Effect layers such asSqliteClient.layer({ filename })这一 API 模式的落点——包本身不绑定 Bun 或 Node 客户端由消费方通过 Layer 注入。DefaultServices合并了EffectCache.Default与EffectLogger.Default与文档中 DefaultServicesshould provide Drizzles default logger/cache services, same as Effect Postgres 的说明一致。$cache.invalidate被接管为cache.onMutate写操作发生后通过 Effect 缓存服务的onMutate失效缓存而不是 no-op基类SQLiteEffectDatabase的默认$cache.invalidate是Effect.void见 db.ts。配置类型EffectDrizzleSQLiteConfig用OmitDrizzleConfig..., cache | logger | schema裁掉了三个字段——logger 和 cache 改由 Layer 提供schema 则通过 relations 参数传入避免重复声明。由此得到的标准用法与文档 Public Surface 示例等价import { SqliteClient } from effect/sql-sqlite-bun import * as Effect from effect/Effect import { EffectDrizzleSqlite } from opencode-ai/effect-drizzle-sqlite const db yield* EffectDrizzleSqlite.make({ relations }).pipe( Effect.provide(EffectDrizzleSqlite.DefaultServices), Effect.provide(SqliteClient.layer({ filename: sqlite.db })), ) yield* db.select().from(users) yield* db.transaction( (tx) Effect.gen(function* () { yield* tx.insert(users).values({ name: Ada }) }), { behavior: immediate }, )仓库自带的 examples/basic.ts 给出了完整的 生产级 写法用Context.Service把数据库包成Database服务Layer.effect(Database, makeDatabase).pipe(Layer.provide(sqliteLayer))再在其上构建一个带领域错误类型UserStoreError用Schema.TaggedErrorClass定义的UserStore服务演示了migrate/create/rename事务内两条写语句/list四个操作最后用Effect.runPromise(program.pipe(Effect.provide(UserStore.layer)))驱动整个程序。迁移目录则指向 examples/migrations/20240101000000_create_users/migration.sql。四、会话层实现Effect 化的查询、缓存与事务文档 Upstream References 一节列出了 API 模式的来源Drizzle Effect Postgres RC 的query-effect.ts、SQLite Effect 分支的up-migrations/effect-sqlite.ts、以及 Effect SQLite 客户端的SqliteClient.ts。包内源码按这些参考移植核心在两个文件。4.1 查询执行EffectSQLiteSessionpackages/effect-drizzle-sqlite/src/effect-sqlite/session.ts 中的EffectSQLiteSession继承 Drizzle 的SQLiteEffectSession把泛型SqlClient适配为 Drizzle SQLite 会话。关键的execute私有方法展示了三种执行路径如何映射到 Effect SQL 客户端private execute(query: Query, params: unknown[], method: SQLiteExecuteMethod | values) { const statement this.client.unsafe(query.sql, params) if (method values) return statement.values if (method get) return statement.withoutTransform.pipe(Effect.map((rows) rows[0])) return statement.withoutTransform }注意withoutTransform的用法适配器要求拿到原始行数组再自行做 Drizzle 的行映射mapAllResult/mapGetResult这样 JIT mappermakeJitQueryMapper与非 JIT 路径mapResultRow的行为和 Drizzle 官方实现保持一致。prepareQuery/prepareRelationalQuery还透传了一个对缓存语义很重要的参数——this.isInTransaction()private isInTransaction() { return Effect.serviceOption(this.client.transactionService).pipe( Effect.map((option) option._tag Some) ) }也就是说查询构建器本身就能感知自己是否处于事务上下文中。在 sqlite-core/effect/session.ts 的queryWithCache里事务内的 select 会跳过缓存策略select cacheConfig?.enabled (yield* this.isInTransaction) 时直接执行查询——事务内读未提交数据时命中缓存会读到脏数据这个细节保证了事务语义与 Drizzle 上游一致。错误处理也在这里收敛所有底层失败都被包装成EffectDrizzleQueryError携带query、params与cause调用方可以在 Effect 的错误通道里拿到结构化的查询错误而不是字符串。4.2 事务begin/commit/ 嵌套 savepointwithTransaction是整个包中最体现 Effect-native 的一段session.ts#L118-L187其结构可以拆解为不可中断外壳Effect.uninterruptibleMask((restore) Effect.withFiber(...))——begin之后的整个窗口对外部 fiber 取消不可中断只有交给用户的事务 body 用restore(effect)恢复可中断性连接选择若 fiber 上下文中已存在client.transactionService即嵌套事务直接复用已有连接并取id 1否则Scope.provide(this.client.reserve, scope)预留新连接reserve失败时先关闭 scope 再向上抛错SQL 语句选择顶层id 0执行begin ${config?.behavior ?? deferred}嵌套层执行savepoint effect_sql_${id}上下文注入Context.add(services, this.client.transactionService, [connection, id])把[连接, id]放入子 fiber 的服务上下文——这就是 4.1 中isInTransaction()与 嵌套Database.use看到当前事务 语义的底层机制结局处理成功且顶层commit。源码中特别处理了一个 SQLite 怪癖——deferred 约束如外键在 commit 阶段失败时事务仍然打开因此 commit 失败会补一条rollback失败则吞掉再抛出原错误成功且嵌套release savepoint effect_sql_${id}失败且顶层rollback失败且嵌套rollback to savepoint effect_sql_${id}后再release资源回收只有新开连接的路径才在onExit时Scope.close(scope, exit)复用连接时 scope 为undefined不做处理。transaction的签名则直接采用 Drizzle 的SQLiteTransactionConfig即{ behavior: deferred | immediate | exclusive }错误通道为E | SqlError。EffectSQLiteTransaction.rollback()返回EffectTransactionRollbackError与 Drizzle 其他 Effect 适配器的显式回滚语义一致。值得强调的是文档在 Opencode Adoption Notes 里明确提醒opencode 当前packages/opencode/src/storage/db.ts有两处非平凡语义嵌套Database.use在Database.transaction内看到当前事务Database.effect在事务内排队 post-commit 副作用、事务外立即执行opencode 包装层要用 Effect 上下文而非LocalContext实现一个私有{ tx, afterCommit }事务上下文来保留这些行为并且不要移除这一行为——SyncEvent.run依赖事务可组合性与behavior: immediate保证顺序正确性。本包提供嵌套 savepoint 机制effect_sql_${id}命名空间正好为将来从复用外层 tx演进到显式 savepoint留了接口。五、迁移migrate与__drizzle_migrations表migrator.ts 只有 14 行读取 Drizzle 标准迁移文件readMigrationFiles再交给 sqlite-core/effect/session.ts 的 Effect 版coreMigrate。核心迁移逻辑保证迁移表默认__drizzle_migrationsconfig.migrationsTable可覆盖schema 为id INTEGER PRIMARY KEY, hash text NOT NULL, created_at numeric, name text, applied_at TEXT只跑未应用的迁移用getMigrationsToRun对比本地迁移readMigrationFiles读到的 hash/folderMillis/name与库中已记录迁移差集为空直接返回——即幂等原子应用所有待跑迁移在session.transaction((tx) ...)内逐条执行tx.run(sql.raw(stmt))每条迁移执行完立即向迁移表插入元数据行hash、created_at、name、applied_at任何一步失败整体回滚init语义config.init true时用于首次初始化——库里已有迁移记录则报MigratorInitErrordatabaseMigrations本地不止一条迁移则报localMigrations否则只登记单条迁移而不执行 SQL。migrate本身返回Effect值Effect.fn(migrate)在消费方的 Effect 程序里yield*即可错误类型是 Drizzle 的迁移错误。examples/basic.ts 的UserStore.migrate展示了典型接法EffectDrizzleSqlite.migrate(db, { migrationsFolder }).pipe(Effect.mapError(...))。六、测试对适配器级保证的验证规格文档 Migration Strategy 第 3 步列出了需要验证的 5 条适配器级保证test/sqlite.test.ts 逐条覆盖测试使用effect/sql-sqlite-bun的SqliteClient.layer({ filename: :memory:, disableWAL: true })与真实临时文件数据库文档要求的保证对应测试查询构建器是 yieldable 的 Effect 值selects rows through Effect-yieldable query buildersyield* db.select().from(users)与db.select({ id: users.id }).from(users).where(eq(users.name, Ada)).get()transaction(..., { behavior: immediate })提交成功写入commits successful transactions失败事务回滚rolls back failed transactionsEffect.fail(boom)后表仍为空与rolls back explicit transaction rollbacktx.rollback()触发EffectTransactionRollbackError迁移只跑一次且有序runs migrations once and records migration metadata同一migrationsFolder连续migrate两次migrated_users只建表一次__drizzle_migrations中恰好一行记录close finalizer 关闭底层数据库通过每个测试的Effect.scopedSqliteClient.layer作用域实现Effect 程序结束时 layer 的 finalizer 关闭 SQLite 连接测试还额外验证了两条更细的保证错误保真preserves failed transaction begin errors用bun:sqlite打开同名文件并begin immediate制造锁竞争adapter 的begin immediate失败必须以SqlError的LockTimeoutErrorcause 信息含 database is locked形式暴露而不是被吞掉或降级returning与写路径校验supports returning and rejects empty update setsinsert...returning/update...returning/delete...returning都返回正确的行对象而db.update(users).set({ name: undefined })会抛出 No values to set——确认 Effect 化的 query builder 保留了 Drizzle 的输入校验行为。七、opencode 的迁移策略与采纳注意事项文档给出的迁移路线Migration Strategy共 8 步其核心思想是先适配器、后领域层、最后迁移调用点建立opencode-ai/effect-drizzle-sqlite用最小化的内存/文件 SQLite 测试 schema从 Drizzle SQLite 分支移植适配器保留上游命名与 API 形态测试上述 5 条适配器级保证把包加为packages/opencode的依赖把packages/opencode/src/storage/db.ts改造成适配器之上的薄兼容包装层 opencode 专属事务/post-commit 上下文先保持现有调用点可用Database.Client()、Database.use(...)、Database.transaction(...)、Database.effect(...)兼容稳定后再把调用点从 callback 风格的Database.use迁移到直接 yield Effect Drizzle 查询最后才在 opencode 存储包装层之上构建 session/message/project 等领域 store。文档同时明确了边界opencode 专属的 path/channel 选择留在packages/opencodeafterCommit在事件发布机制迁移之前保持 opencode 专属默认答案是是。Recommended First PR 一节则要求首个 PR只做包、刻意无聊加包、用极小测试 schema不用 opencode 领域表、证明查询/事务/迁移、不迁移packages/opencode本体——在惊动 opencode 现有数据库运行时之前先有一个专注的验证点。文档末尾的 Open Questions 记录了若干当时未定的决策首个包目标effect/sql-sqlite-bun还是 node、从 Drizzle 分支拷贝多少源码还是直接 import catalog 的drizzle-orm内部实现、上游发布effect-sqlite后的更新路径、兼容包装层是否临时保留同步返回类型等。从当前仓库状态看前两个问题已有答案包运行时依赖泛型SqlClient测试与示例选择了effect/sql-sqlite-bun并依赖 catalog 版drizzle-orm的公开与内部模块实现适配。八、小结这个包值得借鉴的三个设计点从 specs/storage/effect-sqlite-package.md 到 packages/effect-drizzle-sqlite 的落地过程有几条对在大型仓库中引入 Effect 数据层有直接参考价值的经验适配层与领域层严格分层包内没有任何 opencode 领域概念EffectSQLiteDatabase只回答如何让 Drizzle 查询成为 Effect 值领域错误建模如UserStoreError留给消费方——这使得上游drizzle-orm/effect-sqlite发布后替换成本最低嵌套事务用服务上下文而非全局状态表达Context.add(services, client.transactionService, [connection, id])让我在哪个事务里成为 Effect 环境的一部分天然随 fiber 隔离嵌套层用savepoint effect_sql_${id}精确回滚且顶层 commit 对 SQLite deferred 约束失败做了 rollback 兜底用测试固化适配器契约5 条保证yieldable 查询、immediate 提交、失败回滚、迁移幂等、scope 关闭全部有对应测试用例锁竞争场景还验证了错误类型不丢失——这正是后续把 opencode 存储包装层建立在该包之上时最有价值的地基验收单。【免费下载链接】opencodeThe open source coding agent.项目地址: https://gitcode.com/GitHub_Trending/openc/opencode创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表