🧊

ブラウザで動くSQLite alternativesとしてのLovefield

タイトルはさておき、LovefieldというSQLライクなAPIが使えるライブラリがあって、個人的に便利だったので。

Lovefieldとは

https://github.com/google/lovefield

っていうライブラリ。

実績としてはGmailで使われてたらしい。(現在もそうなのかは不明)

Is Lovefield production quality?
Yes. As of May 2016, Inbox by GMail heavily relies on Lovefield to perform complex client-side structural data queries.
https://github.com/google/lovefield/blob/master/docs/FAQ.md#is-lovefield-production-quality

(コードが書かれたのは主に2015年頃らしいなので、今さらも今さらなネタではある。けど、そもそも日本でそんなに話題になってなかった気がする?)

Public archiveだが

リポジトリアーカイブ状態になってるやん!って思いますよね?私は思いました。

これも中の人によると、

Lovefield is under long-term maintenance but there will be no new features (i.e. feature freeze). G-Mail is using it and as long as G-Mail is still using it we'll keep supporting it. There are not many updates because Lovefield has very few bugs (G-Mail only managed to find 7 bugs during their whole usage, and they are all fixed of course).
https://github.com/google/lovefield/issues/266#issuecomment-678883485

という感じで、機能追加の予定がないからアーカイブってだけで、バグってるとかメンテされてないとかそういうわけではないとのこと。

(このコメントは2020年なので、しれっとGmailは2020年でもLovefieldを使ってた情報が更新されてる)

Lovefield-TS

さっきのコメントにもあるけど、GoogleとしてのLovefieldの開発は終わってるけど、中の人が個人的にTypeScriptにポートしたリライト版がある。

https://github.com/arthurhsu/lovefield-ts

サポートブラウザがよりモダンに限定されてたりNode.jsでも動くようになってたり、本家とは微妙にAPIが変わったりしてるらしいけど、今から使うならこっちでよさそう。

というわけで、`lovefield-ts`をnpmからいつもどおりインストールして使えばよい。

モチベーション

そもそもなぜブラウザでSQL?ってところに関しては、まあそうしたい理由があったからってだけなので割愛するとして。

最近ならSQLiteのWASM版を動かすっていう選択肢もあるし、Lovefieldの他にも似たようなライブラリはある。そんな中での差別化ポイントとしては、やはり軽いことと依存がないってところ。

SQLiteのWASM版はbr圧縮でも最低300KBくらいかかるし、他のライブラリたちもそれなりに重い。インメモリでだけ使いたいのに、そう設定してもIndexedDB(FirefoxのPrivateモードで使えない)が必要だったりと、いまいちハマらなかった。その点Lovefield-TSだと依存なしで最大50KB(Tree-shakingされたらもっと小さい)で済む。

SQLite互換なAPIが必要というよりは、単にRDBライクなシンタックスであればなんでもよかった。

基本的な使い方

Lovefield本家はドキュメントがとにかくわかりにくい(個人の感想です)しコードのシンタックスも古いので、Lovefield-TSのリポジトリの`docs`配下を参照するのがもっともよいかと。

https://github.com/arthurhsu/lovefield-ts/blob/master/docs/index.md

いちおう最低限のコードも載せておくと。

import { schema, Type, DataStoreType } from "lovefield-ts/dist/es6/lf"

// 1. Create tables
const builder = schema.create("my-db", 1);
builder
  .createTable("items")
  .addColumn("id", Type.STRING)
  .addColumn("title", Type.STRING)
  .addColumn("count", Type.NUMBER)
  .addColumn("url", Type.STRING)
  .addNullable(["url"])
  .addPrimaryKey(["id"]);

// 2. Connect to instance(default is `INDEXED_DB`)
const db = await builder.connect({ storeType: DataStoreType.MEMORY });

// 3. Insert data
const items = db.getSchema().table("items");
const itemsRows = [];
for (const data of DATA) {
  itemsRows.push(items.createRow(data));
}
await db.insert().into(items).values(itemsRows).exec();

// 4. Query
const rows = await db
  .select(
    items.col("title"),
    items.col("count")
  )
  .from(items)
  .where(items.col("count").gt(4))
  .exec()
  .then((rows) => /** @type {{ title: string; count: number }[]} */ (rows));

という感じ。直感的でよい。

気になったところ

importまわり

├── LICENSE
├── README.md
├── dist
│   ├── es5
│   │   ├── lf.d.ts
│   │   ├── lf.d.ts.map
│   │   ├── lf.js
│   │   └── lf.js.map
│   ├── es6
│   │   ├── lf.d.ts
│   │   ├── lf.d.ts.map
│   │   ├── lf.js
│   │   └── lf.js.map
│   └── lf.ts
├── index.js
└── package.json

npmへはこういうファイル構成で配布されてて、`from "lovefield-ts"`で`import`すると全部いりの`dist/es5/lf`が降ってくるようになってる。

なのでTree-shakingのためには、`from "lovefield-ts/dist/es6/lf"`ってやるか、`"lovefield-ts/dist/lf"`のTSを直で参照してこっちでコンパイルするかになる。

このへんの挙動はバンドラの設定とかでも微妙に変わるはずで、なんしか試行錯誤が少し必要になってなんだかな・・ってちょっとなった。

型のサポート

TSで書かれてるけど、ジェネリクスで定義されてないAPIが結構あって、利用者サイドの型を楽につけられないことが多かった。

// これとか
items.createRow(data); // Table.createRow(value?: object | undefined): Row

// これとか
const rows = await db
  .select()
  .from(items)
  .exec(); // QueryBuilder.exec(): Promise<unknown>

別にキャストすればいいけど、ちょっと物足りない。あとはカラム名の入力補完も効かない。

あくまでSQLライク

そこまで使い込んだわけではないけど、いわゆるSQLでいうアレ、できないの?ってなったところ。

  • `JOIN`は`INNER`と`LEFT OUTER`だけ
  • `DISTINCT`で対象にできるカラムが1つだけ
  • `HAVING`はサポートされてない

でもまあこれくらいかも。ヘビーなクエリは書いてないので。

おわりに

ここで書いてるほかにも、

などなど、いろいろサポートされてるので、また別の機会で使ってみたい一品だったという話でした。