WrenAI wren-core-wasm 演进与实战:在浏览器里跑 DataFusion 语义 SQL 引擎 📅 发布时间:2026/9/13 11:42:52 👁 浏览次数: 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/WrenAIwren-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 typemodule import { WrenEngine } from https://unpkg.com/wrenai/wren-core-wasm0.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: [], }; // 加载 MDLsource 传空字符串表示使用预注册表 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.rsURL 模式http://…或https://…开头对每个模型注册一个 DataFusionListingTable物理文件路径固定为{source}/{裸表名}.parquetfallback 模式source从每个模型的tableReference自动探测 URL 还是本地表用于兼容旧版 MDL本地模式其他任意非空字符串要求调用方已通过registerParquet/registerJson预注册物理表若缺少任何模型的物理表loadMDL会立即返回Unresolved models: [...]错误而不是把问题推迟到查询阶段。URL 模式下每个唯一 origin 只注册一次 HTTP object storesrc/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 rangehttpservernpx serve⚠️ 仅单范围基于sirv请求超出 EOF 时可能返回416npx http-server✅ 支持默认带 CORScaddy file-server✅ 支持生产可用Vite⚠️ 仅单范围同样基于sirv与npx serve有相同的416边界问题webpack-dev-server⚠️ 仅单范围multipart range 请求会回退为返回整个资源快速检查命令curl -I -H Range: bytes0-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 侧体现为两个新 APIcubeQuery()与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__monthdateRange遵循「起始包含、结束排除」的语义。cubeQuery 与 query 如何取舍场景推荐在维度上聚合度量可带时间分桶cubeQuery自由 SQL跨模型 join、窗口函数、自定义 CTEqueryMDL 未定义 CubequeryFilter 支持 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 runtimesrc/lib.rsquery()通过runtime.block_on(...)驱动整个查询未来让 DataFusion 能看到一个活的调度器。没有这层包装UNION ALL这类多分区计划会在 JS 侧表现为晦涩的RuntimeError: unreachable。回归测试test_union_all_does_not_trapsrc/lib.rs与集成测试中的 set operators 一组用例sdk/tests/index.test.mjs共同验证了UNION ALL、UNION、INTERSECT、EXCEPT全部可以正常返回结果。API 参考WrenEngine 完整方法表WrenEngine的 TypeScript 封装位于 sdk/src/index.tsquery()返回Recordstring, unknown[]可直接供 Chart.js、D3、Recharts 等图表库消费。WrenEngine.init(options?)static async init(options?: WrenEngineOptions): PromiseWrenEngine选项类型说明wasmUrlstring \| URL \| BufferSourceWASM 二进制来源。默认通过import.meta.url定位同目录的wren_core_wasm_bg.wasmengine.loadMDL(mdl, profile)async loadMDL(mdl: object, profile: WrenProfile): Promisevoid参数类型说明mdlobjectMDL 清单会被 JSON 序列化profile.sourcestringhttps://...走 URL 模式使用预注册表其他非空字符串走本地模式engine.registerParquet(name, data)async registerParquet(name: string, data: ArrayBuffer): PromisevoidInline 模式下须在loadMDL之前调用。接受任何BufferSourceArrayBuffer、TypedArray 如Uint8Array、Node BufferTypedArray 的byteOffset/byteLength视图元数据会被保留。engine.registerJson(name, data)async registerJson(name: string, data: object[]): Promisevoid底层将 JSON 数组转换为 NDJSON每行一个对象Arrow JSON reader 的格式要求再解析成 Arrow RecordBatch 注册为MemTable。engine.registerCsv(name, data, options?)async registerCsv( name: string, data: string | BufferSource, options?: CsvReadOptions, ): Promisevoid默认第一行为表头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/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): PromiseRecordstring, unknown[] free(): voidquery()执行经过语义层的 SQL 并返回解析后的对象数组free()在引擎不再需要时释放 WASM 内存。从源码构建wasm-pack 全流程构建前提Rust 工具链、wasm-pack、Node.js 16engines字段已在 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-devdebug 构建适合示例调试、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--targetwasm32-unknown-unknown \ wasm-pack build --target web --release可运行的示例页面examples/目录随仓库附带了多个可直接运行的浏览器 demo它们直接引用pkg/下的本地构建产物因此始终反映当前源码状态README.md# 构建 WASM 二进制debug 构建即可运行示例 just build-wasm-dev # 启动支持 CORS Range 的静态开发服务器 just serveDemo展示内容inline.htmlregisterJson 原始 SQLquery()url-mode.html通过 HTTP range 请求读取远端 Parquettest-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 与源码实现以下几点最值得注意模型名区分大小写—— 查询时使用双引号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()返回Recordstring, 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),仅供参考