[Astro] #134 SQLite Viewer v1 — sql.js WASMによるブラウザ完結型SQLiteデータベースビューア/エディタ

[Astro] #134 SQLite Viewer v1 — sql.js WASMによるブラウザ完結型SQLiteデータベースビューア/エディタ

はじめに

.sqlite / .db ファイルをちょっと中身を確認したい。テーブル構造を見たい。簡単なSQLを叩きたい。そのためだけにDB Browserをインストールしたり、コマンドラインで sqlite3 を起動するのは面倒です。

sql.jsはSQLiteをEmscriptenでWASMコンパイルしたライブラリで、CDNから読み込むだけでブラウザ内にSQLiteエンジンが立ち上がります。既存のブラウザ版SQLiteビューアは多数ありますが、lain-labのUIに統合し、スキーマ解析やDB情報パネルなど「ファイルの中身をすばやく把握する」機能に寄せた構成で実装しました。

スクリーンショット

テーブルデータ表示

SQLite Viewer — テーブルデータ表示

スキーマ表示

SQLite Viewer — スキーマ表示

SQLエディタ

SQLite Viewer — SQLエディタ

動画(GIF)

SQLite Viewer — 操作デモ

Sample data

テストデータにはNorthwind Sample Databaseを使用しています。


1. 全体アーキテクチャ

左サイドバー + 右メインエリアの2カラム構成です。

[ Astroページ (sqlite-viewer.astro) ]

   ├── 左サイドバー
   │   ├─ ファイルドロップゾーン
   │   ├─ DB情報パネル(PRAGMA取得)
   │   ├─ テーブル検索フィルタ
   │   └─ テーブル一覧ナビゲーション

   └── 右メインエリア
       ├─ タブ: [テーブルデータ] [スキーマ] [SQLエディタ]
       ├─ 行数制御 (100 / 300 / 1000 / ALL)
       └─ エクスポート (CSV / JSON / DB保存)

WASMエンジンは初回操作時にCDNから遅延ロードし、以降はメモリ内のSQLiteインスタンスに対してすべての操作を実行します。


2. sql.js — SQLite WASMエンジン

ライブラリ選定

sql.js は SQLite を Emscripten で WASM にコンパイルしたライブラリです。安定性が高く、CDNから1行で導入できます。

async function initSqlEngine() {
  if (SQL) return SQL;
  // CDNからスクリプトロード
  if (!window.initSqlJs) {
    await new Promise((resolve, reject) => {
      const script = document.createElement('script');
      script.src = 'https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.12.0/sql-wasm.js';
      script.onload = resolve;
      script.onerror = () => reject(new Error('sql-wasm.js の読み込みに失敗'));
      document.head.appendChild(script);
    });
  }
  SQL = await window.initSqlJs({
    locateFile: (file) =>
      `https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.12.0/${file}`
  });
  return SQL;
}

locateFile コールバックで WASM バイナリの取得先をCDNに向けています。ローカルにファイルを配置する必要はありません。

データベースの読み込み

ファイルを ArrayBuffer として読み込み、Uint8Array に変換して Database コンストラクタに渡します。

async function loadDatabase(buffer, fileName, fileSize) {
  const sqlEngine = await initSqlEngine();
  if (db) db.close();  // 既存DBを閉じる
  db = new sqlEngine.Database(new Uint8Array(buffer));
  currentFileName = fileName || 'database.sqlite';
  currentFileSize = fileSize || buffer.byteLength;
  refreshTableList();
  updateDbInfo();
}

sql.jsはインメモリDBとして動作するため、元のファイルは読み込み後に不要です。編集結果はメモリ上のDBに反映され、db.export() でバイナリ出力できます。


3. DB情報パネル — PRAGMAによるメタデータ取得

読み込んだファイルの詳細情報を左サイドバーに表示します。SQLiteの PRAGMA 文でメタデータを取得します。

function updateDbInfo() {
  if (!db) return;
  // ファイル情報
  document.getElementById('db-info-file').textContent = currentFileName;
  document.getElementById('db-info-size').textContent = formatBytes(currentFileSize);

  // テーブル一覧取得
  const tables = db.exec(
    "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"
  );
  const tableNames = tables.length > 0 ? tables[0].values.map(v => v[0]) : [];
  document.getElementById('db-info-tables').textContent = tableNames.length;

  // 全テーブルの総レコード数
  let totalRows = 0;
  tableNames.forEach(t => {
    try {
      const r = db.exec('SELECT COUNT(*) FROM "' + t + '"');
      if (r.length > 0) totalRows += r[0].values[0][0];
    } catch(e) {}
  });
  document.getElementById('db-info-records').textContent = totalRows.toLocaleString();

  // PRAGMA でDB設定を取得
  const ps = db.exec('PRAGMA page_size');     // ページサイズ (bytes)
  const enc = db.exec('PRAGMA encoding');      // エンコーディング (UTF-8等)
}

表示される情報:

項目取得方法
FileFileReader から取得northwind.sqlite
SizeFile.size284.0 KB
Tablessqlite_master クエリ13
Records各テーブルの COUNT(*) 合算3,308
Page SizePRAGMA page_size1024 bytes
EncodingPRAGMA encodingUTF-8

PRAGMA はSQLite固有のメタコマンドで、通常のSQLでは取得できないDB内部設定にアクセスできます。page_size はSQLiteがディスクI/Oに使うブロックサイズ、encoding はテキストのエンコーディング方式です。


4. テーブル表示とスキーマ解析

テーブルデータ表示

テーブル名をクリックすると SELECT * FROM "tableName" を実行し、結果をHTMLテーブルとして描画します。

function renderTable(tableName) {
  const limit = parseInt(document.getElementById('row-limit')?.value || '300');
  const limitClause = limit > 0 ? ' LIMIT ' + limit : '';
  const res = db.exec('SELECT * FROM "' + tableName + '"' + limitClause + ';');

  if (res.length > 0) {
    buildHtmlTable(dataTable, res[0].columns, res[0].values);
  }
}

行数制御として 100 / 300 / 1000 / ALL のセレクトボックスを用意しました。大きなテーブル(数万行)で ALL を選ぶとブラウザが重くなるため、デフォルトは300件にしています。

スキーマ表示

PRAGMA table_info でカラム定義を取得し、sqlite_master から CREATE TABLE 文を表示します。

function renderSchema(tableName) {
  // カラム定義: cid, name, type, notnull, default, pk
  const info = db.exec('PRAGMA table_info("' + tableName + '")');
  if (info.length > 0) {
    buildHtmlTable(schemaTable,
      ['cid', 'name', 'type', 'notnull', 'default', 'pk'],
      info[0].values
    );
  }

  // CREATE TABLE 文
  const sql = db.exec(
    "SELECT sql FROM sqlite_master WHERE name='" + tableName + "'"
  );
  if (sql.length > 0) {
    createSqlPre.textContent = sql[0].values[0][0];
  }
}

PRAGMA table_info が返すカラム:

フィールド説明
cidカラムID(0始まり)
nameカラム名
typeデータ型(INTEGER, TEXT, REAL等)
notnullNOT NULL制約(0/1)
dflt_valueデフォルト値
pkPRIMARY KEY(0=非PK, 1以上=PK順序)

テーブルデータを見るだけでは分からない型情報やPK構成が一目で把握できます。


5. SQLエディタ

テキストエリアに任意のSQLを入力し、Ctrl+Enter で実行します。

function runQuery() {
  const sql = sqlInput.value.trim();
  if (!sql || !db) return;

  const startTime = performance.now();
  try {
    const res = db.exec(sql);
    const duration = (performance.now() - startTime).toFixed(2);
    execTime.textContent = duration + ' ms';

    if (res.length > 0) {
      buildHtmlTable(sqlResultTable, res[0].columns, res[0].values);
    } else {
      // INSERT/UPDATE/DELETE等、返却行なしのクエリ
      buildHtmlTable(sqlResultTable, ['Result'],
        [['クエリが正常に実行されました(返却行なし)']]);
    }
    refreshTableList();  // CREATE/DROP TABLE後にリスト更新
  } catch (err) {
    sqlError.textContent = err.message;
    sqlError.classList.remove('hidden');
  }
}

db.exec()SELECT だけでなく INSERT, UPDATE, DELETE, CREATE TABLE, DROP TABLE もそのまま実行できます。変更はインメモリDBに反映され、「DBを保存」ボタンでバイナリエクスポートすることで永続化します。

実行時間を performance.now() で計測し、UIに表示しています。sql.jsはWASM実行なのでJavaScript実装のSQLパーサと比べて高速です。


6. エクスポート機能

CSV出力

アクティブなテーブルのデータをCSVとしてダウンロードします。

function exportCsv() {
  if (!activeTableData) return;
  const { columns, values } = activeTableData;
  let csv = columns.map(c => '"' + c.replace(/"/g, '""') + '"').join(',') + '\n';
  values.forEach(row => {
    csv += row.map(v => '"' + String(v ?? '').replace(/"/g, '""') + '"').join(',') + '\n';
  });
  // Blob生成→ダウンロード
}

ダブルクォートのエスケープ処理(""")を入れて、カンマやダブルクォートを含むデータでも正しいCSVを出力します。

JSON出力

同じデータをオブジェクト配列のJSONとして出力します。

function exportJson() {
  const rows = values.map(row => {
    const obj = {};
    columns.forEach((col, i) => { obj[col] = row[i]; });
    return obj;
  });
  const json = JSON.stringify(rows, null, 2);
  // Blob生成→ダウンロード
}

API開発者やデータ分析でJSONが必要なケースに対応。整形済み(indent: 2)で出力します。

DBバイナリ保存

db.export() でSQLiteバイナリ全体を Uint8Array としてエクスポートし、.sqlite ファイルとしてダウンロードします。SQLエディタでINSERT/UPDATEした変更も反映された状態で保存されます。


7. 小さな改善の積み重ね

セルクリックコピー

テーブルのセルをクリックすると、その値がクリップボードにコピーされます。長いテキストやNULL値の確認に便利です。

document.addEventListener('click', function(e) {
  const td = e.target.closest('.data-table td');
  if (!td) return;
  navigator.clipboard.writeText(td.textContent).then(function() {
    td.classList.add('copied');
    setTimeout(function() { td.classList.remove('copied'); }, 800);
  });
});

::after 擬似要素で ✓ マークを表示し、800ms後に消えます。イベント委譲(document にリスナーを1つ)で動的生成されたテーブルにも対応しています。

テーブル検索フィルタ

テーブル数が多いDBでは一覧からの探索が困難です。インクリメンタルサーチで絞り込みます。

document.getElementById('table-search')?.addEventListener('input', function(e) {
  const query = e.target.value.toLowerCase();
  document.querySelectorAll('.table-item').forEach(function(el) {
    el.style.display = el.textContent.toLowerCase().includes(query) ? '' : 'none';
  });
});

input イベントで入力のたびにフィルタが走るため、入力途中でも即座に結果が反映されます。シンプルな includes マッチですが、テーブル名検索にはこれで十分です。

NULL値の視認性

NULL セルにはイタリック+暗い色を適用し、空文字列との区別を明確にしています。

if (val === null) td.classList.add('null-val');
.null-val { color: #444; font-style: italic; }

データベースでは NULL と空文字列 '' は意味が異なるため、視覚的な区別は重要です。


8. サンプルDB生成

ファイルを持っていなくてもツールを試せるように、サンプルDB生成機能を搭載しています。

async function createSampleDb() {
  const sqlEngine = await initSqlEngine();
  db = new sqlEngine.Database();  // 空のインメモリDB

  db.run(`
    CREATE TABLE users (
      id INTEGER PRIMARY KEY,
      name TEXT,
      role TEXT,
      created_at DATETIME DEFAULT CURRENT_TIMESTAMP
    );
    INSERT INTO users (name, role) VALUES
      ('lain', 'Admin'),
      ('Alice', 'Developer'),
      ('Bob', 'Designer');

    CREATE TABLE projects (
      id INTEGER PRIMARY KEY,
      title TEXT,
      status TEXT,
      user_id INTEGER
    );
    INSERT INTO projects (title, status, user_id) VALUES
      ('Text Extractor v1', 'Completed', 1),
      ('SQLite WASM Viewer', 'In Progress', 1),
      ('3D Model Viewer', 'Completed', 2);
  `);
}

new sqlEngine.Database() で空のインメモリDBを生成し、db.run() で複数のSQL文を一括実行します。ファイルの読み込みと同じコードパスでテーブル一覧やDB情報が更新されるため、特別な分岐は不要です。


9. ローカルに必要なファイル

ゼロ

  • sql.js → cdnjs CDN(sql-wasm.js
  • SQLite WASM バイナリ → cdnjs CDN(sql-wasm.wasmlocateFile で指定)

/public にファイルを置く必要は一切ありません。Astroページ1ファイルで完結します。


10. まとめ

  • sql.js: CDNからの遅延ロードでSQLiteエンジンを導入。安定性が高く、db.exec() 1つでSELECT/INSERT/UPDATE/DELETE/DDLすべて対応
  • DB情報パネル: PRAGMA page_size, PRAGMA encoding, sqlite_master クエリでファイルの全体像を即座に把握
  • スキーマ解析: PRAGMA table_info でカラム定義、sqlite_master から CREATE TABLE SQL を表示
  • エクスポート: CSV(ダブルクォートエスケープ対応)、JSON(整形済み)、DBバイナリの3形式
  • 操作の快適性: セルクリックコピー、テーブル検索、行数制御でストレスのないデータ探索

「差別化が難しい」と着手前は思っていましたが、DB情報パネルとスキーマ表示を入れたことで「ファイルの中身をすばやく把握する」ツールとしての軸ができました。既存の汎用ビューアがカバーしきれないニッチな体験は、UIの統一感と細かい操作性の積み重ねで作られます。