8月20日、Farid Zakaria氏が「Three ways to smuggle SQLite into Nix」と題した記事を公開した。Nixのビルトインにはbuiltins.sqliteが存在しない。だがパッケージ数が30万件を超えたとき、JSONの一括パースはもはや現実的でなくなる。では、どうするか——Zakaria氏は3つの「抜け穴」を掘った。
Nixは再現性のあるビルドと宣言的なパッケージ管理を実現するツールで、近年エンジニアリング組織への採用が急増している。その中核となる評価器はNix言語で書かれた設定を解釈するが、外部データを扱うAPIは意図的に制限されている。ファイルを読むbuiltins.readFileやbuiltins.fromJSONは存在するが、いずれも先行評価(eager evaluation)であり、ストリームパースや部分読み込みはできない。小規模なデータなら問題ないが、インデックス規模になると話は変わる。
なぜJSONではなくSQLiteが必要なのか
Zakaria氏が開発するnixpkgs-multiverseは、Nixpkgsのあらゆるバージョンを検索できるインデックスツールだ。その中核は、305,492パッケージバージョン、31,904パッケージ、1,534リビジョンを網羅した2つのJSONファイル(計12.8MiB)で構成される。
問題はbuiltins.fromJSONの挙動だ。特定のパッケージ1件を引くだけで、5.3MBすべてをパースしてNixヒープ上に305,492個の値を展開する。「1件の検索が全件のコストを払う」という構造だ。
NixのAttrSet(連想配列)はソート済み配列なのでルックアップ自体はバイナリサーチで済む。コストはJSONのパース、値のアロケーション、そして大きなファイルのダウンロードにある。必要なのは効率的なデータ符号化と宣言的なクエリ手段、すなわちSQLiteだ。しかしNixにはbuiltins.sqliteは存在しない。
方法1:builtins.exec(最もシンプルな抜け穴)
Nixにはbuiltins.execという関数がある。文字列のリストを受け取り、プログラムを実行し、その標準出力をNix式としてパースするというものだ。有効化には安全でないことを明示する設定フラグ(allow-unsafe-native-code-during-evaluation)が必要になる。
$ nix eval --option allow-unsafe-native-code-during-evaluation true \
--expr 'builtins.exec [ "/bin/sh" "-c" "echo 42" ]'
42
SQLiteはNixのAttrSet構文をそのまま出力できるため、中間シリアライズ形式が不要になる。
let
versionsOf = attr: builtins.exec [
"${sqlite}/bin/sqlite3" "-noheader" "-separator" "" "./index.db"
''
SELECT '{' || group_concat(
'"' || version || '" = ' ||
COALESCE(CAST(rev AS TEXT), 'null') || ';', ' ')
|| '}'
FROM versions WHERE attr = '${attr}';
''
];
in
versionsOf "hello"
{ "2.10" = 728; "2.12" = 822; "2.12.1" = 1369; ... }
欠点は、クエリごとにfork・exec・SQLiteプロセス起動・Nixパーサーによる再パースが走ることだ。クエリ数が少なければ許容範囲だが、大量クエリには向かない。
方法2:builtins.importNative(共有ライブラリの直結)
builtins.importNativeは、共有ライブラリ(.so)のパスとシンボル名を受け取り、dlopenして呼び出すAPIだ。こちらもallow-unsafe-native-code-during-evaluationフラグが必要になる。
実装は通常のC++でNix APIを使う。exec方式との最大の違いはSQLiteハンドルをキャッシュできる点だ。同じデータベースを繰り返し参照する場合、B-treeページをメモリにウォームな状態で保持できる。
/* The whole point: the database handle outlives a single query, so the
b-tree pages we touch stay warm for the rest of the evaluation. */
std::map<std::string, sqlite3 *> handles;
クエリ結果はテキストを介さずNix値として直接構築されるため、オーバーヘッドも小さい。
$ nix eval --impure \
--option allow-unsafe-native-code-during-evaluation true \
--expr '(builtins.importNative
./libnixsqlite.so "nix_sqlite_versions"
) "./index.db" "hello"'
{ "2.10" = 728; "2.12" = 822; ... }
任意のネイティブコードを実行できるという意味では安全性はexec方式と変わらない。ただし、DBハンドルを評価セッション全体で保持できる点で、多数のクエリが走るユースケースに最も向いている。
方法3:builtins.wasm(サンドボックス化されたアプローチ)
Determinate Systemsが2026年3月にリリースしたbuiltins.wasmは、WebAssemblyモジュール内の関数を呼び出す拡張だ。Determinate SystemsはNixの商用サポートを提供する企業で、標準のNixには含まれない独自拡張を複数提供している。builtins.wasmもその一つであり、上記2つと異なりサンドボックス化と決定論的実行を設計上の前提としており、allow-unsafe-native-code-during-evaluationフラグは不要になる。
SQLiteは公式WebAssemblyビルドを提供しているため、組み合わせは理論上可能だ。しかし一つ障壁があった。
Nixの文字列はNULLバイトを含められないため、SQLiteのバイナリファイルをそのまま読み込めない。
$ nix eval --impure --expr 'builtins.stringLength (builtins.readFile ./index.db)'
error: the contents of the file '...' cannot be represented as a Nix string
builtins.wasmのAPIにはread_fileという関数があり、バイナリファイルをWASMメモリに読み込める。しかしファイル全体を読む設計のため、JSONと同じ問題が残る。
そこでZakaria氏はNix自体にパッチを当て、read_file_rangeというオフセットと長さを指定したランダムアクセス関数を追加した。
uint32_t read_file_range(ValueId pathId, uint64_t offset,
uint32_t ptr, uint32_t len)
{
/* pread() で必要なページだけを読む */
auto n = pread(fd.get(), buf.data(), len, offset);
...
}
この関数をSQLiteのカスタムVFS(仮想ファイルシステム)レイヤーのxReadコールバックに接続することで、SQLiteのページャーが要求する分だけをNix評価器から取得できる。
欠点は2つある。builtins.wasmは呼び出しごとにフレッシュなインスタンスを生成する設計のため、セッションをまたいだキャッシュが効かない。また、このアプローチはDeterminate Systems版Nixにさらにread_file_rangeパッチを当てた非標準ビルドを必要とするため、すぐに試せる環境は限られる。
3方式の比較と現状
以下に3方式の特性をまとめる。どれを選ぶかはクエリ数・安全要件・使用するNixのディストリビューションによって変わる。
| 方式 | 追加フラグ | クエリのオーバーヘッド | 安全性 | DBキャッシュ |
|---|---|---|---|---|
builtins.exec |
unsafe-native-code | fork + exec(高) | 低 | なし |
builtins.importNative |
unsafe-native-code | dlopen(低) | 低 | DBハンドル保持可 |
builtins.wasm |
不要(ただしパッチ版Nix必須) | インスタンス再生成(中) | 高 | なし |
いずれも「抜け穴」であることに変わりはなく、Nixコアにbuiltins.sqliteが存在しない以上、現状はこうした迂回路を使うしかない。Zakaria氏は記事の末尾で「本来はbuiltins.sqliteをNix本体に追加するのが正しい解決策だ」と明言しており、今後のNix本体への提案が注目される。
詳細はThree ways to smuggle SQLite into Nixを参照していただきたい。