はじめに
ブラウザのローカル環境だけで動作し、バックエンドを一切必要としないインメモリSQLデータベース兼プレイグラウンド「LuminaDB」を開発しました!
ReactやVueなどのフレームワーク、さらにはSQLite等の外部ライブラリも一切使用せず、HTMLとVanilla JavaScriptのみ(1ファイル)で本格的なSQLエンジンをフルスクラッチで実装しています。
主な機能と特徴
単なるモックではなく、データベースとしての「振る舞い」や「物理的な挙動」のシミュレーションにこだわりました。
1. 本格的なSQLのサポート
SELECT、INSERT、UPDATE、DELETEはもちろんのこと、以下のような高度な構文にも対応しています。
-
テーブル結合:
INNER JOIN,LEFT JOIN -
集計とグループ化:
GROUP BY,HAVING,COUNT,SUMなど -
サブクエリ:
IN (SELECT ...)や、FROM句内でのサブクエリ展開 -
トランザクション:
BEGIN,COMMIT,ROLLBACKに対応し、エラー時の状態巻き戻しが可能。 -
動的評価:
CASE WHEN ... THEN ... END
2. 物理演算と断片化(Fragmentation)のシミュレーション
データの追加や削除を繰り返すと「メモリの断片化(Fragmentation)」が進行し、徐々にクエリの実行速度が低下するギミックを組み込んでいます。
遅くなってきたら、コマンドから VACUUM または OPTIMIZE を実行することで、断片化が解消され速度が回復します。
3. Fetch APIのインターセプト(内部APIサーバー機能)
UI左上の「API Listener」をONにすると、ブラウザの window.fetch をLuminaDBが乗っ取ります(インターセプト)。
これにより、フロントエンドの開発中に実際のバックエンドAPIがまだ無くても、コンソールから fetch('/api/v1/query', { method: 'POST', ... }) を叩くだけで、LuminaDBがモックDBとしてJSONをレスポンスしてくれます。
4. Node.js スタンドアロンサーバーのエクスポート
ブラウザで作ったデータやスキーマ状態をそのまま引き継いだ「Node.jsサーバーのコード」を1ボタンで生成・ダウンロードできます。ダウンロードしたファイル(JS)を node コマンドで実行するだけで、即座にREST API対応のローカルDBサーバーが立ち上がります。
技術スタック・実装の工夫点
「外部ライブラリを使わず、いかにブラウザ上で高速かつ安全にSQLを処理するか」という点に焦点を当てて実装しました。
① 内部データの SoA (Structure of Arrays) 管理
通常、JavaScriptでデータを扱う場合「オブジェクトの配列(AoS: Array of Structures)」になりがちですが、LuminaDBのコアエンジン(ZeroMassEngine)では、パフォーマンスとキャッシュ効率を考慮してSoA(配列の構造体)を採用しています。
// 一般的なAoS(Array of Structures)ではなく…
// [ {id: 1, name: "Alice"}, {id: 2, name: "Bob"} ]
// SoA(Structure of Arrays)で列指向的に管理
this.storageSoA = {
users: {
id: [1, 2, 3, 4, 5],
name: ["Alice", "Bob", "Charlie", "Dave", "Eve"],
age: [25, 30, 22, 35, 28]
}
};
これにより、列指向データベースのように「特定のカラム(年齢など)だけを集計する」際の走査が圧倒的に速くなります。
② String Isolation(文字列の退避)によるパースの安定化
SQLの構文解析(正規表現ベース)を行う際、文字列リテラル内に予約語(SELECT や AND など)が含まれているとパースが壊れてしまいます。
これを防ぐため、クエリ実行の最初にすべての文字列リテラルを抽出し、一時的なプレースホルダー(__LUMINA_STR_x__)に置き換えてから構文解析を行い、最後に復元する「String Isolation」という手法を実装しました。
// 実行前に文字列を退避
sql = sql.replace(/('([^'\\]|\\.)*'|"([^"\\]|\\.)*")/g, match => {
strMap.push(match);
return `__LUMINA_STR_${strMap.length - 1}__`;
});
③ new Function を用いた堅牢な動的ASTコンパイラ
WHERE句やSELECT句内の計算式(例: age >= 30 AND status = 'Active')は、都度JavaScriptの関数として動的にコンパイル(new Function)して評価しています。
この際、任意のコードが実行されないよう(自己崩壊やインジェクション防止)、識別子や演算子を安全な形に変換・置換してから関数化しています。
// WHERE句のコンパイル処理(抜粋)
s = s.replace(/\bAND\b/gi, '&&').replace(/\bOR\b/gi, '||')
.replace(/=/g, '===');
return new Function('row', `
const __resolve = (r, c) => r[c] !== undefined ? r[c] : null;
const __like = (val, pattern) => { /* LIKE演算の処理 */ };
try { return ${s}; } catch(e) { return null; }
`);
④ window.fetch のオーバーライドによるローカルAPI化
ブラウザ内にAPIサーバーを疑似的に立てるため、標準の fetch をラップしています。特定のエンドポイント(/api/v1/query)に対するリクエストのみをLuminaDBエンジンに流し、それ以外はオリジナルの fetch にフォールバックさせる仕組みです。
const originalFetch = window.fetch;
window.fetch = async function() {
let [resource, config] = arguments;
// 特定のAPIエンドポイントだけをインターセプト
if (typeof resource === 'string' && resource.endsWith('/api/v1/query') && config?.method === 'POST') {
const body = JSON.parse(config.body);
const result = await window.LuminaAPI.query(body);
return new Response(JSON.stringify(result.response), {
status: result.status, headers: { 'Content-Type': 'application/json' }
});
}
// それ以外は通常のfetchを実行
return originalFetch.apply(this, arguments);
};
おわりに
「ブラウザのローカル環境だけで、どこまでバックエンド的な挙動(RDB)を再現できるか」という研究開発(R&D)テーマで作成しました。
ちょっとしたデータ操作のテストや、フロントエンド開発時のモックAPIサーバー、あるいはSQLの学習用プレイグラウンドとしても活用できるかと思います。CSVのインポート/エクスポートにも対応しているので、ぜひ手元のデータを流し込んで遊んでみてください!
また、LuminaDBはまだまだ足りていない機能がたくさんありますし、複雑なクエリ等をきちんと試せていない状態なので想定外の不具合が発生するかもしれません。もし使っていただいて、何か発見等ありましたらお気軽にコメントいただけると幸いです。
