@effect/sql-pglite 演进全解析:在 Effect 中集成 WASM 版 PostgreSQL(PGlite)客户端
【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3code
本篇技术指南以@effect/sql-pglite包的 CHANGELOG.md 为主线,结合该包在 effect-smol 仓库中的源码与测试,系统讲解它如何把 PGlite(WASM 构建的 PostgreSQL)接入 Effect 的 SQL 客户端体系。读完你将掌握该包的核心能力清单(Postgres 方言编译、基于 savepoint 的事务、LISTEN/NOTIFY、数据目录导出、迁移器)、SQL 错误分类体系、关键 API 演进(如valuesUnprepared与UniqueViolation),并了解其与 effect 核心包的版本同步机制。
一、包的定位:从 Changelog 首次发布看设计意图
@effect/sql-pglite在 Changelog 中的4.0.0-beta.57条目(CHANGELOG.md 中的 Minor Changes)给出了该包最权威的定位声明:
新增
@effect/sql-pglite包,封装@electric-sql/pglite,并配套 Effect SQL 客户端能力:Postgres 方言、通过 savepoint 实现的 Effect 托管事务、listen/notify、dumpDataDir/refreshArrayTypes,以及一个 Migrator。
PGlite 是 PostgreSQL 的 WASM 构建,可在浏览器、Node.js 与 Bun 中运行(见 README.md)。这意味着该包承担了两个关键职责:
- 桥接:把 PGlite 实例(
PGliteInterface)转化为符合 EffectSqlClient契约的客户端; - 增强:在通用 SQL 客户端之上补充 PGlite 特有的操作(监听通知、数据目录导出、数组类型刷新)。
在package.json(package.json)中可以看到它仅依赖@electric-sql/pglite(^0.5.6),而effect作为 peerDependency,从依赖设计上就决定了“核心逻辑在 effect,PGlite 只是接入方”的分层。
二、安装与包结构
安装命令(来自 README.md):
npm install effect@rc @effect/sql-pglite@rc注意安装的是 rc 预发布版本,与 Changelog 中4.0.0-rc.*/4.0.0-beta.*的版本命名一致。包的源码结构非常精简(src 目录):
PgliteClient.ts:客户端主体,包含make/fromClient/layer等构造器与 Layer、SQL 语句编译器、错误分类逻辑;PgliteMigrator.ts:迁移器,复用 effect 的共享迁移加载器;index.ts:统一出口,分别导出PgliteClient与PgliteMigrator两个命名空间。
三、核心能力清单与源码印证
3.1 两种客户端构建方式:托管实例 vs 外部实例
从 PgliteClient.ts 的类型定义看,PgliteClientConfig分为Create与Live两种形态:
Create:传入 PGlite 构造选项(PGliteOptions),由make内部创建受 Scope 管理的实例——Effect.acquireRelease创建并在作用域结束时close()(带 1000ms 超时),生命周期完全由 Effect 托管;Live:传入调用方已创建好的liveClient,此时实例归调用方所有,Effect 客户端不会关闭它。
对应的两个构造器是make(options)与fromClient(options),配合三个 Layer 工厂使用:
layer(config?):基于具体配置创建 Layer;layerFrom(acquire):基于任意 acquire Effect 创建 Layer;layerConfig(config):基于 EffectConfig(支持从环境变量等读取配置)创建 Layer。
三者都会同时提供PgliteClient与通用SqlClient两个服务标签(Context.make(PgliteClient, client) + Context.add(Client.SqlClient, client)),因此下游业务代码既可以按需访问 PGlite 特有能力,也可以只依赖标准的SqlClient。
3.2 Postgres 方言编译器
makeCompiler生成符合 PostgreSQL 语法习惯的语句编译器(PgliteClient.ts):
- 参数占位符使用 PostgreSQL 的
$1、$2形式; - 标识符使用双引号转义(
escape基于Statement.defaultEscape("\"")); - 支持
onRecordUpdate(生成(values ...) AS alias(cols) RETURNING ...形式的批量更新); - 支持 JSON 自定义片段(
PgJson),可选的transformJson会对接入的 JSON 值做名称转换。
测试用例(Client.test.ts)对这些编译行为给出了可直接验证的断言,例如:
const [query, params] = sql`INSERT INTO people ${sql.insert({ name: "Tim", age: 10 })}`.compile() // query === `INSERT INTO people ("name","age") VALUES ($1,$2)` // params === ["Tim", 10]以及in助手、and助手、updateValues(生成FROM (values ($1),($2)) AS data("name"))等常用片段。
3.3 基于 savepoint 的 Effect 托管事务
Changelog 明确将“Effect-managed transactions via savepoints”列为核心能力。源码中transactionAcquirer使用Effect.uninterruptibleMask+ 信号量(Semaphore.makeUnsafe(1))保证同一时刻只有一个事务持有连接,并把信号量释放注册为当前作用域的 finalizer;嵌套事务则通过 savepoint 实现。
Transaction.test.ts 覆盖了四类典型场景,可作为理解其语义的权威参考:
- 事务内提交(
withTransaction commit):插入的数据在提交后可见; - 事务内回滚(
withTransaction rollback):Effect 失败后整条记录消失; - 嵌套事务成功:外层与内层同时提交,两条记录都在;
- 嵌套事务回滚:内层失败只回滚到 savepoint,外层记录保留(最终只有 1 条);
- 并发嵌套事务:即使内层并发且部分失败,成功分支的数据依然保留。
3.4 listen/notify、dumpDataDir 与 refreshArrayTypes
PgliteClient接口在标准SqlClient之上扩展了四个 PGlite 专属能力(PgliteClient.ts):
| 成员 | 签名 | 说明 |
|---|---|---|
json | (_: unknown) => Fragment | 构造 JSON 参数片段,配合::jsonb使用 |
listen | (channel: string) => Stream<Stream<string, SqlError>> | 订阅频道,返回 Effect Stream |
notify | (channel: string, payload: string) => Effect<void, SqlError> | 发送通知(内部执行转义后的NOTIFY,payload 会做单引号转义) |
dumpDataDir | (compression?: "none" \| "gzip" \| "auto") => Effect<File \| Blob, SqlError> | 导出整个数据目录 |
refreshArrayTypes | Effect<void, SqlError> | 刷新数组类型注册 |
这些操作统一经过信号量串行化(semaphore.withPermit),避免与常规查询并发时产生竞争。测试中可以看到典型用法:Client.test.ts 演示了listen("ch1", ...)+notify("ch1", "hello")的配对流程,以及创建mood[]枚举数组类型后调用refreshArrayTypes再插入数组值。
3.5 迁移器(Migrator)
PgliteMigrator.ts 导出run与layer:
run(options):基于当前SqlClient执行待应用的迁移文件,返回已应用迁移的[id, name]列表;它不需要独立的 PGlite 服务,连接由活跃的SqlClient提供;layer(options):在 Layer 构建期间执行迁移(Layer.effectDiscard(run(options)))。
迁移器复用 effect 的共享Migrator模块(export * from "effect/unstable/sql/Migrator"),并依赖统一的effect_sql_migrations记录表。Migrator.test.ts 验证了迁移按migration_id顺序执行并写入记录表;PersistedQueue.test.ts 则展示了迁移器的另一个用途——为PersistedQueue持久化队列自动建表并确保只记录一次迁移。
四、SQL 错误分类体系:UniqueViolation的引入
Changelog4.0.0-beta.65条目记录了一次重要的错误分类增强:
新增
UniqueViolation作为新的 SQL 错误原因。受支持的唯一约束冲突现在归类为UniqueViolation,而不再落入更宽泛的ConstraintError。UniqueViolation.constraint保存可用的约束/索引/键标识符,当无法取得可靠标识符时回退为"unknown"。该分类覆盖 PostgreSQL、PGlite、MySQL、MSSQL 以及 SQLite 家族的共享分类逻辑。
在 PgliteClient.ts 的classifyError中可以看到完整的映射表,它依据 PostgreSQL 的 SQLSTATE 错误码前缀分类:
| SQLSTATE | 分类结果 |
|---|---|
08xx | ConnectionError(连接错误) |
28xx | AuthenticationError(认证错误) |
42501 | AuthorizationError(权限错误) |
42xx | SqlSyntaxError(语法错误) |
23505 | UniqueViolation(唯一约束冲突,携带 constraint) |
23xx(其余) | ConstraintError(一般完整性约束) |
40P01 | DeadlockError |
40001 | SerializationError |
55P03 | LockTimeoutError |
57014 | StatementTimeoutError |
| 其他 | UnknownError |
约束名的提取有精细的兜底逻辑(PgliteClient.ts 的pgConstraintFromCause):约束缺失、非字符串或全空白时统一回退为"unknown",有效值会先trim再返回。SqlErrorClassification.test.ts 正是围绕这三点写的测试:" users_email_key "会被修剪为users_email_key,而缺约束 / 数字约束 / 空白约束均返回"unknown",23503(外键)则保持ConstraintError不变。
五、关键 API 演进:valuesUnprepared与入口调整
Changelog 记录了若干影响使用方式的 API 变化:
4.0.0-beta.86:新增Statement.valuesUnprepared,用于以数组形式返回未预编译 SQL 语句的行。对应实现在 Statement.ts 中,PgliteClient的底层连接通过executeValuesUnprepared(内部以rowMode: "array"查询)支撑它,返回ReadonlyArray<ReadonlyArray<unknown>>。4.0.0-beta.103:移除了显式的./index入口("Removed explicit ./index entrypoints")。这与 package.json 中的 exports 配置相呼应——"./index": null、"./*/index": null明确禁用了旧入口,导入请使用包根或具体子路径。4.0.0-rc.112:更新生产依赖到最新版本(由@tim-smart提交),属于常规的依赖刷新。
六、版本节奏与 effect 核心的同步机制
通读整个 Changelog 可以发现一个明显规律:几乎每个版本的 "Patch Changes" 都只包含 "Updated dependencies" 指向effect@4.0.0-x.y.z,这说明@effect/sql-pglite与 effect 核心走的是同版本号、同步发布策略。实际含义是:
- 每次 effect 核心更新,所有 SQL 包统一跟随升级,避免版本矩阵错位;
- 该包自身几乎没有独立功能变更——真正的 SQL 客户端基础设施(
SqlClient、SqlError、Statement、Migrator)都沉淀在 effect 核心中,本包只做 PGlite 的适配层; - 因此升级时应保持
effect与@effect/sql-pglite版本一致(如均为4.0.0-rc.112),这从 package.json 的 peerDependencies(effect: workspace:^)也能看出。
从源码结构看,这种“核心在 effect、适配在 sql/*”的组织方式同样存在于packages/sql/pg、mysql2、sqlite等兄弟包(它们共享同一套SqlError分类与valuesUnprepared特性,可从各自 CHANGELOG 中看到同名条目)。
七、使用建议与注意事项
综合 Changelog、源码与测试,落地使用时有几点值得注意:
- 生命周期:优先使用
layer()/layerConfig()让 Effect 托管 PGlite 实例(Scope 结束自动关闭);若使用fromClient({ liveClient })包装外部实例,务必自行负责关闭,客户端不会替你清理。 - 事务语义:嵌套事务基于 savepoint,内层失败只回滚到 savepoint;并发嵌套事务受信号量保护,同一连接不会同时跑多个事务。
- 错误处理:针对唯一约束冲突,直接匹配
UniqueViolation并读取constraint字段即可拿到约束名(可能为"unknown");其他完整性错误仍为ConstraintError。 - 版本对齐:安装时让
effect与@effect/sql-pglite保持同版本(当前均为4.0.0-rc.112),并留意 Changelog 中 "Updated dependencies" 列出的 effect 版本,这是判断兼容性的最快途径。 - 持久化队列等扩展场景:
PgliteMigrator与PersistedQueue可组合使用,迁移器会自动为队列建表并幂等记录迁移(见 PersistedQueue.test.ts)。
八、总结
@effect/sql-pglite是一个“小而精”的适配包:Changelog 中绝大部分条目是跟随 effect 核心的依赖同步,仅有的几条独立变更(包首发、UniqueViolation分类、valuesUnprepared、入口清理)恰好勾勒出它的全部技术边界。结合 PgliteClient.ts、PgliteMigrator.ts 与 test 目录,即可在浏览器、Node.js 或 Bun 中,用完全符合 Effect 生态惯用法的类型安全方式操作一个嵌入式的、无外部进程依赖的 PostgreSQL 数据库。
【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3code
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考