WrenAI wren-core-wasm 演进与实战:在浏览器里跑 DataFusion 语义 SQL 引擎
2026/9/13 11:42:42 网站建设 项目流程

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.02026-05-05新增 wren-core-wasm 模块(浏览器 WASM 支持),并将 wren-engine 导入 core/ 目录
0.3.02026-05-05正式发布带浏览器 WASM 支持的 wren-core-wasm 模块
0.4.02026-05-15完整 Cube 支持:校验、翻译、PyO3、CLI、WASM、文档全线打通
0.4.12026-05-15Bug 修复:让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 信息,包括namebaseObjectmeasuresdimensionstimeDimensionshierarchies,便于 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 BYDATE_TRUNCWHERE子句),再走与query()相同的执行路径。时间分桶的结果列以<dim>__<granularity>的形式暴露,例如created_at__monthdateRange遵循「起始包含、结束排除」的语义。

cubeQuery 与 query 如何取舍

场景推荐
在维度上聚合度量(可带时间分桶)cubeQuery
自由 SQL:跨模型 join、窗口函数、自定义 CTEquery
MDL 未定义 Cubequery

Filter 支持 12 种操作符:eqneqinnot_ingtgteltltecontainsstarts_withis_nullis_not_null。其中in/not_invalue传数组,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::spawnspawn在没有 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 ALLUNIONINTERSECTEXCEPT全部可以正常返回结果。

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>
选项类型说明
wasmUrlstring \| URL \| BufferSourceWASM 二进制来源。默认通过import.meta.url定位同目录的wren_core_wasm_bg.wasm

engine.loadMDL(mdl, profile)

async loadMDL(mdl: object, profile: WrenProfile): Promise<void>
参数类型说明
mdlobjectMDL 清单(会被 JSON 序列化)
profile.sourcestring"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)类型默认值说明
headerbooleantrue首行是否为表头
delimiterstring","字段分隔符(单个 ASCII 字符)
quotestring"\""引号字符(单个 ASCII 字符)
escapestring未设置转义字符(单个 ASCII 字符)
terminatorstring\n\r\n记录终止符(单个 ASCII 字符)
batchSizenumber8192RecordBatch 大小
inferRowsnumber1000用于推断 schema 的行数;设置schema时忽略
schemaCsvSchemaColumn[]推断显式 Arrow schema{ name, type, nullable? }[]

schema 列类型(大小写不敏感):int8/int16/int32/int64uint8/uint16/uint32/uint64float32/float64booleanstring(别名utf8/varchar/text)、date/date32/date64timestamptimestamp_{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(): void

query()执行经过语义层的 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.htmlregisterJson+ 原始 SQLquery()
url-mode.html通过 HTTP range 请求读取远端 Parquet
test-cdn.html从 unpkg 加载已发布包
cube-quickstart.html最小cubeQuery()—— 三个预设查询(分组、过滤、时间分桶)
cube-explorer.html表单驱动的CubeQuery构建器:选度量/维度、加过滤、选粒度与日期范围
csv-quickstart.htmlregisterCsv()读取data/真实文件:schema 推断、自定义分隔符(TSV)、带显式 schema 的无表头 CSV

修改 Rust 代码后,重新运行just build-wasm-dev并刷新页面即可生效,因为示例直接从pkg/wren_core_wasm.js导入。

实战建议与常见坑位

综合 AGENT_GUIDE.md 的 Common Pitfalls 与源码实现,以下几点最值得注意:

  1. 模型名区分大小写—— 查询时使用双引号:FROM "Orders",而非FROM Orders
  2. 调用顺序—— Inline 模式下必须先registerJson/registerParquet/registerCsv,再loadMDL;本地模式下缺少物理表会在加载时立即报Unresolved models,而不是等到查询才崩溃。
  3. WASM 体积较大(约 68 MB 原始 / 约 14 MB gzip)—— 在WrenEngine.init()期间应显示加载指示器。
  4. source: ''的语义是「仅使用预注册表」—— 期望 URL 模式时不要传空字符串。
  5. URL 模式需要 HTTP(S) + CORS—— 用file://打开页面无法作为数据源,页面与 Parquet 都应通过 HTTP(S) 提供服务并配置 CORS。
  6. Range 支持是 URL 模式的前提——python -m http.server不支持,可改用支持 Range 的服务器或回退到 Inline 模式。
  7. Node 环境必须传wasmUrl: BufferSource—— 否则init()会因file://fetch 不被支持而立即失败。
  8. 注册操作必须串行—— WASM 引擎是单线程的,并发注册registerParquet/registerJson不安全。
  9. 查询结果可直接渲染——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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询