从 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时不支持事务 | make中const 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.makeCompilerSqlite | makeCompilerSqlite(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(见runIterator与runValues中的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 allowing
SqliteClientto 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" )- 仅传
db时:withTransaction返回一个直接Effect.fail的实现,错误消息明确提示“需要传入storage”。测试 Client.test.ts 中db-only transactions fail clearly without transaction SQL用例验证了这一点,并断言不会向存储发出任何BEGIN/COMMIT/ROLLBACK/SAVEPOINTSQL; - 传入
storage时:withTransaction将整个 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的托管事务包装了三条关键路径(均有测试覆盖):
- 业务失败回滚:事务内 Effect 以
Exit.isFailure结束时调用txn.rollback(),随后重新抛出原始失败(storage-backed failed transactions roll back and re-emit the original failure断言错误原样为"boom"且数据未写入); - 中断回滚:外部
Fiber.interrupt时通过interrupted标志在事务回调中提前resolve(),配合Effect.onExit保证回滚后再放行(storage-backed interrupted transactions roll back before release); - 禁止嵌套事务:事务作用域内再次调用
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:
Add
Statement.valuesUnpreparedfor returning unprepared SQL statement rows as arrays.
对应到 sqlite-do 的连接层,SqliteClient.ts 的Connection同时实现了executeValues与executeValuesUnprepared,两者都走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 等于2067 | UniqueViolation | constraint从cause.constraint或消息中"UNIQUE constraint failed:"前缀提取 |
code 前缀匹配SQLITE_AUTH | AuthenticationError | 也支持code & 0xff数值映射 |
code 前缀匹配SQLITE_PERM | AuthorizationError | |
code 前缀匹配SQLITE_CONSTRAINT | ConstraintError | |
code 前缀匹配SQLITE_BUSY/SQLITE_LOCKED | LockTimeoutError | 便于上层做重试决策 |
code 前缀匹配SQLITE_CANTOPEN | ConnectionError | |
| 其他 | UnknownError | 无法确认时兜底 |
SqlError的message优先取 reason 的 message,否则取_tag;isRetryable委托给 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),提供run与layer两个入口:
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 中接入该客户端的推荐范式为:
- 构造客户端时传入
ctx.storage(DurableObjectStorage),而不是只传ctx.storage.sql,否则withTransaction与迁移都会不可用; - 在对象构造阶段通过
SqliteMigrator.run/layer完成本地 schema 初始化,请求处理应等待迁移层完成; - 用信号量串行化的视角规划事务:事务会独占连接,保持事务短小、不跨无关异步工作挂起,多语句写操作才需要事务;
- 依赖错误分类做重试时,优先处理
UniqueViolation(如SQLITE_CONSTRAINT_UNIQUE/ errno 2067),再区分LockTimeoutError、ConstraintError等 reason; - 涉及 blob 字段时,下游按
Uint8Array取值;updateValues不可用。
完整可运行示例可直接参考 Client.test.ts 中的makeClient辅助函数与各it.effect用例,它们同时覆盖了普通查询、db-only 事务失败、storage-backed 事务成功/回滚/中断回滚、嵌套事务拒绝与迁移执行五类关键路径。
【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3code
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考