[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情報パネルなど「ファイルの中身をすばやく把握する」機能に寄せた構成で実装しました。
スクリーンショット
テーブルデータ表示
スキーマ表示
SQLエディタ
動画(GIF)
Sample data
テストデータにはNorthwind Sample Databaseを使用しています。
Northwind SQLite3 — GitHub
A SQLite3 version of the Microsoft Northwind sample database
github.com1. 全体アーキテクチャ
左サイドバー + 右メインエリアの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等)
}
表示される情報:
| 項目 | 取得方法 | 例 |
|---|---|---|
| File | FileReader から取得 | northwind.sqlite |
| Size | File.size | 284.0 KB |
| Tables | sqlite_master クエリ | 13 |
| Records | 各テーブルの COUNT(*) 合算 | 3,308 |
| Page Size | PRAGMA page_size | 1024 bytes |
| Encoding | PRAGMA encoding | UTF-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等) |
| notnull | NOT NULL制約(0/1) |
| dflt_value | デフォルト値 |
| pk | PRIMARY 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.wasm、locateFileで指定)
/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 TABLESQL を表示 - エクスポート: CSV(ダブルクォートエスケープ対応)、JSON(整形済み)、DBバイナリの3形式
- 操作の快適性: セルクリックコピー、テーブル検索、行数制御でストレスのないデータ探索
「差別化が難しい」と着手前は思っていましたが、DB情報パネルとスキーマ表示を入れたことで「ファイルの中身をすばやく把握する」ツールとしての軸ができました。既存の汎用ビューアがカバーしきれないニッチな体験は、UIの統一感と細かい操作性の積み重ねで作られます。