WrenAI 浏览器端分析引擎 wren-core-wasm 完全指南:基于 DataFusion 的 WASM 语义层与 SQL 执行
【免费下载链接】WrenAIGenBI (Generative BI) for AI agents, an open-source, governed text-to-SQL through an open context layer that turns natural-language questions into trusted dashboards, charts, and SQL across 20+ data sources, such as BigQuery, Snowflake, PostgreSQL, ClickHouse, Amazon Redshift, Databricks and more.项目地址: https://gitcode.com/GitHub_Trending/wr/WrenAI
wren-core-wasm 是 WrenAI 项目中一个将 Wren Engine 编译为 WebAssembly 的独立 crate,让浏览器可以在纯客户端环境中通过 MDL(Modeling Definition Language)语义层直接对 Parquet / CSV / JSON 数据执行 SQL 查询,底层由 DataFusion 提供查询能力。本文以 core/wren-core-wasm/.claude/CLAUDE.md 为核心骨架,结合 src/lib.rs、TypeScript SDK、justfile 与集成测试 等仓库源码,系统讲解该模块的架构设计、WASM 专属约束、MDL 加载模式、API 使用方式与构建测试流程,读完即可上手把语义层分析能力嵌入自己的浏览器应用。
一、wren-core-wasm 是什么
wren-core-wasm 是 Wren Engine 的 WebAssembly 版本,用于浏览器原生分析(browser-native analytics)。它的工作模式可以概括为:在浏览器端直接执行 SQL 查询,数据来源于 Parquet / CSV / JSON 文件,查询路径要经过 MDL 语义层,且整个执行过程完全发生在客户端,不需要任何服务端计算。
浏览器 JS ├── registerParquet(name, bytes) → Arrow RecordBatch → MemTable ├── registerJson(name, json) → Arrow JSON reader → MemTable ├── loadMDL(mdl_json, source) → AnalyzedWrenMDL + 表解析 │ URL 模式: source="https://..." → ListingTable(走 HttpStore) │ 本地模式: source="./..." → 期望预先注册好的表 │ 回退模式: source="" → 从 tableReference 自动检测 └── query(sql) → MDL 重写 → DataFusion 执行 → JSON 结果从源码结构看,该模块的核心价值在于把 WrenAI 的语义层能力(MDL 建模、cube 查询)从服务端搬进了浏览器,为"静态站点 + 本地数据文件"形态的分析应用(例如 examples 下的演示页)提供了完整的执行链路。
二、为什么独立成 crate
CLAUDE.md 明确指出,wren-core-wasm 之所以独立于wren-core/workspace,主要基于两个原因:
- 使用上游 DataFusion(crates.io 上的 v53),而非 Canner fork。WASM 版本直接通过 DataFusion 执行查询,不需要 SQL unparser 或方言转译(dialect transpilation),因此不需要 Canner fork 中针对 unparser 的修复。
- 避免依赖冲突:该 crate 被放在
wren-core/workspace 之外,防止两套 DataFusion 依赖树相互干扰。
在 Cargo.toml 中可以验证这一点——datafusion = { version = "53", default-features = false, ... },注释明确写着 "DataFusion: upstream latest (NOT Canner fork)"。而 WrenAI 仓库根目录下的wren-core/core则使用 Canner fork 版本,两者用途不同:native 端需要生成目标方言 SQL,WASM 端则把 DataFusion 作为最终执行引擎。
同时它复用了两个共享代码库:
- wren-core-base(
../wren-core-base):共享的 manifest 类型定义,不依赖 DataFusion; - wren-core语义层(
../wren-core/core):提供 MDL 分析规则,例如AnalyzedWrenMDL::analyze_with_tables与apply_wren_on_ctx。
三、核心源码结构与 WrenEngine 生命周期
CLAUDE.md 指出 crate 是"单文件 crate",所有核心逻辑集中在 src/lib.rs。WrenEngine是暴露给 JS 的唯一门面结构体,内部持有三个关键成员(见 lib.rs#L39-L52):
#[wasm_bindgen] pub struct WrenEngine { ctx: datafusion::execution::context::SessionContext, analyzed_mdl: Option<std::sync::Arc<wren_core::mdl::AnalyzedWrenMDL>>, runtime: tokio::runtime::Runtime, // 单线程 tokio 运行时 }ctx:DataFusion 的会话上下文,负责 SQL 解析、规划与执行;analyzed_mdl:loadMDL之后保留的分析结果,供cubeQuery/listCubes读取 manifest;runtime:单线程 tokio 运行时,这是 WASM 环境下的关键设计(详见本文"WASM 特定约束"一节)。
整个 API 通过#[wasm_bindgen(js_name = camelCase)]映射为 JS 侧的 camelCase 方法名。接下来按生命周期逐一讲解。
3.1 初始化:WrenEngine.new()→ SessionContext
new()构造器(lib.rs#L62-L89)做了三件事:
- 调用
console_error_panic_hook::set_once(),把 Rust panic 消息输出到浏览器 console,避免只看到模糊的RuntimeError: unreachable; - 创建
SessionConfig::new().with_target_partitions(1)——强制单分区,适配 WASM 单线程环境; - 把会话时区设置为 UTC(
+00:00),保证浏览器端的时间戳推断与比较和 native 侧create_wren_ctx行为一致; - 构建
tokio::runtime::Builder::new_current_thread()单线程运行时。
这里有一个容易被忽视的实现细节:为什么 WASM 里还要一个 tokio 运行时?因为 DataFusion 的物理算子(例如CoalescePartitionsExec,它会包裹任何多分区计划如UNION ALL/INTERSECT/EXCEPT)内部会调用tokio::task::spawn。如果spawn不在一个活着的 tokio 运行时上下文中执行,就会 panic 报there is no reactor running。wasm-bindgen-futures本身并不提供 tokio 调度器上下文,所以WrenEngine自己持有一个 current-thread runtime,并在查询时用runtime.block_on(...)驱动 future。集成测试 test_union_all_does_not_trap 正是为这个问题的回归而写。
3.2 数据注册:registerJson / registerParquet / registerCsv
在本地模式下,物理表需要先注册进引擎,之后才能被loadMDL引用。三个注册方法都遵循同一个模式:把输入数据读成 Arrow RecordBatch → 包成 DataFusion MemTable → 注册到 SessionContext。
registerJson(table_name, json_data)(lib.rs#L99-L140)
- 输入是 JSON 对象数组字符串(如
[{"a":1,"b":"x"},...]); - 内部先把 JSON 数组转成 NDJSON(每行一个对象),因为 Arrow 的 JSON reader 只接受 NDJSON 格式;
- 用
infer_json_schema推断 schema,再按 8192 的 batch size 读取为 RecordBatch; - 空数据会返回
No data in JSON input错误。
registerParquet(table_name, data)(lib.rs#L146-L177)
- 输入是 Parquet 文件字节(JS 侧传
Uint8Array/ArrayBuffer); - 使用
ParquetRecordBatchReaderBuilder读取,支持 snappy 与 lz4 压缩(无 zstd,原因见依赖一节); - 空文件返回
No data in Parquet file错误。
registerCsv(table_name, data, options_json)(lib.rs#L209-L280)
这是三者中最灵活的,支持通过 JSON 选项字符串定制读取行为,完整选项如下(Rust 侧定义于 lib.rs#L786-L809):
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
header | boolean | true | 首行是否为表头 |
delimiter | string | "," | 字段分隔符,取字符串首字节,必须为 ASCII |
quote | string | '"' | 引号字符,取首字节 |
escape | string | 未设置 | 转义字符 |
terminator | string | "\n"或"\r\n" | 记录终止符 |
batchSize | number | 8192 | RecordBatch 大小 |
inferRows | number | 1000 | schema 推断时读取的行数,显式提供schema时忽略 |
schema | array | 无 | 显式列定义[{name, type, nullable}],提供后跳过推断 |
显式schema支持的类型(大小写不敏感)包括:int8~int64、uint8~uint64、float32/float64、boolean、string/utf8/varchar/text、date/date32/date64、timestamp/timestamp_s/timestamp_ms/timestamp_us/timestamp_ns,以及int/integer/bigint/long/float/double/real/number/bool等别名(对应 Rust 侧的 arrow_schema_from_columns)。单字符选项(delimiter/quote 等)只取字符串第一个字节,非 ASCII 会报single ASCII character错误——测试 rejects non-ASCII delimiter 覆盖了这一行为。
3.3 loadMDL:三种物理表解析模式
loadMDL(mdl_json, source)(lib.rs#L307-L344)是整个语义层的入口。它先解析 MDL JSON 为Manifest,然后根据source参数选择三种模式之一来注册物理表并分析语义层,最后调用apply_wren_on_ctx(Mode::LocalRuntime,直接 DataFusion 执行,不做 SQL 生成)把 MDL 分析规则挂到 SessionContext 上,并把分析结果保存在self.analyzed_mdl。
三种模式(source参数判定逻辑见 is_url_source):
URL 模式(source 以http://或https://开头)(lib.rs#L355-L426)
- 为每个 model 注册一个 DataFusion
ListingTable,URL 固定为{source}/{裸表名}.parquet,无需预先注册表; - 每个唯一 origin 注册一个 HTTP object store(
HttpBuilder),DataFusion 通过 HTTP Range 请求读取远端 Parquet(schema 通过 Range GET 读取 Parquet footer 推断,见 build_listing_table); - 阶段性限制:
s3://和gs://属于 Phase 4 计划,当前会落入本地模式并在缺少表时报错;Phase 2 假设扁平的 Parquet 布局,若两个不同 schema 下的 model 共享同一个裸表名(如raw.orders与staging.orders),会静默冲突到{source}/orders.parquet。更丰富的 schema 映射({source}/{schema}/{name}.parquet)也在 Phase 4 计划中; - 采用"先全部暂存、后统一注册"的策略:所有 model 的 schema 推断都成功后才会修改
self.ctx,失败的loadMDL不会留下半注册的表,便于重试。
本地模式(其他非空字符串)(lib.rs#L431-L475)
- 期望调用方已通过
registerParquet/registerJson/registerCsv预先注册每个 model 的物理表; - 按裸表名在
datafusion.publiccatalog 中查找;若任何 model 的表缺失,loadMDL会立即返回Unresolved models: [...]错误,而不是把问题推迟到查询时——测试 rejects MDL with missing tables in local mode 验证了这一前置校验。
回退模式(source 为空字符串)(lib.rs#L480-L552)
- M3+ 的向后兼容行为:自动检测 MDL 中每个 model 的
tableReference是否为 URL,只要有一个可解析的 URL 就走analyze_with_url_tables(URL 表路径),否则走本地表路径; - 保留它是因为历史 MDL 会把 URL 直接嵌在
tableReference里。
加载完成后,裸 model 名会在 MDL 的 catalog/schema(通常是wren.public)下解析,查询时不需要带 catalog 前缀,直接用"Orders"即可。这一点在 test_bare_model_name_query 与 SDK 测试 loads MDL and queries via model name 中均有覆盖。
3.4 查询:query / cubeQuery / listCubes
query(sql)(lib.rs#L599-L633)是核心执行路径:
- 用
self.runtime.block_on(...)包裹,保证 DataFusion 内部的tokio::task::spawn有活着的调度器; ctx.sql(sql)解析并应用 MDL 分析规则,df.collect()执行并收集 RecordBatch;- 用 Arrow JSON writer(
with_explicit_nulls(true),显式输出 null)把结果序列化为 JSON 对象数组字符串(如[{"count":42,"avg":3.14},...])。
cubeQuery(cube_query_json)(lib.rs#L643-L660)接受 JSON 编码的CubeQuery,经由 wren-core 的cube_query_to_sql转成 SQL,再委托给query()执行——也就是结构化 cube 查询与手写 SQL 走同一条执行链路。它要求先调用loadMDL,否则报No MDL loaded. Call loadMDL() first.。
listCubes()(lib.rs#L667-L705)返回加载的 MDL 中定义的 cube 列表,每条记录包含name、baseObject、measures、dimensions、timeDimensions、hierarchies,同样要求先loadMDL。这对于 Agent 在调用cubeQuery之前探查"有哪些可查询的 cube"非常有用。
四、TypeScript SDK 封装层
raw wasm-bindgen API 暴露的是底层方法,真正的开发体验由 sdk/src/index.ts 提供——它把整个 wasm 接口封装成一个类WrenEngine,并处理了 WASM 二进制加载、BufferSource 归一化、JSON 序列化/反序列化等细节。
初始化与生命周期:
WrenEngine.init(options?)(index.ts#L172-L180):加载 WASM 二进制并创建引擎。options.wasmUrl可以是 URL 字符串、URL 对象(浏览器内 fetch),或BufferSource(如 ArrayBuffer、Node.js Buffer,直接实例化);缺省时通过import.meta.url解析同目录的wren_core_wasm_bg.wasm;engine.free()(index.ts#L302-L304):释放 WASM 内存。
数据注册(index.ts#L206-L262):
registerParquet(name, data):接受任意BufferSource,对 TypedArray 会保留byteOffset/byteLength视图信息;registerJson(name, data):接受 JS 对象数组,内部JSON.stringify后传给底层;registerCsv(name, data, options?):data可以是 CSV 字符串(按 UTF-8 编码)或任意BufferSource;options即上文表格中的CsvReadOptions。
语义层与查询:
loadMDL(mdl, profile)(index.ts#L194-L197):mdl是 MDL manifest 对象,profile为{ source: string }。注释明确区分三种 source 语义:URL 模式、本地模式(source: "./data/")与回退模式(source: ""),并提醒 URL 模式的裸表名冲突风险;query(sql)(index.ts#L268-L272):返回解析后的Record<string, unknown>[]数组,而不是原始 JSON 字符串;cubeQuery(query)(index.ts#L283-L287):入参CubeQueryInput包含cube、measures、dimensions、timeDimensions、filters、limit、offset等字段,SDK 注释建议聚合查询优先用它,因为 cube 层会自动拼装GROUP BY、DATE_TRUNC和WHERE子句;listCubes()(index.ts#L295-L299):返回强类型的CubeInfo[]。
SDK 还导出了一系列 TypeScript 类型定义:WrenProfile、Granularity(year 到 minute)、FilterOperator(eq/neq/in/not_in/gt/gte/lt/lte/contains/starts_with/is_null/is_not_null)、FilterValue、TimeDimensionInput、CubeFilterInput、CubeQueryInput、CubeInfo、CsvReadOptions等,与 wren_core_wasm.d.ts(手工维护的 wasm-bindgen 输出类型存根)共同构成完整的类型层。
五、依赖矩阵与版本选择
Cargo.toml 的依赖设计直接服务于 WASM 目标,每一行都有明确理由:
| 依赖 | 版本/特性 | 设计意图 |
|---|---|---|
| DataFusion | v53(upstream),default-features = false+ 选定特性 | 查询引擎;不启用 parquet 特性(DataFusion 的 parquet 默认开启 zstd,无法编译到 WASM),改为自己读 Parquet 字节 → RecordBatch → MemTable |
| Arrow | v58.1,json+csv特性 | JSON/CSV 读取器与 JSON 结果写出 |
| Parquet | v58.1,仅snap+lz4 | 无 zstd(依赖 C 库zstd-sys,无法编译到 WASM),snappy/lz4 为纯 Rust 实现 |
| object_store | v0.13.1,aws+http特性 | URL 模式下 HttpStore 读取远端 Parquet |
| wren-core | path../wren-core/core,default-features = false | 语义层(MDL 分析规则),WASM 下不启用多线程 |
| wren-core-base | path../wren-core-base | 共享 manifest 类型,无 DataFusion 依赖 |
| wasm-bindgen / js-sys / web-sys | — | WASM ↔ JS 绑定与 console 输出 |
| tokio | rt+macros,无rt-multi-thread | 单线程运行时(见 3.1 节) |
| chrono | wasmbind特性 | WASM 下用js_sys::Date替代SystemTime |
| getrandom | 0.2/0.3/0.4 三个主版本并存 | 依赖树中三版都需要显式启用js/wasm_js特性([target.'cfg(target_arch = "wasm32")'.dependencies]节) |
数据融合方向值得注意:DataFusion 的表达式特性(nested_expressions、crypto_expressions、datetime_expressions、encoding_expressions、regex_expressions、unicode_expressions)也被显式选中,意味着浏览器端同样能使用这些函数族。
六、WASM 特定约束与性能目标
这是该模块区别于 native 端最重要的部分,CLAUDE.md 用专门一节列出:
- 单线程:
SessionConfig::with_target_partitions(1);tokio 只用rt(无rt-multi-thread)。所有物理算子都在单分区、单线程内执行; - 无 zstd:
zstd-sys(C 库)编译不到 wasm32 目标,因此 Parquet 只支持 snappy + lz4(纯 Rust 压缩实现); - 无 SystemTime:chrono 必须启用
wasmbind特性,时间相关操作走js_sys::Date; - getrandom:依赖树中 0.2、0.3、0.4 三个主版本同时存在,每个都需要在 wasm32 目标下显式指定 JS 后端(
js/wasm_js特性); - macOS 构建:C 依赖需要 LLVM(
brew install llvm),justfile 会自动设置CC_wasm32_unknown_unknown、AR_wasm32_unknown_unknown等环境变量指向 Homebrew 的 LLVM; - 二进制体积:约 68 MB raw / 约 14 MB gzip,gzip 目标是保持在 15 MB 以下。
just size命令可以直接报告当前构建产物的 raw 与 gzip 体积。
七、开发命令与构建流程
CLAUDE.md 列出了完整的 just 命令集(justfile 有对应实现):
just build # 完整构建:WASM(release)+ TypeScript SDK → dist/ just build-wasm # 仅 WASM(wasm-pack → pkg/),macOS 自动检测 LLVM just build-wasm-dev # WASM debug 构建(更快,不加 --release) just build-dist # 从 pkg/ + TS 组装 dist/(要求 pkg/ 已存在) just test # SDK 集成测试(要求 dist/) just typecheck # 仅 TypeScript 类型检查 just serve # 本地 HTTP 服务(localhost:8787),用于浏览器示例 just size # 报告 WASM 二进制体积(raw + gzip) just clean # 清理 pkg/、dist/、target/各命令的职责链:build由build-wasm(wasm-pack build --target web --release)与build-dist(npm install+node scripts/build.mjs)串联而成。其中 scripts/build.mjs 负责把pkg/下的 wasm 产物(wren_core_wasm.js、wren_core_wasm.d.ts、wren_core_wasm_bg.wasm、wren_core_wasm_bg.wasm.d.ts)拷贝到dist/,再调用tsc -p sdk/tsconfig.json编译 TypeScript。just size内部用stat+gzip -c统计原始与压缩后体积。clean通过rm -rf pkg/ dist/ target/清理三类构建产物。
八、npm 包与浏览器示例
package.json 定义了@wrenai/wren-core-wasm这个 ESM 包:
- 入口
dist/index.js,类型dist/index.d.ts,exports同时暴露.与./dist/*; files只包含dist/,即发布物 = ESM + 类型 +.wasm二进制;- 构建脚本与 justfile 对应:
build:wasm(wasm-pack release)、build:ts、build:dist、build、typecheck、test(node --test sdk/tests/index.test.mjs); engines.node >= 16,sideEffects: false,TypeScript 为唯一 devDependency;- 发布后可经由 CDN(unpkg/jsDelivr)直接使用。
仓库提供了 6 个浏览器示例页(examples/),其中两个最能体现核心能力:
inline.html(examples/inline.html)演示完整的最小链路:init()加载 WASM →registerJson('orders', ...)注册内嵌 JSON →loadMDL(mdl, '')加载语义层 → 执行 SQLSELECT customer, sum(amount) AS total, count(*) AS orders FROM "Orders" GROUP BY customer ORDER BY total DESC并把结果渲染成表格。
url-mode.html(examples/url-mode.html)演示 URL 模式:把source指向{origin}/examples/data/,loadMDL自动注册 ListingTable,DataFusion 通过 HTTP Range 请求远端 Parquet,全程不需要registerParquet。
运行这些示例用 examples/serve.mjs:node examples/serve.mjs [port](默认 8787)。这个静态服务器特意实现了CORS + Range 请求 + MIME 类型:URL 模式下 DataFusion 读取远端 Parquet 依赖 HTTP Range 请求(读取 footer 推断 schema),因此服务器必须响应206 Partial Content并暴露Accept-Ranges: bytes、Content-Range等响应头,同时用路径规范化拦截目录穿越。
九、测试与代码约定
测试体系分两层:
- Rust WASM 测试:使用
wasm-bindgen-test(lib.rs 测试模块),配置为run_in_node_experimental,可在 Node 中跑无需浏览器。代表性用例:test_basic_query:注册 JSON 后执行count(*)/avg(amount)聚合;test_union_all_does_not_trap:回归测试——0.4.0 在UNION ALL这类多分区计划上会因tokio::spawn无 reactor 而RuntimeError: unreachable;test_is_url_source与test_extract_bare_table_name:覆盖 URL 判定(s3://、gs://当前不算 URL 模式)与tableReference解析(引号内的点不会被错误切开,如"schema"."has.dot"→has.dot);test_bare_model_name_query:验证裸 model 名无需wren.public.前缀即可查询。
- SDK 集成测试(sdk/tests/index.test.mjs):使用 Node 内置
node:test,加载真实dist/wren_core_wasm_bg.wasm验证全链路。覆盖了引擎多实例隔离、JSON/CSV/Parquet 注册、空结果集、类型保持、聚合、MDL 加载(本地/回退模式)、缺失表报错、非法 SQL/表报错、集合算子(UNION/INTERSECT/EXCEPT)、free()、cubeQuery/listCubes及时间维度分桶(dateRange+granularity: month产生created_at__month列)等场景。
代码约定(来自 CLAUDE.md 的 Conventions 节):
- Rust 使用
cargo fmt格式化、clippy -D warnings零警告 lint; - TypeScript 使用 strict 模式、ES2020 target;
- 面向 JS 的 API 用
#[wasm_bindgen(js_name = camelCase)]命名; - 错误以
JsError传播(浏览器 console 中可见含堆栈的错误信息); - 测试:Rust 侧
wasm-bindgen-test,SDK 侧node:test。
十、小结:什么时候适合使用 wren-core-wasm
结合以上源码证据,wren-core-wasm 的适用形态可以归纳为:数据以 Parquet/CSV/JSON 文件形式存在、且可以通过 HTTP Range 或本地上传提供给浏览器的纯前端分析场景。它的语义层能力(MDL 建模、cube 查询、model 名解析)全部在客户端完成,服务端只承担静态文件服务。URL 模式直接读取远端 Parquet 省去了数据落地环节,本地模式则适合文件由用户本地选择的场景。
需要留意的边界(均可在 src/lib.rs 中确认):单线程执行意味着计算密集型聚合的性能上限受限于单个浏览器线程;s3:///gs://支持与 schema 级目录映射属于 Phase 4 计划;URL 模式下同名裸表存在文件级静默冲突风险。若你的数据源是 BigQuery、Snowflake、PostgreSQL 这类需要通过连接器访问的数据库,则应回到 WrenAI 的服务端语义层方案,而不是浏览器端 WASM 引擎。
进一步阅读:WrenAI 的 MDL 概念可参考 docs/core/concepts/what_is_mdl.md,WASM 语义层的 manifest 类型定义位于 wren-core-base,MDL 分析规则的语义层实现位于 wren-core/core/src/mdl。
【免费下载链接】WrenAIGenBI (Generative BI) for AI agents, an open-source, governed text-to-SQL through an open context layer that turns natural-language questions into trusted dashboards, charts, and SQL across 20+ data sources, such as BigQuery, Snowflake, PostgreSQL, ClickHouse, Amazon Redshift, Databricks and more.项目地址: https://gitcode.com/GitHub_Trending/wr/WrenAI
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考