WrenAI wren-core-wasm 演进与实战:在浏览器里跑 DataFusion 语义 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 将核心 SQL 引擎编译为 WebAssembly 的产物:它以 Apache DataFusion 为执行内核,把语义层(MDL)的查询改写能力直接搬到浏览器与 Node.js 环境,无需任何服务端即可对 Parquet、CSV、JSON 数据执行 SQL。本文以 CHANGELOG.md 的版本演进为时间线,结合 README.md、AGENT_GUIDE.md 与 src/lib.rs 的源码实现,完整讲解它的安装方式、两种数据加载模式、Cube 查询 API、底层 tokio 运行时修复原理与从源码构建的完整流程。读完本文,你可以独立在浏览器或 Node 环境中搭建一个免服务器的语义层查询应用,并理解其关键实现细节与踩坑点。
版本演进脉络:从 WASM 模块到完整 Cube 支持
core/wren-core-wasm的 CHANGELOG.md 记录了四个阶段的演进:
| 版本 | 时间 | 核心变更 |
|---|---|---|
| 0.2.0 | 2026-05-05 | 新增 wren-core-wasm 模块(浏览器 WASM 支持),并将 wren-engine 导入 core/ 目录 |
| 0.3.0 | 2026-05-05 | 正式发布带浏览器 WASM 支持的 wren-core-wasm 模块 |
| 0.4.0 | 2026-05-15 | 完整 Cube 支持:校验、翻译、PyO3、CLI、WASM、文档全线打通 |
| 0.4.1 | 2026-05-15 | Bug 修复:让query()通过 tokio runtime 驱动,解决UNION ALL崩溃(trap)问题 |
从 src/lib.rs 中定义的里程碑(M1 到 M4)可以看出这套架构的成型路线:M1 完成 DataFusion 的 WASM 编译与内存查询,M2 支持浏览器内 Parquet 上传查询,M3 引入 wren-core 语义层(MDL 计划改写),M4 发布 npm 包与 TypeScript API 封装。当前仓库状态对应 M4 完成后的成熟形态。
安装与引入:npm 包与 CDN 两种方式
通过 package.json 可以确认包名与入口,安装命令如下:
npm install @wrenai/wren-core-wasm或者在浏览器中通过 CDN 直接以 ES Module 方式引入:
<script type="module"> import { WrenEngine } from 'https://unpkg.com/@wrenai/wren-core-wasm@0.3.0/dist/index.js'; </script>注意:请使用 unpkg,不要用 jsDelivr。jsDelivr 免费 CDN 对单个文件有 50 MB 限制,而该 WASM 二进制原始体积约 68 MB,会导致
.wasm请求被拒绝。这一点在 README.md 与 AGENT_GUIDE.md 中均有明确提示。
快速上手:Inline 模式(本地开发与内嵌仪表盘的推荐路径)
Inline 模式直接将数据注册到引擎内存中,不依赖任何服务器,也避开了 HTTP Range 请求与 CORS 的坑。对于数据总量约 50 MB 以下的场景,这是阻力最小的路径。
import { WrenEngine } from '@wrenai/wren-core-wasm'; const engine = await WrenEngine.init(); // 将 JSON 数据注册为表 await engine.registerJson('orders', [ { id: 1, customer: 'Alice', amount: 100 }, { id: 2, customer: 'Alice', amount: 250 }, { id: 3, customer: 'Bob', amount: 120 }, ]); // 或者从 ArrayBuffer 注册 Parquet const response = await fetch('orders.parquet'); await engine.registerParquet('orders', await response.arrayBuffer()); // 或者注册 CSV —— 字符串或字节皆可,可指定 schema / delimiter / quote await engine.registerCsv('orders', 'id,customer,amount\n1,Alice,100\n2,Bob,200'); const mdl = { catalog: 'wren', schema: 'public', models: [ { name: 'Orders', tableReference: { table: 'orders' }, columns: [ { name: 'id', type: 'INTEGER' }, { name: 'customer', type: 'VARCHAR' }, { name: 'amount', type: 'DOUBLE' }, ], primaryKey: 'id', }, ], relationships: [], views: [], }; // 加载 MDL,source 传空字符串表示使用预注册表 await engine.loadMDL(mdl, { source: '' }); const rows = await engine.query('SELECT * FROM "Orders" LIMIT 10');一个常见的仪表盘开发模式是:先用fetch()并行拉取每个 Parquet 文件,再按顺序调用registerParquet注册。因为 WASM 引擎是单线程的,并发注册并不安全(详见 AGENT_GUIDE.md 的 Common Pitfalls)。
URL 模式:通过 HTTP Range 请求直读远端 Parquet
当数据量较大或已经托管在 CDN 上时,可以使用 URL 模式。此时数据仍存放在 HTTP 服务器上,DataFusion 通过HTTP range 请求逐个读取 Parquet 文件(先读 footer,再按行组读取)。服务器必须支持Range:请求头,否则查询会在读取 footer 之后静默挂起。
await engine.loadMDL(mdl, { source: 'https://your-cdn.com/data/' }); const rows = await engine.query('SELECT customer, sum(amount) AS total FROM "Orders" GROUP BY customer'); console.table(rows); // [{ customer: 'Alice', total: 350 }, { customer: 'Bob', total: 120 }]底层实现:URL 模式如何工作
从源码看,loadMDL会根据source参数分发到三种模式(src/lib.rs):
- URL 模式(
http://…或https://…开头):对每个模型注册一个 DataFusionListingTable,物理文件路径固定为{source}/{裸表名}.parquet; - fallback 模式(
source=""):从每个模型的tableReference自动探测 URL 还是本地表,用于兼容旧版 MDL; - 本地模式(其他任意非空字符串):要求调用方已通过
registerParquet/registerJson预注册物理表,若缺少任何模型的物理表,loadMDL会立即返回Unresolved models: [...]错误,而不是把问题推迟到查询阶段。
URL 模式下每个唯一 origin 只注册一次 HTTP object store(src/lib.rs),并且所有模型的 schema 推断会先暂存、全部成功后才写入self.ctx,避免失败的loadMDL留下半注册状态。tableReference使用裸表名(如"orders"),引擎会自动在 URL 模式下拼接{source}/{name}.parquet。
需要注意的是:当前阶段s3://和gs://尚未纳入 URL 模式(标记为 Phase 4 工作),会被回退到本地模式并快速失败。
如何选择本地开发服务器
URL 模式依赖 DataFusion 的ListingTable通过 HTTP range 请求读取 Parquet,因此本地开发服务器必须支持Range:请求头:
| 服务器 | Range 支持 | 说明 |
|---|---|---|
python -m http.server | ❌ 不支持 | Python 内置,URL 模式下应避免 |
python -m RangeHTTPServer | ✅ 支持 | pip install rangehttpserver |
npx serve | ⚠️ 仅单范围 | 基于sirv;请求超出 EOF 时可能返回416 |
npx http-server | ✅ 支持 | 默认带 CORS |
caddy file-server | ✅ 支持 | 生产可用 |
| Vite | ⚠️ 仅单范围 | 同样基于sirv,与npx serve有相同的416边界问题 |
webpack-dev-server | ⚠️ 仅单范围 | multipart range 请求会回退为返回整个资源 |
快速检查命令:
curl -I -H "Range: bytes=0-1023" http://localhost:PORT/file.parquet应返回HTTP/1.1 206 Partial Content而非200。如果只能使用不支持 Range 的服务器,请改用 Inline 模式:用fetch()一次性拉取每个文件,再通过registerParquet注册。
Node.js 中使用:必须显式传入 WASM 二进制
WrenEngine.init()默认通过import.meta.url定位同目录的wren_core_wasm_bg.wasm,在 Node 中该 URL 会解析为file://。Node 的undicifetch 不支持file://协议,因此init()会直接抛出异常。正确做法是把 WASM 二进制作为BufferSource直接传入:
import { readFileSync } from 'node:fs'; import { WrenEngine } from '@wrenai/wren-core-wasm'; const buf = readFileSync( 'node_modules/@wrenai/wren-core-wasm/dist/wren_core_wasm_bg.wasm' ); const engine = await WrenEngine.init({ wasmUrl: buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength), });这一模式同样适用于单元测试与 CI 冒烟检查(node --test)。仓库内的集成测试 sdk/tests/index.test.mjs 正是采用这种方式:用readFileSync读取dist/wren_core_wasm_bg.wasm后传入WrenEngine.init({ wasmUrl: wasmBytes })。
0.4.0 核心特性:完整 Cube 支持
0.4.0 版本为 WASM 模块补全了 Cube 语义,覆盖校验、翻译、PyO3、CLI、WASM 与文档。在 JS 侧体现为两个新 API:cubeQuery()与listCubes()(类型定义见 sdk/src/index.ts)。
listCubes:先探索再查询
listCubes()返回 MDL 中定义的全部 Cube 信息,包括name、baseObject、measures、dimensions、timeDimensions与hierarchies,便于 Agent 在调用cubeQuery前先发现可查询的度量与维度:
const cubes = engine.listCubes(); // → [{ name: "order_metrics", baseObject: "orders", measures: [...], // dimensions: [...], timeDimensions: [...], hierarchies: {...} }]cubeQuery:结构化聚合查询
const rows = await engine.cubeQuery({ cube: "order_metrics", measures: ["revenue", "order_count"], dimensions: ["status"], timeDimensions: [{ dimension: "created_at", granularity: "month", dateRange: ["2024-01-01", "2025-01-01"], }], filters: [ { dimension: "status", operator: "eq", value: "completed" }, ], limit: 100, });其底层实现(src/lib.rs)将结构化CubeQuery通过 wren-core 翻译为 SQL(自动生成GROUP BY、DATE_TRUNC、WHERE子句),再走与query()相同的执行路径。时间分桶的结果列以<dim>__<granularity>的形式暴露,例如created_at__month;dateRange遵循「起始包含、结束排除」的语义。
cubeQuery 与 query 如何取舍
| 场景 | 推荐 |
|---|---|
| 在维度上聚合度量(可带时间分桶) | cubeQuery |
| 自由 SQL:跨模型 join、窗口函数、自定义 CTE | query |
| MDL 未定义 Cube | query |
Filter 支持 12 种操作符:eq、neq、in、not_in、gt、gte、lt、lte、contains、starts_with、is_null、is_not_null。其中in/not_in的value传数组,is_null/is_not_null省略value。时间粒度支持year/quarter/month/week/day/hour/minute七档。注意:listCubes()与cubeQuery()都要求先完成loadMDL(),否则会抛出错误,这一点在测试用例(如 sdk/tests/index.test.mjs 中的cubeQuery without loadMDL fails clearly)中有明确验证。
0.4.1 关键修复:query() 经由 tokio runtime 驱动
0.4.1 的修复条目看似只有一行,却解决了一个非常隐蔽的 WASM 运行时崩溃问题。源码注释(src/lib.rs)揭示了完整原因:
DataFusion 的物理算子(例如
CoalescePartitionsExec,它会包裹任何多分区计划,如UNION ALL/INTERSECT/EXCEPT)内部会调用tokio::task::spawn。spawn在没有 tokio runtime 上下文时会以there is no reactor runningpanic —— 而仅靠wasm-bindgen-futures并不会提供这个上下文。
因此WrenEngine结构体持有一个current_thread 模式的 tokio runtime(src/lib.rs),query()通过runtime.block_on(...)驱动整个查询未来,让 DataFusion 能看到一个活的调度器。没有这层包装,UNION ALL这类多分区计划会在 JS 侧表现为晦涩的RuntimeError: unreachable。
回归测试test_union_all_does_not_trap(src/lib.rs)与集成测试中的 set operators 一组用例(sdk/tests/index.test.mjs)共同验证了UNION ALL、UNION、INTERSECT、EXCEPT全部可以正常返回结果。
API 参考:WrenEngine 完整方法表
WrenEngine的 TypeScript 封装位于 sdk/src/index.ts,query()返回Record<string, unknown>[],可直接供 Chart.js、D3、Recharts 等图表库消费。
WrenEngine.init(options?)
static async init(options?: WrenEngineOptions): Promise<WrenEngine>| 选项 | 类型 | 说明 |
|---|---|---|
wasmUrl | string \| URL \| BufferSource | WASM 二进制来源。默认通过import.meta.url定位同目录的wren_core_wasm_bg.wasm |
engine.loadMDL(mdl, profile)
async loadMDL(mdl: object, profile: WrenProfile): Promise<void>| 参数 | 类型 | 说明 |
|---|---|---|
mdl | object | MDL 清单(会被 JSON 序列化) |
profile.source | string | "https://..."走 URL 模式;""使用预注册表;其他非空字符串走本地模式 |
engine.registerParquet(name, data)
async registerParquet(name: string, data: ArrayBuffer): Promise<void>Inline 模式下须在loadMDL之前调用。接受任何BufferSource(ArrayBuffer、TypedArray 如Uint8Array、Node Buffer),TypedArray 的byteOffset/byteLength视图元数据会被保留。
engine.registerJson(name, data)
async registerJson(name: string, data: object[]): Promise<void>底层将 JSON 数组转换为 NDJSON(每行一个对象,Arrow JSON reader 的格式要求)再解析成 Arrow RecordBatch 注册为MemTable。
engine.registerCsv(name, data, options?)
async registerCsv( name: string, data: string | BufferSource, options?: CsvReadOptions, ): Promise<void>默认第一行为表头,schema 从前 1000 行推断。完整选项如下:
| 选项(camelCase) | 类型 | 默认值 | 说明 |
|---|---|---|---|
header | boolean | true | 首行是否为表头 |
delimiter | string | "," | 字段分隔符(单个 ASCII 字符) |
quote | string | "\"" | 引号字符(单个 ASCII 字符) |
escape | string | 未设置 | 转义字符(单个 ASCII 字符) |
terminator | string | \n或\r\n | 记录终止符(单个 ASCII 字符) |
batchSize | number | 8192 | RecordBatch 大小 |
inferRows | number | 1000 | 用于推断 schema 的行数;设置schema时忽略 |
schema | CsvSchemaColumn[] | 推断 | 显式 Arrow schema{ name, type, nullable? }[] |
schema 列类型(大小写不敏感):int8/int16/int32/int64、uint8/uint16/uint32/uint64、float32/float64、boolean、string(别名utf8/varchar/text)、date/date32/date64、timestamp及timestamp_{s,ms,us,ns}。源码中还接受int/integer/bigint/long/float/double/real/number/bool等别名(src/lib.rs)。
engine.query(sql)与engine.free()
async query(sql: string): Promise<Record<string, unknown>[]> free(): voidquery()执行经过语义层的 SQL 并返回解析后的对象数组;free()在引擎不再需要时释放 WASM 内存。
从源码构建:wasm-pack 全流程
构建前提:Rust 工具链、wasm-pack、Node.js 16+(engines字段已在 package.json 中声明)。
cd core/wren-core-wasm # 安装 TypeScript 开发依赖 npm install # 构建 WASM 二进制(需要 wasm32-unknown-unknown target) wasm-pack build --target web --release # 构建 TypeScript 封装并组装 dist/ npm run build:dist # 运行集成测试 npm test # 仅做类型检查 npm run typecheck仓库还提供了 justfile 封装常用任务:just build-wasm-dev(debug 构建,适合示例调试)、just serve(在 localhost:8787 启动带 CORS 与 Range 支持的静态开发服务器)、just size(报告 WASM 二进制体积)等。
macOS 注意事项
在 macOS 上构建 WASM 可能需要 LLVM 来编译 C 依赖:
brew install llvm CC_wasm32_unknown_unknown=/opt/homebrew/opt/llvm/bin/clang \ AR_wasm32_unknown_unknown=/opt/homebrew/opt/llvm/bin/llvm-ar \ CFLAGS_wasm32_unknown_unknown="--target=wasm32-unknown-unknown" \ wasm-pack build --target web --release可运行的示例页面
examples/目录随仓库附带了多个可直接运行的浏览器 demo,它们直接引用pkg/下的本地构建产物,因此始终反映当前源码状态(README.md):
# 构建 WASM 二进制(debug 构建即可运行示例) just build-wasm-dev # 启动支持 CORS + Range 的静态开发服务器 just serve| Demo | 展示内容 |
|---|---|
| inline.html | registerJson+ 原始 SQLquery() |
| url-mode.html | 通过 HTTP range 请求读取远端 Parquet |
| test-cdn.html | 从 unpkg 加载已发布包 |
| cube-quickstart.html | 最小cubeQuery()—— 三个预设查询(分组、过滤、时间分桶) |
| cube-explorer.html | 表单驱动的CubeQuery构建器:选度量/维度、加过滤、选粒度与日期范围 |
| csv-quickstart.html | registerCsv()读取data/真实文件:schema 推断、自定义分隔符(TSV)、带显式 schema 的无表头 CSV |
修改 Rust 代码后,重新运行just build-wasm-dev并刷新页面即可生效,因为示例直接从pkg/wren_core_wasm.js导入。
实战建议与常见坑位
综合 AGENT_GUIDE.md 的 Common Pitfalls 与源码实现,以下几点最值得注意:
- 模型名区分大小写—— 查询时使用双引号:
FROM "Orders",而非FROM Orders。 - 调用顺序—— Inline 模式下必须先
registerJson/registerParquet/registerCsv,再loadMDL;本地模式下缺少物理表会在加载时立即报Unresolved models,而不是等到查询才崩溃。 - WASM 体积较大(约 68 MB 原始 / 约 14 MB gzip)—— 在
WrenEngine.init()期间应显示加载指示器。 source: ''的语义是「仅使用预注册表」—— 期望 URL 模式时不要传空字符串。- URL 模式需要 HTTP(S) + CORS—— 用
file://打开页面无法作为数据源,页面与 Parquet 都应通过 HTTP(S) 提供服务并配置 CORS。 - Range 支持是 URL 模式的前提——
python -m http.server不支持,可改用支持 Range 的服务器或回退到 Inline 模式。 - Node 环境必须传
wasmUrl: BufferSource—— 否则init()会因file://fetch 不被支持而立即失败。 - 注册操作必须串行—— WASM 引擎是单线程的,并发注册
registerParquet/registerJson不安全。 - 查询结果可直接渲染——
query()返回Record<string, unknown>[],配合 Chart.js/D3/Recharts 或console.table即可快速可视化。
结合 CHANGELOG.md 的版本轨迹可以看到,wren-core-wasm 的核心价值在于把「MDL 语义层 + DataFusion 执行引擎」完整编译到浏览器端:既能在无服务器场景下对中小数据量做即席分析,又能通过 URL 模式直连 CDN 上的大规模 Parquet 数据集,同时以 Cube API 为上层 Agent 提供了结构化的聚合查询入口。
【免费下载链接】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),仅供参考