與實(shí)戰(zhàn):在瀏覽器里跑 DataFusion 語(yǔ)義 SQL 引擎)
WrenAI wren-core-wasm 演進(jìn)與實(shí)戰(zhàn)在瀏覽器里跑 DataFusion 語(yǔ)義 SQL 引擎【免費(fèi)下載鏈接】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.項(xiàng)目地址: https://gitcode.com/GitHub_Trending/wr/WrenAIwren-core-wasm是 WrenAI 將核心 SQL 引擎編譯為 WebAssembly 的產(chǎn)物它以 Apache DataFusion 為執(zhí)行內(nèi)核把語(yǔ)義層MDL的查詢(xún)改寫(xiě)能力直接搬到瀏覽器與 Node.js 環(huán)境無(wú)需任何服務(wù)端即可對(duì) Parquet、CSV、JSON 數(shù)據(jù)執(zhí)行 SQL。本文以 CHANGELOG.md 的版本演進(jìn)為時(shí)間線(xiàn)結(jié)合 README.md、AGENT_GUIDE.md 與 src/lib.rs 的源碼實(shí)現(xiàn)完整講解它的安裝方式、兩種數(shù)據(jù)加載模式、Cube 查詢(xún) API、底層 tokio 運(yùn)行時(shí)修復(fù)原理與從源碼構(gòu)建的完整流程。讀完本文你可以獨(dú)立在瀏覽器或 Node 環(huán)境中搭建一個(gè)免服務(wù)器的語(yǔ)義層查詢(xún)應(yīng)用并理解其關(guān)鍵實(shí)現(xiàn)細(xì)節(jié)與踩坑點(diǎn)。版本演進(jìn)脈絡(luò)從 WASM 模塊到完整 Cube 支持core/wren-core-wasm的 CHANGELOG.md 記錄了四個(gè)階段的演進(jìn)版本時(shí)間核心變更0.2.02026-05-05新增 wren-core-wasm 模塊瀏覽器 WASM 支持并將 wren-engine 導(dǎo)入 core/ 目錄0.3.02026-05-05正式發(fā)布帶瀏覽器 WASM 支持的 wren-core-wasm 模塊0.4.02026-05-15完整 Cube 支持校驗(yàn)、翻譯、PyO3、CLI、WASM、文檔全線(xiàn)打通0.4.12026-05-15Bug 修復(fù)讓query()通過(guò) tokio runtime 驅(qū)動(dòng)解決UNION ALL崩潰trap問(wèn)題從 src/lib.rs 中定義的里程碑M1 到 M4可以看出這套架構(gòu)的成型路線(xiàn)M1 完成 DataFusion 的 WASM 編譯與內(nèi)存查詢(xún)M2 支持瀏覽器內(nèi) Parquet 上傳查詢(xún)M3 引入 wren-core 語(yǔ)義層MDL 計(jì)劃改寫(xiě)M4 發(fā)布 npm 包與 TypeScript API 封裝。當(dāng)前倉(cāng)庫(kù)狀態(tài)對(duì)應(yīng) M4 完成后的成熟形態(tài)。安裝與引入npm 包與 CDN 兩種方式通過(guò) package.json 可以確認(rèn)包名與入口安裝命令如下npm install wrenai/wren-core-wasm或者在瀏覽器中通過(guò) CDN 直接以 ES Module 方式引入script typemodule import { WrenEngine } from https://unpkg.com/wrenai/wren-core-wasm0.3.0/dist/index.js; /script注意請(qǐng)使用 unpkg不要用 jsDelivr。jsDelivr 免費(fèi) CDN 對(duì)單個(gè)文件有 50 MB 限制而該 WASM 二進(jìn)制原始體積約 68 MB會(huì)導(dǎo)致.wasm請(qǐng)求被拒絕。這一點(diǎn)在 README.md 與 AGENT_GUIDE.md 中均有明確提示。快速上手Inline 模式本地開(kāi)發(fā)與內(nèi)嵌儀表盤(pán)的推薦路徑Inline 模式直接將數(shù)據(jù)注冊(cè)到引擎內(nèi)存中不依賴(lài)任何服務(wù)器也避開(kāi)了 HTTP Range 請(qǐng)求與 CORS 的坑。對(duì)于數(shù)據(jù)總量約 50 MB 以下的場(chǎng)景這是阻力最小的路徑。import { WrenEngine } from wrenai/wren-core-wasm; const engine await WrenEngine.init(); // 將 JSON 數(shù)據(jù)注冊(cè)為表 await engine.registerJson(orders, [ { id: 1, customer: Alice, amount: 100 }, { id: 2, customer: Alice, amount: 250 }, { id: 3, customer: Bob, amount: 120 }, ]); // 或者從 ArrayBuffer 注冊(cè) Parquet const response await fetch(orders.parquet); await engine.registerParquet(orders, await response.arrayBuffer()); // 或者注冊(cè) CSV —— 字符串或字節(jié)皆可可指定 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 傳空字符串表示使用預(yù)注冊(cè)表 await engine.loadMDL(mdl, { source: }); const rows await engine.query(SELECT * FROM Orders LIMIT 10);一個(gè)常見(jiàn)的儀表盤(pán)開(kāi)發(fā)模式是先用fetch()并行拉取每個(gè) Parquet 文件再按順序調(diào)用registerParquet注冊(cè)。因?yàn)?WASM 引擎是單線(xiàn)程的并發(fā)注冊(cè)并不安全詳見(jiàn) AGENT_GUIDE.md 的 Common Pitfalls。URL 模式通過(guò) HTTP Range 請(qǐng)求直讀遠(yuǎn)端 Parquet當(dāng)數(shù)據(jù)量較大或已經(jīng)托管在 CDN 上時(shí)可以使用 URL 模式。此時(shí)數(shù)據(jù)仍存放在 HTTP 服務(wù)器上DataFusion 通過(guò)HTTP range 請(qǐng)求逐個(gè)讀取 Parquet 文件先讀 footer再按行組讀取。服務(wù)器必須支持Range:請(qǐng)求頭否則查詢(xún)會(huì)在讀取 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 }]底層實(shí)現(xiàn)URL 模式如何工作從源碼看loadMDL會(huì)根據(jù)source參數(shù)分發(fā)到三種模式src/lib.rsURL 模式http://…或https://…開(kāi)頭對(duì)每個(gè)模型注冊(cè)一個(gè) DataFusionListingTable物理文件路徑固定為{source}/{裸表名}.parquetfallback 模式source從每個(gè)模型的tableReference自動(dòng)探測(cè) URL 還是本地表用于兼容舊版 MDL本地模式其他任意非空字符串要求調(diào)用方已通過(guò)registerParquet/registerJson預(yù)注冊(cè)物理表若缺少任何模型的物理表loadMDL會(huì)立即返回Unresolved models: [...]錯(cuò)誤而不是把問(wèn)題推遲到查詢(xún)階段。URL 模式下每個(gè)唯一 origin 只注冊(cè)一次 HTTP object storesrc/lib.rs并且所有模型的 schema 推斷會(huì)先暫存、全部成功后才寫(xiě)入self.ctx避免失敗的loadMDL留下半注冊(cè)狀態(tài)。tableReference使用裸表名如orders引擎會(huì)自動(dòng)在 URL 模式下拼接{source}/{name}.parquet。需要注意的是當(dāng)前階段s3://和gs://尚未納入 URL 模式標(biāo)記為 Phase 4 工作會(huì)被回退到本地模式并快速失敗。如何選擇本地開(kāi)發(fā)服務(wù)器URL 模式依賴(lài) DataFusion 的ListingTable通過(guò) HTTP range 請(qǐng)求讀取 Parquet因此本地開(kāi)發(fā)服務(wù)器必須支持Range:請(qǐng)求頭服務(wù)器Range 支持說(shuō)明python -m http.server? 不支持Python 內(nèi)置URL 模式下應(yīng)避免python -m RangeHTTPServer? 支持pip install rangehttpservernpx serve?? 僅單范圍基于sirv請(qǐng)求超出 EOF 時(shí)可能返回416npx http-server? 支持默認(rèn)帶 CORScaddy file-server? 支持生產(chǎn)可用Vite?? 僅單范圍同樣基于sirv與npx serve有相同的416邊界問(wèn)題webpack-dev-server?? 僅單范圍multipart range 請(qǐng)求會(huì)回退為返回整個(gè)資源快速檢查命令curl -I -H Range: bytes0-1023 http://localhost:PORT/file.parquet應(yīng)返回HTTP/1.1 206 Partial Content而非200。如果只能使用不支持 Range 的服務(wù)器請(qǐng)改用 Inline 模式用fetch()一次性拉取每個(gè)文件再通過(guò)registerParquet注冊(cè)。Node.js 中使用必須顯式傳入 WASM 二進(jìn)制WrenEngine.init()默認(rèn)通過(guò)import.meta.url定位同目錄的wren_core_wasm_bg.wasm在 Node 中該 URL 會(huì)解析為file://。Node 的undicifetch 不支持file://協(xié)議因此init()會(huì)直接拋出異常。正確做法是把 WASM 二進(jìn)制作為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), });這一模式同樣適用于單元測(cè)試與 CI 冒煙檢查node --test。倉(cāng)庫(kù)內(nèi)的集成測(cè)試 sdk/tests/index.test.mjs 正是采用這種方式用readFileSync讀取dist/wren_core_wasm_bg.wasm后傳入WrenEngine.init({ wasmUrl: wasmBytes })。0.4.0 核心特性完整 Cube 支持0.4.0 版本為 WASM 模塊補(bǔ)全了 Cube 語(yǔ)義覆蓋校驗(yàn)、翻譯、PyO3、CLI、WASM 與文檔。在 JS 側(cè)體現(xiàn)為兩個(gè)新 APIcubeQuery()與listCubes()類(lèi)型定義見(jiàn) sdk/src/index.ts。listCubes先探索再查詢(xún)listCubes()返回 MDL 中定義的全部 Cube 信息包括name、baseObject、measures、dimensions、timeDimensions與hierarchies便于 Agent 在調(diào)用cubeQuery前先發(fā)現(xiàn)可查詢(xún)的度量與維度const cubes engine.listCubes(); // → [{ name: order_metrics, baseObject: orders, measures: [...], // dimensions: [...], timeDimensions: [...], hierarchies: {...} }]cubeQuery結(jié)構(gòu)化聚合查詢(xún)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, });其底層實(shí)現(xiàn)src/lib.rs將結(jié)構(gòu)化CubeQuery通過(guò) wren-core 翻譯為 SQL自動(dòng)生成GROUP BY、DATE_TRUNC、WHERE子句再走與query()相同的執(zhí)行路徑。時(shí)間分桶的結(jié)果列以dim__granularity的形式暴露例如created_at__monthdateRange遵循「起始包含、結(jié)束排除」的語(yǔ)義。cubeQuery 與 query 如何取舍場(chǎng)景推薦在維度上聚合度量可帶時(shí)間分桶cubeQuery自由 SQL跨模型 join、窗口函數(shù)、自定義 CTEqueryMDL 未定義 CubequeryFilter 支持 12 種操作符eq、neq、in、not_in、gt、gte、lt、lte、contains、starts_with、is_null、is_not_null。其中in/not_in的value傳數(shù)組is_null/is_not_null省略value。時(shí)間粒度支持year/quarter/month/week/day/hour/minute七檔。注意listCubes()與cubeQuery()都要求先完成loadMDL()否則會(huì)拋出錯(cuò)誤這一點(diǎn)在測(cè)試用例如 sdk/tests/index.test.mjs 中的cubeQuery without loadMDL fails clearly中有明確驗(yàn)證。0.4.1 關(guān)鍵修復(fù)query() 經(jīng)由 tokio runtime 驅(qū)動(dòng)0.4.1 的修復(fù)條目看似只有一行卻解決了一個(gè)非常隱蔽的 WASM 運(yùn)行時(shí)崩潰問(wèn)題。源碼注釋src/lib.rs揭示了完整原因DataFusion 的物理算子例如CoalescePartitionsExec它會(huì)包裹任何多分區(qū)計(jì)劃如UNION ALL/INTERSECT/EXCEPT內(nèi)部會(huì)調(diào)用tokio::task::spawn。spawn在沒(méi)有 tokio runtime 上下文時(shí)會(huì)以there is no reactor runningpanic —— 而僅靠wasm-bindgen-futures并不會(huì)提供這個(gè)上下文。因此WrenEngine結(jié)構(gòu)體持有一個(gè)current_thread 模式的 tokio runtimesrc/lib.rsquery()通過(guò)runtime.block_on(...)驅(qū)動(dòng)整個(gè)查詢(xún)未來(lái)讓 DataFusion 能看到一個(gè)活的調(diào)度器。沒(méi)有這層包裝UNION ALL這類(lèi)多分區(qū)計(jì)劃會(huì)在 JS 側(cè)表現(xiàn)為晦澀的RuntimeError: unreachable。回歸測(cè)試test_union_all_does_not_trapsrc/lib.rs與集成測(cè)試中的 set operators 一組用例sdk/tests/index.test.mjs共同驗(yàn)證了UNION ALL、UNION、INTERSECT、EXCEPT全部可以正常返回結(jié)果。API 參考WrenEngine 完整方法表WrenEngine的 TypeScript 封裝位于 sdk/src/index.tsquery()返回Recordstring, unknown[]可直接供 Chart.js、D3、Recharts 等圖表庫(kù)消費(fèi)。WrenEngine.init(options?)static async init(options?: WrenEngineOptions): PromiseWrenEngine選項(xiàng)類(lèi)型說(shuō)明wasmUrlstring \| URL \| BufferSourceWASM 二進(jìn)制來(lái)源。默認(rèn)通過(guò)import.meta.url定位同目錄的wren_core_wasm_bg.wasmengine.loadMDL(mdl, profile)async loadMDL(mdl: object, profile: WrenProfile): Promisevoid參數(shù)類(lèi)型說(shuō)明mdlobjectMDL 清單會(huì)被 JSON 序列化profile.sourcestringhttps://...走 URL 模式使用預(yù)注冊(cè)表其他非空字符串走本地模式engine.registerParquet(name, data)async registerParquet(name: string, data: ArrayBuffer): PromisevoidInline 模式下須在loadMDL之前調(diào)用。接受任何BufferSourceArrayBuffer、TypedArray 如Uint8Array、Node BufferTypedArray 的byteOffset/byteLength視圖元數(shù)據(jù)會(huì)被保留。engine.registerJson(name, data)async registerJson(name: string, data: object[]): Promisevoid底層將 JSON 數(shù)組轉(zhuǎn)換為 NDJSON每行一個(gè)對(duì)象Arrow JSON reader 的格式要求再解析成 Arrow RecordBatch 注冊(cè)為MemTable。engine.registerCsv(name, data, options?)async registerCsv( name: string, data: string | BufferSource, options?: CsvReadOptions, ): Promisevoid默認(rèn)第一行為表頭schema 從前 1000 行推斷。完整選項(xiàng)如下選項(xiàng)camelCase類(lèi)型默認(rèn)值說(shuō)明headerbooleantrue首行是否為表頭delimiterstring,字段分隔符單個(gè) ASCII 字符quotestring\引號(hào)字符單個(gè) ASCII 字符escapestring未設(shè)置轉(zhuǎn)義字符單個(gè) ASCII 字符terminatorstring\n或\r\n記錄終止符單個(gè) ASCII 字符batchSizenumber8192RecordBatch 大小inferRowsnumber1000用于推斷 schema 的行數(shù)設(shè)置schema時(shí)忽略schemaCsvSchemaColumn[]推斷顯式 Arrow schema{ name, type, nullable? }[]schema 列類(lèi)型大小寫(xiě)不敏感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()執(zhí)行經(jīng)過(guò)語(yǔ)義層的 SQL 并返回解析后的對(duì)象數(shù)組free()在引擎不再需要時(shí)釋放 WASM 內(nèi)存。從源碼構(gòu)建wasm-pack 全流程構(gòu)建前提Rust 工具鏈、wasm-pack、Node.js 16engines字段已在 package.json 中聲明。cd core/wren-core-wasm # 安裝 TypeScript 開(kāi)發(fā)依賴(lài) npm install # 構(gòu)建 WASM 二進(jìn)制需要 wasm32-unknown-unknown target wasm-pack build --target web --release # 構(gòu)建 TypeScript 封裝并組裝 dist/ npm run build:dist # 運(yùn)行集成測(cè)試 npm test # 僅做類(lèi)型檢查 npm run typecheck倉(cāng)庫(kù)還提供了 justfile 封裝常用任務(wù)just build-wasm-devdebug 構(gòu)建適合示例調(diào)試、just serve在 localhost:8787 啟動(dòng)帶 CORS 與 Range 支持的靜態(tài)開(kāi)發(fā)服務(wù)器、just size報(bào)告 WASM 二進(jìn)制體積等。macOS 注意事項(xiàng)在 macOS 上構(gòu)建 WASM 可能需要 LLVM 來(lái)編譯 C 依賴(lài)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可運(yùn)行的示例頁(yè)面examples/目錄隨倉(cāng)庫(kù)附帶了多個(gè)可直接運(yùn)行的瀏覽器 demo它們直接引用pkg/下的本地構(gòu)建產(chǎn)物因此始終反映當(dāng)前源碼狀態(tài)README.md# 構(gòu)建 WASM 二進(jìn)制debug 構(gòu)建即可運(yùn)行示例 just build-wasm-dev # 啟動(dòng)支持 CORS Range 的靜態(tài)開(kāi)發(fā)服務(wù)器 just serveDemo展示內(nèi)容inline.htmlregisterJson 原始 SQLquery()url-mode.html通過(guò) HTTP range 請(qǐng)求讀取遠(yuǎn)端 Parquettest-cdn.html從 unpkg 加載已發(fā)布包c(diǎn)ube-quickstart.html最小cubeQuery()—— 三個(gè)預(yù)設(shè)查詢(xún)分組、過(guò)濾、時(shí)間分桶cube-explorer.html表單驅(qū)動(dòng)的CubeQuery構(gòu)建器選度量/維度、加過(guò)濾、選粒度與日期范圍csv-quickstart.htmlregisterCsv()讀取data/真實(shí)文件schema 推斷、自定義分隔符TSV、帶顯式 schema 的無(wú)表頭 CSV修改 Rust 代碼后重新運(yùn)行just build-wasm-dev并刷新頁(yè)面即可生效因?yàn)槭纠苯訌膒kg/wren_core_wasm.js導(dǎo)入。實(shí)戰(zhàn)建議與常見(jiàn)坑位綜合 AGENT_GUIDE.md 的 Common Pitfalls 與源碼實(shí)現(xiàn)以下幾點(diǎn)最值得注意模型名區(qū)分大小寫(xiě)—— 查詢(xún)時(shí)使用雙引號(hào)FROM Orders而非FROM Orders。調(diào)用順序—— Inline 模式下必須先registerJson/registerParquet/registerCsv再loadMDL本地模式下缺少物理表會(huì)在加載時(shí)立即報(bào)Unresolved models而不是等到查詢(xún)才崩潰。WASM 體積較大約 68 MB 原始 / 約 14 MB gzip—— 在WrenEngine.init()期間應(yīng)顯示加載指示器。source: 的語(yǔ)義是「僅使用預(yù)注冊(cè)表」—— 期望 URL 模式時(shí)不要傳空字符串。URL 模式需要 HTTP(S) CORS—— 用file://打開(kāi)頁(yè)面無(wú)法作為數(shù)據(jù)源頁(yè)面與 Parquet 都應(yīng)通過(guò) HTTP(S) 提供服務(wù)并配置 CORS。Range 支持是 URL 模式的前提——python -m http.server不支持可改用支持 Range 的服務(wù)器或回退到 Inline 模式。Node 環(huán)境必須傳wasmUrl: BufferSource—— 否則init()會(huì)因file://fetch 不被支持而立即失敗。注冊(cè)操作必須串行—— WASM 引擎是單線(xiàn)程的并發(fā)注冊(cè)registerParquet/registerJson不安全。查詢(xún)結(jié)果可直接渲染——query()返回Recordstring, unknown[]配合 Chart.js/D3/Recharts 或console.table即可快速可視化。結(jié)合 CHANGELOG.md 的版本軌跡可以看到wren-core-wasm 的核心價(jià)值在于把「MDL 語(yǔ)義層 DataFusion 執(zhí)行引擎」完整編譯到瀏覽器端既能在無(wú)服務(wù)器場(chǎng)景下對(duì)中小數(shù)據(jù)量做即席分析又能通過(guò) URL 模式直連 CDN 上的大規(guī)模 Parquet 數(shù)據(jù)集同時(shí)以 Cube API 為上層 Agent 提供了結(jié)構(gòu)化的聚合查詢(xún)?nèi)肟凇!久赓M(fèi)下載鏈接】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.項(xiàng)目地址: https://gitcode.com/GitHub_Trending/wr/WrenAI創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考