从 CHANGELOG 到源码:解读 @effect/sql-sqlite-do 的 Cloudflare Durable Object SQLite 客户端演进
2026/9/15 13:41:56 网站建设 项目流程

从 CHANGELOG 到源码:解读 @effect/sql-sqlite-do 的 Cloudflare Durable Object SQLite 客户端演进

【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3code

@effect/sql-sqlite-do是 Effect SQL 生态中面向 Cloudflare Durable Object 内置 SQLite 存储的适配层,让开发者能在 Durable Object 内以类型安全、可组合的 Effect 风格执行查询、事务与迁移。本文以该包的 CHANGELOG 为主线,结合仓库内 SqliteClient.ts、SqliteMigrator.ts 与测试用例,系统梳理其核心能力、事务与错误分类机制的演进,读完即可在 Worker/Durable Object 场景中正确接入并运用这套 SQL 工具链。

一、包定位与版本发布节奏

@effect/sql-sqlite-do在 packages/sql/sqlite-do 目录下,是一个很薄的适配层:它把 Durable Object 的SqlStorage句柄适配为 Effect 的通用SqlClient服务,并额外暴露 Durable Object 专用的SqliteClient服务。从 package.json 可以看到:

  • 当前版本为4.0.0-rc.112,与effect@4.0.0-rc.112严格对齐(peerDependencies 为effect: workspace:^);
  • 包通过exports映射暴露src/index.ts,同时将./index./*/index显式置为null
  • 依赖@cloudflare/workers-types(^5.x)提供DurableObjectStorage/SqlStorage类型。

CHANGELOG 中绝大多数条目是 "Updated dependencies"(同步effect核心包升级),真正属于 sqlite-do 自身的功能变更集中在几个关键节点上:事务支持(beta.88)、valuesUnprepared(beta.86)、错误分类重塑(beta.65 / beta.37)、入口结构调整(beta.103),以及 v4 里程碑(beta.0)。这说明该包的演化节奏基本跟随 Effect 核心的 SQL 抽象层(effect/unstable/sql/*),而自身维护着面向 Durable Object 的差异逻辑。

安装方式遵循官方 README(README.md):

npm install effect@rc @effect/sql-sqlite-do@rc

二、客户端配置:SqliteClientConfig 与双服务提供

在 SqliteClient.ts 中,SqliteClientConfig是构造客户端的唯一配置入口:

export interface SqliteClientConfig { readonly db?: SqlStorage | undefined readonly storage?: DurableObjectStorage | undefined readonly spanAttributes?: Record<string, unknown> | undefined readonly transformResultNames?: ((str: string) => string) | undefined readonly transformQueryNames?: ((str: string) => string) | undefined }

各字段的含义与影响(依据源码实现):

配置项作用源码依据
db直接传入SqlStorage句柄,用于普通查询。仅提供db不支持事务makeconst db = options.storage?.sql ?? options.db
storage传入完整DurableObjectStorage,事务会通过storage.transaction路由,是withTransaction生效的前提makeStorageBackedWithTransaction
spanAttributes追加到 SQL span 的标签,实现会固定注入db.system.name = "sqlite"spanAttributes: [...(options.spanAttributes ? ... : []), [ATTR_DB_SYSTEM_NAME, "sqlite"]]
transformResultNames对结果行字段名做转换(默认走Statement.defaultTransforms的数组形态)transformRows分支
transformQueryNames对查询语句中的命名参数做转换,传给Statement.makeCompilerSqlitemakeCompilerSqlite(options.transformQueryNames)

当两者都不传时,make会直接Effect.die("SqliteClient.make requires either a Durable Object storage or sql storage"),属于构造期硬性校验。

客户端构造完成后,会同时以两个 tag 注册进 Effect Context:SqliteClient服务与通用的Client.SqlClient服务(见layer/layerConfig的实现),因此业务代码既可以用 Durable Object 专属 API,也可以完全面向通用SqlClient编程,保持可移植性。

值得注意的实现细节(源码注释明确说明):

  • Blob 归一化SqlStorage.exec返回的 SQLite blob 是ArrayBuffer,该客户端统一转换为Uint8Array(见runIteratorrunValues中的value instanceof ArrayBuffer ? new Uint8Array(value) : value);
  • updateValues不支持SqliteClient接口中将updateValues声明为never,与 SQLite 能力边界一致。

三、事务能力演进:从“明确报错”到 DurableObjectStorage 托管事务

CHANGELOG 中4.0.0-beta.88是 sqlite-do 自身最重要的一次功能变更:

Support Cloudflare Durable Object SQLite transactions by allowingSqliteClientto be configured withDurableObjectStorageand routingwithTransactionthroughstorage.transaction.

理解这次演进,需要先看 Durable Object SQLite 的约束:SQLite 存储是按 object id 隔离的,每个实例拥有独立数据库;并且 Cloudflare 不鼓励(部分环境禁止)通过BEGIN/COMMITSQL 管理事务,而应使用DurableObjectStorage.transaction的托管事务 API。因此客户端采用了双路径设计

withTransaction: options.storage ? makeStorageBackedWithTransaction(options.storage, connection, semaphore) : makeUnsupportedWithTransaction( "Transactions require Durable Object storage; pass ctx.storage as the storage option" )
  • 仅传dbwithTransaction返回一个直接Effect.fail的实现,错误消息明确提示“需要传入storage”。测试 Client.test.ts 中db-only transactions fail clearly without transaction SQL用例验证了这一点,并断言不会向存储发出任何BEGIN/COMMIT/ROLLBACK/SAVEPOINTSQL;
  • 传入storagewithTransaction将整个 Effect 包裹进storage.transaction((txn) => ...),利用 Cloudflare 托管事务,而不是生成BEGIN/COMMITSQL。测试storage-backed transactions use DurableObjectStorage.transaction断言storage.transactionCalls === 1且无事务型 SQL 泄漏到存储层。

3.1 串行化:单连接信号量

源码中客户端通过Semaphore.make(1)创建单许可信号量:

  • 普通查询走acquirer = semaphore.withPermits(1)(Effect.succeed(connection)),所有语句串行执行;
  • 事务会持有该许可直到事务作用域结束transactionAcquirer使用Effect.uninterruptibleMask获取许可,并通过Scope.addFinalizer在作用域结束时释放。

因此事务期间其他查询会被阻塞排队,这正是文档注释强调“保持事务短小、避免在无关工作上挂起事务”的原因。

3.2 失败与中断都要回滚

makeStorageBackedWithTransaction的托管事务包装了三条关键路径(均有测试覆盖):

  1. 业务失败回滚:事务内 Effect 以Exit.isFailure结束时调用txn.rollback(),随后重新抛出原始失败(storage-backed failed transactions roll back and re-emit the original failure断言错误原样为"boom"且数据未写入);
  2. 中断回滚:外部Fiber.interrupt时通过interrupted标志在事务回调中提前resolve(),配合Effect.onExit保证回滚后再放行(storage-backed interrupted transactions roll back before release);
  3. 禁止嵌套事务:事务作用域内再次调用withTransaction会直接失败,提示 “Nested transactions are not supported by Cloudflare Durable Object SQLite storage”(nested transactions fail clearly without savepoint SQL用例同时验证外层回滚被触发、且无 SAVEPOINT SQL 发出)。

四、结果取值扩展:valuesUnprepared

4.0.0-beta.86引入Statement.valuesUnprepared

AddStatement.valuesUnpreparedfor returning unprepared SQL statement rows as arrays.

对应到 sqlite-do 的连接层,SqliteClient.ts 的Connection同时实现了executeValuesexecuteValuesUnprepared,两者都走runValues——即直接消费sqlStorage.exec(...).raw()游标、按数组形态返回行数据(并完成 ArrayBuffer → Uint8Array 归一化)。这与默认的“对象行”路径(execute/executeUnprepared,将列名与值组装为对象)形成互补,适用于需要最原始行布局、避免键名组装开销的批处理场景。

五、错误分类:UniqueViolation 与 reason 化 SqlError

sqlite-do 的错误体系由 Effect 核心的 SqlError.ts 提供,CHANGELOG 记录了这条演进主线:

  • 4.0.0-beta.37:将SqlError收敛为reason-based结构,把原生失败分类为结构化 reason,拿不到原生码时回退到UnknownError
  • 4.0.0-beta.65:新增UniqueViolationreason,唯一约束冲突从宽泛的ConstraintError中独立出来;UniqueViolation.constraint尽量携带约束/索引/键标识,无法确定时回退为字符串"unknown"

sqlite-do 内部通过classifyError = classifySqliteError(cause, { message, operation })把所有执行错误统一送入classifySqliteError。从 SqlError.ts 的分类逻辑看,SQLite 家族的分类依据是:

判定方式命中 reason备注
code 等于"SQLITE_CONSTRAINT_UNIQUE"或 errno 等于2067UniqueViolationconstraintcause.constraint或消息中"UNIQUE constraint failed:"前缀提取
code 前缀匹配SQLITE_AUTHAuthenticationError也支持code & 0xff数值映射
code 前缀匹配SQLITE_PERMAuthorizationError
code 前缀匹配SQLITE_CONSTRAINTConstraintError
code 前缀匹配SQLITE_BUSY/SQLITE_LOCKEDLockTimeoutError便于上层做重试决策
code 前缀匹配SQLITE_CANTOPENConnectionError
其他UnknownError无法确认时兜底

SqlErrormessage优先取 reason 的 message,否则取_tagisRetryable委托给 reason 判定。测试中classifies native errors without stable sqlite codes as UnknownError用例使用一个直接throw new Error("boom")的假存储,验证了无稳定码时回退UnknownError的行为。这也解释了为何 CHANGELOG 里大量 sqlite-do 版本号只是随effect升级——错误分类的实际代码位于 effect 核心,sqlite-do 只是调用方。

六、迁移能力:SqliteMigrator 与 Durable Object 的本地 schema

SqliteMigrator.ts 复用了 Effect SQL 的通用Migrator(位于 Migrator.ts),提供runlayer两个入口:

export const run = ({ loader, schemaDirectory, table }: Migrator.MigratorOptions<R2>) => Migrator.make({})(...) export const layer = (options: Migrator.MigratorOptions<R>) => Layer.effectDiscard(run(options))

要点(来自模块注释与MigratorOptions定义):

  • 迁移默认记录在effect_sql_migrations表中,按<id>_<name>的文件/记录键约定加载,MigratorOptions支持schemaDirectory与自定义table
  • 加载器支持fromRecord(内存中的迁移 Effect 映射,见 Migrator.ts)与fromFileSystem(读取文件系统迁移目录);
  • 迁移同样需要 storage-backed 客户端:注释明确要求“对着对象实际使用的同一DurableObjectStorage-backed 客户端运行”,以便迁移在 Cloudflare 托管事务中执行;
  • 迁移按 object id 隔离,对某个对象执行不会影响其他实例;
  • 当前实现不会schemaDirectory写出 SQLite schema dump(与 PG 等驱动的dumpSchema行为不同);
  • 这些 SQL 迁移与 Cloudflare 的 Durable Object class 迁移是两回事,使用前必须先让 Durable Object 启用 SQLite 存储。

测试SqliteMigrator.run works with storage-backed transactions完整演示了链路:fromRecord({ "1_create": ... })创建表并插入数据 →run返回[[1, "create"]]effect_sql_migrations表中出现{ migration_id: 1, name: "create", created_at: "current_timestamp" }→ 且全程storage.transactionCalls === 1、无事务型 SQL 下发。

七、入口与模块层面的收尾变更

CHANGELOG 中还有两条影响包结构的变更:

  • 4.0.0-beta.103移除显式的./index入口。这与 package.json 中"./index": null"./*/index": null的导出配置吻合,统一通过顶层index.ts聚合导出;
  • 4.0.0-beta.44:将ServiceMap模块重命名为Context(属于 effect 核心的全局 API 整理,sqlite-do 的 CHANGELOG 一并记录)。

index.ts的导出结构很简洁(src/index.ts):

export * as SqliteClient from "./SqliteClient.ts" export * as SqliteMigrator from "./SqliteMigrator.ts"

八、使用范式与注意事项小结

综合源码、测试与 CHANGELOG,在 Durable Object 中接入该客户端的推荐范式为:

  1. 构造客户端时传入ctx.storageDurableObjectStorage,而不是只传ctx.storage.sql,否则withTransaction与迁移都会不可用;
  2. 在对象构造阶段通过SqliteMigrator.run/layer完成本地 schema 初始化,请求处理应等待迁移层完成;
  3. 用信号量串行化的视角规划事务:事务会独占连接,保持事务短小、不跨无关异步工作挂起,多语句写操作才需要事务;
  4. 依赖错误分类做重试时,优先处理UniqueViolation(如SQLITE_CONSTRAINT_UNIQUE/ errno 2067),再区分LockTimeoutErrorConstraintError等 reason;
  5. 涉及 blob 字段时,下游按Uint8Array取值;updateValues不可用。

完整可运行示例可直接参考 Client.test.ts 中的makeClient辅助函数与各it.effect用例,它们同时覆盖了普通查询、db-only 事务失败、storage-backed 事务成功/回滚/中断回滚、嵌套事务拒绝与迁移执行五类关键路径。

【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3code

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询