ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

@effect/sql-sqlite-bun 4.0 变更全解析:从并发锁策略到结构化错误分类的演进

@effect/sql-sqlite-bun 4.0 变更全解析:从并发锁策略到结构化错误分类的演进 effect/sql-sqlite-bun 4.0 变更全解析从并发锁策略到结构化错误分类的演进【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3code本篇指南以effect/sql-sqlite-bun包的 CHANGELOG.md 为骨架梳理该 Effect SQL SQLite 客户端在 v4 预发布周期内4.0.0-beta.0至4.0.0-rc.112的关键行为变更并结合 SqliteClient.ts、SqliteMigrator.ts、Client.test.ts 与 SqlError.ts 的源码深入讲解每个变更背后的实现原理与工程动机。读完你将掌握Bun 运行时下 Effect SQL 客户端的并发与事务策略、只读模式与超时参数的实际取值逻辑、结构化SqlError分类体系的演进路径以及迁移模块的使用方式。一、包定位Bun 运行时上的 Effect SQL 客户端effect/sql-sqlite-bun是 Effect 官方 SQL 生态中专用于Bun 运行时的 SQLite 适配层底层直接构建在bun:sqlite之上。其 package.json 中的描述为 A SQLite toolkit for Effect声明了对effect的 peer 依赖版本号与主库effect保持同步当前仓库锁定的主库版本为effect4.0.0-rc.112见根目录 pnpm-workspace.yaml。从 src/index.ts 可以看到包只导出两个命名空间模块SqliteClient核心客户端负责打开数据库、执行语句、事务管理与错误分类SqliteMigrator迁移执行器复用 Effect 共享的迁移机制。该模块头部的文档注释SqliteClient.ts 第 1-15 行概括了它的核心契约串行化数据库访问、默认启用 WAL 模式、默认等待忙库最多五秒、显式事务使用BEGIN IMMEDIATE并明确指出流式查询streaming与updateValues不受支持。这些能力与限制正是 CHANGELOG 中多条变更的落点。二、版本谱系从 beta.0 到 rc.112 的演进脉络CHANGELOG 完整记录了该包自4.0.0-beta.0对应 PR #1183 v4 beta以来的全部发布记录共 100 余个预发布版本。版本号分两段演进阶段版本区间性质Beta 阶段4.0.0-beta.0→4.0.0-beta.107功能开发与破坏性变更Major Changes集中发生RC 阶段4.0.0-rc.108→4.0.0-rc.112收敛期以依赖同步与补丁修复为主值得注意的是绝大多数条目是 Updated dependencies 形式的依赖升级记录——这是 changesets 自动生成的产物代表effect主库的每次迭代都会带动全部 SQL 适配包同步发版。真正体现effect/sql-sqlite-bun自身行为变化的是其中少数带有 PR 链接的Patch Changes说明它们是本文接下来重点剖析的对象。三、并发与事务策略五秒忙等待与立即事务beta.1074.0.0-beta.107版本引入了一条对本包行为影响最深的变更PR #7162Use a configurable five-second busy timeout and immediate transactions by default to avoid SQLite lock failures under concurrent access. Busy waits can block the event loop, while immediate transactions serialize behind other writers.这句话浓缩了两个核心决策下面结合源码逐一验证。3.1 可配置的五秒忙等待超时在 SqliteClient.ts 第 138-142 行忙等待超时通过以下公式计算并写入PRAGMA busy_timeoutconst busyTimeout Math.min( MAX_BUSY_TIMEOUT, Math.max(0, Math.round(Duration.toMillis(options.busyTimeout ?? Duration.seconds(5)))) ) db.run(PRAGMA busy_timeout ${busyTimeout};)其中MAX_BUSY_TIMEOUT 2_147_483_647第 34 行这是 SQLite 内部接受的最大毫秒数。也就是说默认值未配置时取Duration.seconds(5)即 5000ms最小值下限Math.max(0, ...)保证非负最大值上限Duration.infinity会被钳制到MAX_BUSY_TIMEOUT即约 24.8 天避免溢出可配置项对应SqliteClientConfig.busyTimeout?: Duration.Input第 100 行。这些行为在 Client.test.ts 第 11-22 行有直接断言// 默认 5 秒 assert.deepStrictEqual(yield* sqlPRAGMA busy_timeout, [{ timeout: 5000 }]) // 自定义 1 秒 const custom yield* SqliteClient.make({ filename: :memory:, busyTimeout: 1 second }) assert.deepStrictEqual(yield* customPRAGMA busy_timeout, [{ timeout: 1000 }]) // infinity 被钳制到 SQLite 最大值 const infinite yield* SqliteClient.make({ filename: :memory:, busyTimeout: Duration.infinity }) assert.deepStrictEqual(yield* infinitePRAGMA busy_timeout, [{ timeout: 2_147_483_647 }])CHANGELOG 中 Busy waits can block the event loop 的警告是真实存在的bun:sqlite是同步 API忙等待期间事件循环会被阻塞。这是选用此适配层时必须接受的运行时特性也是为什么文档建议在高并发写场景下评估其他运行时方案。3.2 立即事务BEGIN IMMEDIATE同一变更将显式事务的开启语句从延迟事务改为立即事务。在 SqliteClient.ts 第 238 行创建客户端时传入了beginTransaction: BEGIN IMMEDIATE,普通BEGIN延迟事务在首次读时才尝试获取读锁一旦后续要写需要把读锁升级为写锁而 SQLite 不允许就地升级会直接返回SQLITE_BUSY。BEGIN IMMEDIATE则在事务开始时就获取写锁将事务串行化排在其它写入者之后——即使事务内部只做读操作也是如此。CHANGELOG 原文 immediate transactions serialize behind other writers 指的就是这一行为。唯一的例外是readonly: true打开的客户端不受影响见下一节。Client.test.ts 第 24-45 行用两个客户端竞争验证了这一语义客户端client持有事务时竞争者contender执行BEGIN IMMEDIATE会失败并抛出_tag SqlError、cause 匹配/database is locked/i的错误。四、只读模式强制化与写入拒绝beta.1044.0.0-beta.104版本的变更PR #6993非常简短Enforce read-only mode when opening Bun SQLite databases.对照源码它在 SqliteClient.ts 第 130-136 行的数据库打开逻辑中落地const readonly options.readonly true const db new Database(options.filename, { readonly, readwrite: readonly ? false : options.readwrite ?? true, create: readonly ? false : options.create ?? true } as any)解读这一实现readonly: true时readwrite与create被强制设为false即不能创建文件、不能以读写模式打开默认非只读时readwrite与create均默认true与 SQLite 常规语义一致只读模式下WAL 不会被启用第 144 行if (options.disableWAL ! true !readonly)因为开启 WAL 需要写权限。测试 Client.test.ts 第 47-71 行验证先用可写客户端建表再用readonly: true打开同一文件读与事务内读均正常返回空结果而INSERT会失败为SqlErrorcause 匹配/attempt to write a readonly database/i。五、错误模型演进从缺陷到结构化 SqlErrorCHANGELOG 中关于错误处理有三条相互关联的变更构成一条清晰的演进线索5.1 语句准备失败转为类型化错误beta.884.0.0-beta.88PR #2399Fail with a typedSqlErrorwhen Bun SQLite statement preparation throws (for example a missing table or a syntax error), instead of letting the driver error escape as a defect, closes #2385.此前的实现中db.query(sql)准备语句时抛出的原生异常可能以 defect未捕获缺陷形式逃逸无法被 Effect 的错误通道处理。变更后所有执行路径都包裹在try/catch中并通过Effect.fail(new SqlError(...))返回。看 SqliteClient.ts 第 155-181 行的run与runValues两个核心函数这一模式非常清晰try { return Effect.succeed((prepare(sql, useSafeIntegers).all(...(params as any)) ?? []) as Arrayany) } catch (cause) { return Effect.fail(new SqlError({ reason: classifyError(cause, Failed to execute statement, execute) })) }错误分类由classifySqliteError完成第 36-37 行它定义在 Effect 共享层 SqlError.ts 中。同一模式还用于exportdb.serialize()与loadExtensiondb.loadExtension(path)两条路径第 204-213 行。5.2 基于 reason 的错误形状统一beta.374.0.0-beta.37PR #1812Consolidate the SqlError changes to the new reason-based shape across effect and the SQL drivers, classifying native failures into structured reasons with Unknown fallback where native codes are unavailable.这一步把错误模型统一为 reason-based 结构SqlError是外层包装内部通过reason字段承载分类结果。从 SqlError.ts 的导出可以看到完整的 reason 类型族测试 SqlError.test.ts 第 24-40 行逐一验证了每个类型的 tag 与可重试性Reason 类型_tagisRetryable含义ConnectionErrorConnectionErrortrue连接/打开失败AuthenticationErrorAuthenticationErrorfalse认证失败AuthorizationErrorAuthorizationErrorfalse授权/权限失败SqlSyntaxErrorSqlSyntaxErrorfalseSQL 语法错误UniqueViolationUniqueViolationfalse唯一约束冲突带 constraint 字段ConstraintErrorConstraintErrorfalse其他约束违规DeadlockErrorDeadlockErrortrue死锁SerializationErrorSerializationErrortrue序列化失败LockTimeoutErrorLockTimeoutErrortrue锁等待超时StatementTimeoutErrorStatementTimeoutErrortrue语句执行超时UnknownErrorUnknownErrorfalse原生错误码无法归类时的兜底isRetryable是这套模型的工程价值所在并发类错误连接、死锁、序列化、锁超时、语句超时标记为可重试配合 Effect 的重试机制可以安全地自动恢复而语法、认证、授权、约束类错误重试无意义直接暴露给调用方。5.3 新增 UniqueViolation reasonbeta.654.0.0-beta.65PR #2148AddUniqueViolationas a new SQL error reason. Supported unique constraint violations now classify asUniqueViolationinstead of the broaderConstraintErrorreason...UniqueViolation.constraintcontains the best available constraint, index, or key identifier and falls back to exactlyunknownwhen no reliable identifier is available.这是对 5.2 分类体系的细化将唯一约束冲突从宽泛的ConstraintError中拆分出来。其结构定义在 SqlError.ts 第 133-136 行在通用ReasonFieldscause、message、operation之外增加了constraint: Schema.String字段。CHANGELOG 明确该分类覆盖 PostgreSQL、PGlite、MySQL、MSSQL 以及 SQLite 系列客户端共享的 SQLite 分类逻辑当原生错误无法提供可靠标识符时constraint精确回退为字符串unknown。测试 SqlError.test.ts 第 113-120 行构造了code: 23505PostgreSQL 唯一约束冲突码的用例来验证该 reason 的识别。六、API 演进移除 index 入口与新增 unprepared 取值除了错误模型CHANGELOG 还记录了两条 API 层面的变更4.0.0-beta.103PR #6701Removed explicit ./index entrypoints。这一变更在 package.json 的exports字段中有直接对应——./index: null与./*/index: null显式禁用了 index 入口发布产物只暴露.与./*两级路径。4.0.0-beta.86PR #2462AddStatement.valuesUnpreparedfor returning unprepared SQL statement rows as arrays。这一能力落地在连接层对应 SqliteClient.ts 第 195-197 行的executeValuesUnprepared方法内部复用runValues走statement.values()以数组形式返回行因此也继承了一致的SqlError分类与SafeIntegers处理。七、迁移模块复用共享机制执行 SQL 迁移SqliteMigrator模块SqliteMigrator.ts虽然不在 CHANGELOG 的明细变更列表中但它是该包开箱即用的组成模块值得一并说明第 20 行export * from effect/unstable/sql/Migrator直接转发共享的迁移加载器与错误类型run(options)第 28-36 行基于Migrator.make(...)构造使用当前SqlClient执行待应用的迁移文件返回ReadonlyArray[id, name]已应用迁移的编号与名称失败类型为MigrationError | SqlErrorlayer(options)第 84-87 行在 Layer 构造期间执行迁移Layer.effectDiscard(run(options))适合在应用启动时通过依赖注入自动完成建表等操作。源码中保留了被注释掉的 Bun 特定 schema dump 实现第 35-75 行注释明确说明当前不提供 Bun 特定的 schema dump 支持迁移执行完全依赖共享的 SQL migrator——这也解释了为什么该模块如此轻量。八、在 t3code 仓库中的上下文与使用方式该包作为参考仓库effect-smol的一部分被纳入 t3code 仓库的.repos/目录完整路径 .repos/effect-smol/packages/sql/sqlite-bun用于对照参考 Effect 官方 SQL 生态的实现。其 peer 依赖版本effect4.0.0-rc.112与根仓库 pnpm-workspace.yaml 中 catalog 锁定的effect: 4.0.0-rc.112完全一致说明两者处于同一 Effect v4 RC 版本线。如需在 Bun 项目中安装使用该客户端READMEREADME.md给出的命令是npm install effectrc effect/sql-sqlite-bunrc最小使用模式基于 SqliteClient.ts 的公开 API 与 Client.test.ts 的用法import { Effect } from effect import { Reactivity } from effect/unstable/reactivity import { SqliteClient } from effect/sql-sqlite-bun const program Effect.gen(function*() { const sql yield* SqliteClient.make({ filename: app.db }) const rows yield* sqlSELECT * FROM users return rows }) program.pipe(Effect.provide(Reactivity.layer))几点取自源码的实际使用提示make需要Scope与Reactivity环境第 121 行类型签名测试中统一通过Effect.provide(Reactivity.layer)满足后者连接关闭通过Effect.addFinalizer(() Effect.sync(() db.close()))保证第 137 行同一客户端内部通过Semaphore.make(1)串行化连接访问第 217 行事务获取连接使用uninterruptibleMask包裹并绑定作用域 finalizer 释放信号量第 221-231 行客户端同时提供SqliteClient与通用SqlClient两个服务 taglayer/layerConfig第 260-288 行layerConfig额外支持从 EffectConfig读取配置可用配置项完整清单见 SqliteClientConfig 第 89-106 行filename、readonly、create、readwrite、disableWAL、busyTimeout、spanAttributes、transformResultNames、transformQueryNames。九、变更时间线速查版本变更类型核心内容4.0.0-beta.0Majorv4 beta 启动PR #11834.0.0-beta.37PatchSqlError 统一为 reason-based 结构原生失败归类为结构化 reason无法归类时回退Unknown4.0.0-beta.65Patch新增UniqueViolationreason唯一约束冲突从ConstraintError中独立constraint缺失时回退unknown4.0.0-beta.86Patch新增valuesUnprepared/executeValuesUnprepared数组取值能力4.0.0-beta.88Patch语句准备失败缺表、语法错误等转为类型化SqlError不再以 defect 逃逸4.0.0-beta.103Patch移除显式./index入口4.0.0-beta.104Patch打开 Bun SQLite 数据库时强制执行只读模式4.0.0-beta.107Patch默认五秒忙等待超时 BEGIN IMMEDIATE立即事务缓解并发访问下的锁失败4.0.0-rc.108→4.0.0-rc.112Patch仅依赖升级随effect主库同步发版十、小结纵观4.0.0-beta.0到4.0.0-rc.112的完整变更记录effect/sql-sqlite-bun在 v4 周期内完成了三条主线演进并发正确性五秒忙等待、BEGIN IMMEDIATE立即事务、强制只读模式、错误模型现代化reason-based 结构化分类、UniqueViolation细化、语句准备错误类型化、以及API 收敛移除 index 入口、新增 unprepared 数组取值。其中每条行为变更都能在 SqliteClient.ts 的实现与 Client.test.ts、SqlError.test.ts 的测试中得到印证。对于在 Bun 上使用 Effect SQL 的开发者而言这些变更直接决定了事务该如何写、只读连接该如何开、错误该如何捕获与重试——理解它们就是在理解这个客户端最核心的工程契约。【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3code创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

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