🍃このブログは移転しました。
3秒後、自動的に移動します・・・。

SQLite公式のWASM実装を試す

sqlite3 wasm docs: sqlite3 WebAssembly & JavaScript Documentation Index

別にいままでも、公式ではないWASM実装はブラウザで使えてたけど、公式が出すからには何かあるに違いない!ということで一応。

インストール

いつものようにまず、`npm`からインストール・・・できない。

少なくとも、調べた限りでは、ダウンロードページから`.zip`で落としてくるほかなさそうだった。

(この時点でそういうステータスなんやなと悟る)

SQLite Download Page

このページにある、WebAssembly & JavaScriptのところ。
これを書いてる時点では、`sqlite-wasm-3400000.zip`ってやつだった。

中身はこんな感じ。

sqlite-wasm-3400000
├── README.txt
├── common
│   ├── SqliteTestUtil.js
│   ├── emscripten.css
│   └── testing.css
├── demo-123-worker.html
├── demo-123.html
├── demo-123.js
├── demo-jsstorage.html
├── demo-jsstorage.js
├── demo-worker1-promiser.html
├── demo-worker1-promiser.js
├── demo-worker1.html
├── demo-worker1.js
├── index.html
├── jswasm
│   ├── sqlite3-opfs-async-proxy.js
│   ├── sqlite3-worker1-promiser.js
│   ├── sqlite3-worker1.js
│   ├── sqlite3.js
│   └── sqlite3.wasm
├── tester1-worker.html
├── tester1.html
└── tester1.js

肝心のコードも、ただのデモのコードもごっちゃになって入ってる。

使ってみる

とりあえず動かしてみる。
同梱されてるデモでも確認できるけど、最小構成を試したいので自力で書く。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>SQLite WASM</title>
</head>
<body>
  <script src="./sqlite-wasm-3400000/jswasm/sqlite3.js"></script>
  <script src="./main.js"></script>
</body>
</html>

ダウンロードしてきたファイルにある、`jswasm/sqlite3.js`を使うのが最小構成。

`main.js`ではこのように。

(async () => {
  const sqlite3 = await window.sqlite3InitModule();
  console.log(sqlite3);
})();

`sqlite3InitModule()`を呼ぶと、`jswasm/sqlite3.wasm`が読み込まれて、JSから操作できるようになる。

にしてもこの`sqlite3.js`、ESMでもなくTree-shakingできない上に10000行!みたいな感じで、うーむ。
ドキュメントには`sqlite3.mjs`も同梱って書いてあるけど、この時は入ってなかった。

CJSっぽいコードは見たので、自前でバンドルすればそういう書き方はできるはず。

Workerからも使える

new Worker("./jswasm/sqlite3-worker1.js");

というようにもできるし、

<script src="./jswasm/sqlite3-worker1-promiser.js"></script>

から、Workerとのやり取りを`Promise`でラップしたものが使えてちょっとうれしい。

sqlite3 wasm docs: Workers and Promises (a.k.a. Worker1 and Promiser)

sqlite3オブジェクト

さっきのコードで取得した`sqlite3`から、各種オペレーションをやっていくことになるが、使えるAPIは大きく2つある。

前者はC言語APIに準拠してるので、よりstableとのこと。(`sqlite3_open`とかそういうやつ)

後者はJSで使いやすいようにしてみた!というやつで、変更の可能性もあるやつとのこと。

まあ我々が期待するのは後者かなって感じ。

sqlite3 wasm docs: Object Oriented API #1 (a.k.a. oo1)

コードで書いてみるとこんな具合。

(async () => {
  const sqlite3 = await window.sqlite3InitModule();

  const { DB } = sqlite3.oo1;
  // Use :memory: storage
  const db = new DB();

  db.exec("CREATE TABLE IF NOT EXISTS users(id INTEGER, name TEXT)");

  const stmt = db.prepare("insert into users values(?, ?)");
  stmt.bind([1, "Alice"]).stepReset();
  stmt.bind([2, "Bob"]).stepReset();
  stmt.finalize();

  const resultRows = [];
  db.exec({
    sql: "SELECT * FROM users",
    rowMode: "object",
    resultRows,
  });

  // Logs { id, name }[]
  console.log(resultRows);
})();

ふむ。

このOO1、他にもDBの状態を`LocalStorage`なんかに保存する機能や、OPFSで永続化するためのコードなんかも入ってる。

The File System Access API with Origin Private File System | WebKit

OPFSって単語は初見やったけど、要はFile System Access APIのことね。

まとめ

Except where noted in the non-goals, provide a more-or-less feature-complete wrapper to the sqlite3 C API, insofar as WASM feature parity with C allows for. In fact, provide at least the following APIs...
1. Bind a low-level sqlite3 API which is as close to the native one as feasible in terms of usage.
2. A higher-level OO API, more akin to sql.js and node.js-style implementations. This one speaks directly to the low-level API. This API must be used from the same thread as the low-level API.
3. A Worker-based API which speaks to the previous APIs via Worker messages. This one is intended for use in the main thread, with the lower-level APIs installed in a Worker thread, and talking to them via Worker messages. Because Workers are asynchronous and have only a single message channel, some acrobatics are needed here to feed async work results back to the client (as we cannot simply pass around callbacks between the main and Worker threads).
4. A Promise-based variant of the Worker API (#3, above) which entirely hides the cross-thread communication aspects from the user.

Insofar as possible, support persistent client-side storage using available JS APIs. As of this writing, that includes the Origin-Private FileSystem (OPFS) and (very limited) storage via the window.localStorage and window.sessionStorage backend.

というわけで、

  • 各種APIをWASMから提供することが目標
    • C APIみたいなLowレベルのもの
    • OO APIみたいなHighレベルのもの
    • Workerで利用できるもの
    • WorkerをメインスレッドからもPromise経由で利用できるもの
  • ブラウザでの永続化もできるようにしたい

という汎用的な目標があるそうな。

ローカルで完結するアプリを作る!って場合に、はじめて輝きそうではある。
まあブラウザでSQLite使いたいモチベーションって、それくらいなもんかって気もするけど。

今のままだと`sql.js`よりもフットプリントが大きいので、汎用性は求めてないって場合には使わないかも。