DuckDB-Wasm(TypeScript)を使ったときにちょっとつまづいた点のメモ
公式ドキュメントにはコードがどーんと載せられているだけでまったく解説ないので、ちょっとよくわからなくて戸惑った点をメモっておきます。DuckDB-Wasm の使い方自体は Zenn 上でもいくつか記事があるので、そっちを読むといいと思います。
Wasm MVP vs Wasm EH
たとえば↑に載っている Vite のコードはこんな感じです。
import * as duckdb from '@duckdb/duckdb-wasm';
import duckdb_wasm from '@duckdb/duckdb-wasm/dist/duckdb-mvp.wasm?url';
import mvp_worker from '@duckdb/duckdb-wasm/dist/duckdb-browser-mvp.worker.js?url';
import duckdb_wasm_eh from '@duckdb/duckdb-wasm/dist/duckdb-eh.wasm?url';
import eh_worker from '@duckdb/duckdb-wasm/dist/duckdb-browser-eh.worker.js?url';
const MANUAL_BUNDLES: duckdb.DuckDBBundles = {
mvp: {
mainModule: duckdb_wasm,
mainWorker: mvp_worker,
},
eh: {
mainModule: duckdb_wasm_eh,
mainWorker: eh_worker,
},
};
// Select a bundle based on browser checks
const bundle = await duckdb.selectBundle(MANUAL_BUNDLES);
// Instantiate the asynchronus version of DuckDB-wasm
const worker = new Worker(bundle.mainWorker!);
const logger = new duckdb.ConsoleLogger();
const db = new duckdb.AsyncDuckDB(logger, worker);
await db.instantiate(bundle.mainModule, bundle.pthreadWorker);
この mvp とか eh とかいうのは何なのでしょうか? どうやら、MVP は「Minimum Viable Product」で、初期の Wasm バージョンのことを指すみたいです。一方、EH は「Error Handling」らしく、要は例外処理機能があるバージョンです。
参考:https://github.com/WebAssembly/design/blob/main/MVP.md
つまり、上のコードは、Wasm の実装が古いブラウザ(あるいはブラウザ以外のランタイム)でも動くように、ここでブラウザの状態をチェックして適切な方を選択するようになっています。
// Select a bundle based on browser checks
const bundle = await duckdb.selectBundle(MANUAL_BUNDLES);
逆に言えば、新しめのブラウザで動けばいいだけなら、もうちょっと短く書けます。EH の方だけを使って、こんな感じで動くはずです。ちなみに、db.instantiate() の第二引数は、もともと bundle.pthreadWorker が null だったので省略しています。なにか指定すべきものがあるのかもしれませんが、私にはわかりませんでした。とりあえず動作には支障なさそうです。
import * as duckdb from '@duckdb/duckdb-wasm';
import duckdb_wasm_eh from '@duckdb/duckdb-wasm/dist/duckdb-eh.wasm?url';
import eh_worker from '@duckdb/duckdb-wasm/dist/duckdb-browser-eh.worker.js?url';
const worker = new Worker(eh_worker);
const logger = new duckdb.ConsoleLogger();
const db = new duckdb.AsyncDuckDB(logger, worker);
await db.instantiate(duckdb_wasm_eh);
AsyncDuckDBConnection.query<T>() の T に指定する型
こういう感じのコードを書いていたんですが、TypeScript には integer 型がないので range() と型が合わずにエラーになってしまいます。
stmt = await conn.prepare(`SELECT ... FROM range(?);`);
await stmt.query(nPoints)
Error: Binder Error: No function matches the given name and argument types 'range(DOUBLE)'. You might need to add explicit type casts.
Candidate functions:
range(BIGINT)
range(BIGINT, BIGINT)
range(BIGINT, BIGINT, BIGINT)
range(TIMESTAMP, TIMESTAMP, INTERVAL)
最初、これは stmt.query<arrow.Int>(nPoints) みたいな感じで指定すれば解決すると思ったんですが、ここに指定できるのは戻り値のカラムの型でした。つまり、こんな感じです。
stmt = await conn.prepare(`SELECT a, b FROM range(?);`);
await stmt.query<{ a: arrow.Int, b: arrow.Float16 }>(nPoints)
ということで、TypeScript 側から入力の型を指定することはできなさそうです(明示的に Apache Arrow のデータに変換してからやればいける?)。今回は、SQL の側でキャストするのが正解でした。
stmt = await conn.prepare(`SELECT a, b FROM range(?::BIGINT);`);
AsyncDuckDBConnection.query() の戻り値の型
これは、arrow.Table のはずなんですが、外から見るとこの Table が何の型かわからなくなっていて型のエラーが出ます。とりあえず as unknown as arrow.Table とかでむりやり型を付ければ動くのですが、適切な解決方法が何かあるのかもしれません。
query<T>(text): Promise<Table<T>>
デモ

デモページ:https://yutannihilation.github.io/maplibre-geoarrow-deckgl-layers/duckdb
コード:https://github.com/yutannihilation/maplibre-geoarrow-deckgl-layers/blob/main/src/routes/duckdb/%2Bpage.svelte
DuckDB-Wasm で生成したデータを GeoArrow として吐き出して、deck.gl の上に描いています。GeoArrow と deck.gl 関連の話は、昨日記事に書いたのでそっちを読んでください。
Discussion
そこには第三の矢である
COI (Cross Origin Isolation)版で引き渡すものだったと記憶しています。シングルスレッド前提の
MVPとEHはnullで問題ナッシングです。型が潰されるのはどうにもできなかったので
でお茶を濁してます。(
StructRowProxyでも型潰れてる)StructRowProxyは@apache-arrow/tsで型付けできます。ありがとうございます!! 情報とても助かります。
なるほど、そういうことなんですね! 理解が深まりました。
Table の型の方は、そういうやり方もあるんですね。次やるときはそれを試してみようと思います。