🍣

【決定版】TypeScriptのSQLタグ付きテンプレートリテラルで10種類のSQLiteドライバー全部試す

に公開

はじめに

TypeScriptのSQLタグ付きテンプレートリテラルを数行で書けて超絶便利だったので、10種類のSQLiteドライバーで試した結果を紹介します。

SQLタグ付きテンプレートリテラル

今回の記事で何回も登場しますが、先にSQLタグ付きテンプレートリテラルだけ紹介します。

type SqlBinding = null | number | string;

type SqlQuery = {
  query: string;
  bindings: SqlBinding[];
};

function sql(strings: TemplateStringsArray, ...bindings: SqlBinding[]): SqlQuery {
  const query = strings.map((str, i) => str + (i < bindings.length ? "?" : "")).join("").trim();
  return { query, bindings };
}

以下のコードを実行するとqueryとbindingsのオブジェクトが表示されます。

const content = "Hello, World!";
const insertPost = sql`INSERT INTO posts (content) VALUES (${content})`;
console.log(insertPost);
{
  query: 'INSERT INTO posts (content) VALUES (?)',
  bindings: [ 'Hello, World!' ]
}

型と関数をexportすればモジュールして使うこともできます。

SQLインジェクションが発生しそうで不安になりますが、クエリがクエスチョンマークのプレースホルダーに変換され、変数がバインドに分離するため安全に実行できます。

このSQLタグ付きテンプレートリテラルはクエスチョンマークのプレースホルダーが実装されていて、クエリにバインドできるデータベースであればSQLiteでなくても動きます。

ここまでの解説を理解できてSQLite以外の各種リレーショナルデータベースでSQLタグ付きテンプレートリテラルを使うのであれば以降の解説は読まなくても大丈夫です。

モチベーション

ではなぜ今回10種類のSQLiteドライバーで試した記事を書いたのかというと、私は2022年に

  • かんたんDenoでSQLite
  • [WebAssembly]かんたんindex.htmlでSQLite(sql.js, cdnjs)
  • [JavaScript]Node.jsとDenoとBunでSQLite書き比べ(Pythonもあるよ)

という3つの初心者向け記事を書いていたのですが、初心者向けなのでかなり簡略化した書き方をしていたり、クエリをループしていたり、無駄な情報が多かったり、3年経過して色々と古くなっていったこともあったので、改訂版としてすべて書き直した記事を公開することにしました。

ChatGPTに質問した際、メジャーなパッケージであれば非常に良い回答をしてくれますが、マニアックな動かし方や新し目のパッケージだと全く動かない回答をしてくるので、まだまだこういったニッチな記事は需要がある気がしますし、何より私自身が困るので。

解説について

使用したコマンドおよびバージョンは以下のとおりです。

$ node -v
v22.18.0
$ npm -v
10.9.3
$ deno --version
deno 2.5.0 (stable, release, x86_64-unknown-linux-gnu)
v8 14.0.365.4-rusty
typescript 5.9.2
$ bun -v
1.2.21
$ npx wrangler -v

 ⛅️ wrangler 4.35.0
───────────────────
$ python -V
Python 3.12.11

コードはES2020 + ES Modules + Top-Level Awaitの言語仕様で書いています。

今回、Type Strippingがデフォルトで有効なNode.js v22.18.0を使用しているため、コードを実行する時はNode.jsのバージョンに注意してください。

https://nodejs.org/en/blog/release/v22.18.0

10種類のSQLiteドライバーで試した後のpackage.jsonファイルは以下のとおりです。

package.json
{
  "dependencies": {
    "@sqlite.org/sqlite-wasm": "^3.50.4-build1",
    "@types/better-sqlite3": "^7.6.13",
    "@types/bun": "^1.2.21",
    "@types/sql.js": "^1.4.9",
    "better-sqlite3": "^12.2.0",
    "sql.js": "^1.13.0",
    "sqlite3": "^5.1.7"
  },
  "type": "module",
  "engines": {
    "node": "^22.18.0 || ^24.0.0"
  }
}

解説は主に以下の流れで行っていきます。

  1. コードの紹介
  2. コードを実行するコマンド
  3. 簡単なSQLiteドライバーの解説と他SQLiteドライバーとの違い(あれば補足情報を詳しく)
  4. クエリ実行時に使用したメソッドの情報(型定義の詳細とオーバーロードについては省略しています)

以下の流れでSQLを実行しています。

  1. postsという名前のテーブルを作成
  2. 続けてバインドせず、postsテーブルに「Hello, World!」というcontentを追加
  3. バインドして、postsテーブルに「Hello, World!」というcontentを2回追加
  4. postsテーブル内のすべてのデータを取得

ほぼ全てのコードでSQLiteのlineモード(sqlite3 -line)に似た表示ができるよう、以下のlineMode()関数を使用しています。

function lineMode(data: Record<string, string>[]): string {
  const keyWidth = Math.max(...data.flatMap((obj) => Object.keys(obj).map((k) => k.length)));
  const lines: string[] = [];
  for (const obj of data) {
    for (const [k, v] of Object.entries(obj)) {
      lines.push(`${k.padStart(keyWidth)} = ${v}`);
    }
    lines.push("");
  }
  return lines.join("\n");
}

コードを実行すると以下のように表示されます。

        id = 1
   content = Hello, World!
created_at = 2025-09-12 07:00:00
updated_at = 2025-09-12 07:00:00

        id = 2
   content = Hello, World!
created_at = 2025-09-12 07:00:00
updated_at = 2025-09-12 07:00:00

        id = 3
   content = Hello, World!
created_at = 2025-09-12 07:00:00
updated_at = 2025-09-12 07:00:00

また、以下の内容については解説しません。

  • Node.jsとnpmのインストール方法
  • Cloudflare Workersの使い方
  • VS Codeとtscの型に関する設定
  • package.json以外の型定義ファイルについて(bun-typesnpx wrangler typesコマンドで出力されるworker-configuration.d.tsファイルなど)

node-sqlite3

https://www.npmjs.com/package/sqlite3

node-sqlite3-gen.ts
import sqlite3 from "sqlite3";

type SqlBinding = null | number | string;

type SqlQuery = {
  query: string;
  bindings: SqlBinding[];
};

function sql(strings: TemplateStringsArray, ...bindings: SqlBinding[]): SqlQuery {
  const query = strings.map((str, i) => str + (i < bindings.length ? "?" : "")).join("").trim();
  return { query, bindings };
}

function lineMode(data: Record<string, string>[]): string {
  const keyWidth = Math.max(...data.flatMap((obj) => Object.keys(obj).map((k) => k.length)));
  const lines: string[] = [];
  for (const obj of data) {
    for (const [k, v] of Object.entries(obj)) {
      lines.push(`${k.padStart(keyWidth)} = ${v}`);
    }
    lines.push("");
  }
  return lines.join("\n");
}

const db = new sqlite3.Database("sqlite.db");

db.serialize(() => {
  const createTable = sql`
    CREATE TABLE IF NOT EXISTS posts (
      id         INTEGER PRIMARY KEY AUTOINCREMENT,
      content    TEXT,
      created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
      updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    );
    INSERT INTO posts (content) VALUES ('Hello, World!');
  `;
  db.exec(createTable.query);

  const content = "Hello, World!";
  const insertPost = sql`INSERT INTO posts (content) VALUES (${content})`;
  db.run(insertPost.query, insertPost.bindings);
  db.run(insertPost.query, insertPost.bindings);

  const selectPosts = sql`SELECT * FROM posts`;
  db.all(selectPosts.query, (_err, rows) => {
    const output = lineMode(rows as Record<string, string>[]);
    console.log(output);
    db.close();
  });
});
node node-sqlite3-gen.ts
  • Node.js用パッケージの中では最も歴史がある。
  • コールバックが多用されており、並列実行によるエラーが発生することがある。
  • Database.serialize()のコールバック内で処理を記述すれば問題を防げる。

クエリ実行時に使用したメソッドは以下のとおりです。

Database.exec(sql: string, callback?: (this: sqlite3.Statement, err: Error | null) => void): sqlite3.Database
Database.run(sql: string, params: any, callback?: (this: sqlite3.RunResult, err: Error | null) => void): sqlite3.Database
Database.all<unknown>(sql: string, callback?: ((this: sqlite3.Statement, err: Error | null, rows: unknown[]) => void) | undefined): sqlite3.Database

better-sqlite3

https://www.npmjs.com/package/better-sqlite3

better-sqlite3-gen.ts
import Database from "better-sqlite3";

type SqlBinding = null | number | string;

type SqlQuery = {
  query: string;
  bindings: SqlBinding[];
};

function sql(strings: TemplateStringsArray, ...bindings: SqlBinding[]): SqlQuery {
  const query = strings.map((str, i) => str + (i < bindings.length ? "?" : "")).join("").trim();
  return { query, bindings };
}

function lineMode(data: Record<string, string>[]): string {
  const keyWidth = Math.max(...data.flatMap((obj) => Object.keys(obj).map((k) => k.length)));
  const lines: string[] = [];
  for (const obj of data) {
    for (const [k, v] of Object.entries(obj)) {
      lines.push(`${k.padStart(keyWidth)} = ${v}`);
    }
    lines.push("");
  }
  return lines.join("\n");
}

const db = new Database("sqlite.db");

const createTable = sql`
  CREATE TABLE IF NOT EXISTS posts (
    id         INTEGER PRIMARY KEY AUTOINCREMENT,
    content    TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  );
  INSERT INTO posts (content) VALUES ('Hello, World!');
`;
db.exec(createTable.query);

const content = "Hello, World!";
const insertPost = sql`INSERT INTO posts (content) VALUES (${content})`;
db.prepare(insertPost.query).run(insertPost.bindings);
db.prepare(insertPost.query).run(insertPost.bindings);

const selectPosts = sql`SELECT * FROM posts`;
const results = db.prepare(selectPosts.query).all();

const output = lineMode(results as Record<string, string>[]);
console.log(output);
db.close();
node better-sqlite3-gen.ts
  • node-sqlite3の使いづらさを解消し、速いNode.js用パッケージ。
  • Statementクラスが必要で、SQL文をバインドするにはDatabase.prepare()から始める。
  • Statement.all()でDB結果を配列で取得できる。

クエリ実行時に使用したメソッドは以下のとおりです。

BetterSqlite3.Database.exec(source: string): BetterSqlite3.Database
BetterSqlite3.Database.prepare<unknown[], unknown>(source: string): BetterSqlite3.Statement<unknown[], unknown>
BetterSqlite3.Statement<unknown[], unknown>.run(...params: unknown[]): Database.RunResult
BetterSqlite3.Statement<unknown[], unknown>.all(...params: unknown[]): unknown[]

node-sqlite

https://nodejs.org/api/sqlite.html

node-sqlite-gen.ts
import { DatabaseSync } from "node:sqlite";

type SqlBinding = null | number | string;

type SqlQuery = {
  query: string;
  bindings: SqlBinding[];
};

function sql(strings: TemplateStringsArray, ...bindings: SqlBinding[]): SqlQuery {
  const query = strings.map((str, i) => str + (i < bindings.length ? "?" : "")).join("").trim();
  return { query, bindings };
}

function lineMode(data: Record<string, string>[]): string {
  const keyWidth = Math.max(...data.flatMap((obj) => Object.keys(obj).map((k) => k.length)));
  const lines: string[] = [];
  for (const obj of data) {
    for (const [k, v] of Object.entries(obj)) {
      lines.push(`${k.padStart(keyWidth)} = ${v}`);
    }
    lines.push("");
  }
  return lines.join("\n");
}

const db = new DatabaseSync("sqlite.db");

const createTable = sql`
  CREATE TABLE IF NOT EXISTS posts (
    id         INTEGER PRIMARY KEY AUTOINCREMENT,
    content    TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  );
  INSERT INTO posts (content) VALUES ('Hello, World!');
`;
db.exec(createTable.query);

const content = "Hello, World!";
const insertPost = sql`INSERT INTO posts (content) VALUES (${content})`;
db.prepare(insertPost.query).run(...insertPost.bindings);
db.prepare(insertPost.query).run(...insertPost.bindings);

const selectPosts = sql`SELECT * FROM posts`;
const results = db.prepare(selectPosts.query).all();

const output = lineMode(results as Record<string, string>[]);
console.log(output);
db.close();
node --no-warnings node-sqlite-gen.ts
  • Node.jsのネイティブ実装モジュールで、Node.js v22.5.0から導入された。
  • 同期APIのみ提供されている。
  • 従来のNode.js用パッケージと異なり、インストールせずに使用可能。

node:sqliteはDeno v2.2以降もサポートしており、全く同じコードを実行できます。

https://docs.deno.com/api/node/sqlite/

curl -fsSL https://deno.land/install.sh | sh -s -- -y
export PATH="$HOME/.deno/bin:$PATH"
deno -RW node-sqlite-gen.ts

https://x.com/boshen_c/status/1963776912335270390

Nova EngineとOxcを基盤とする、Rustで一から構築されたJavaScript/TypeScriptランタイム、Andromedaでもimport文を削除することで同じ同期APIを実行できます。

https://tryandromeda.dev/docs/api/sqlite

--- a/node-sqlite-gen.ts        2025-09-12 07:00:00.000000000 +0000
+++ b/node-sqlite-gen.ts        2025-09-12 07:00:00.000000000 +0000
@@ -1,2 +0,0 @@
-import { DatabaseSync } from "node:sqlite";
-
curl -fsSL https://tryandromeda.dev/install.sh | bash
export PATH="$HOME/.local/bin:$PATH"
andromeda node-sqlite-gen.ts

ただAndromeda 0.1.0-draft36ではオブジェクトのキーがバラバラになるバグのような表示になります。

created_at = 2025-09-12 07:00:00
updated_at = 2025-09-12 07:00:00
        id = 1
   content = Hello, World!

        id = 2
created_at = 2025-09-12 07:00:00
updated_at = 2025-09-12 07:00:00
   content = Hello, World!

        id = 3
updated_at = 2025-09-12 07:00:00
   content = Hello, World!
created_at = 2025-09-12 07:00:00

クエリ実行時に使用したメソッドは以下のとおりです。

DatabaseSync.exec(sql: string): void
DatabaseSync.prepare(sql: string): StatementSync
StatementSync.run(...anonymousParameters: SQLInputValue[]): StatementResultingChanges
StatementSync.all(...anonymousParameters: SQLInputValue[]): Record<string, SQLOutputValue>[]

node-sql-js

https://www.npmjs.com/package/sql.js

node-sql-js-gen.ts
import initSqlJs from "sql.js";

type SqlBinding = null | number | string;

type SqlQuery = {
  query: string;
  bindings: SqlBinding[];
};

function sql(strings: TemplateStringsArray, ...bindings: SqlBinding[]): SqlQuery {
  const query = strings.map((str, i) => str + (i < bindings.length ? "?" : "")).join("").trim();
  return { query, bindings };
}

function lineMode(data: Record<string, string>[]): string {
  const keyWidth = Math.max(...data.flatMap((obj) => Object.keys(obj).map((k) => k.length)));
  const lines: string[] = [];
  for (const obj of data) {
    for (const [k, v] of Object.entries(obj)) {
      lines.push(`${k.padStart(keyWidth)} = ${v}`);
    }
    lines.push("");
  }
  return lines.join("\n");
}

function convertSqlJsResult(queryResults: initSqlJs.QueryExecResult[]): Record<string, initSqlJs.SqlValue>[] {
  const rows: Record<string, initSqlJs.SqlValue>[] = [];
  for (const { columns, values } of queryResults) {
    for (const row of values) {
      rows.push(Object.fromEntries(columns.map((col, i) => [col, row[i]])));
    }
  }
  return rows;
}

const SQL = await initSqlJs({ locateFile: (file) => `node_modules/sql.js/dist/${file}` });
const db = new SQL.Database();

const createTable = sql`
  CREATE TABLE IF NOT EXISTS posts (
    id         INTEGER PRIMARY KEY AUTOINCREMENT,
    content    TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  );
  INSERT INTO posts (content) VALUES ('Hello, World!');
`;
db.exec(createTable.query);

const content = "Hello, World!";
const insertPost = sql`INSERT INTO posts (content) VALUES (${content})`;
db.run(insertPost.query, insertPost.bindings);
db.run(insertPost.query, insertPost.bindings);

const selectPosts = sql`SELECT * FROM posts`;
const results = db.exec(selectPosts.query);
const resultRows = convertSqlJsResult(results);

const output = lineMode(resultRows as Record<string, string>[]);
console.log(output);
db.close();
node-sql-js-gen.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>sql.js</title>
    <meta name="description" content="Zenn">
  </head>
  <body>
    <pre id="sqlJs"></pre>
    <script src="https://cdn.jsdelivr.net/npm/sql.js@1.13.0/dist/sql-wasm.min.js"></script>
    <script type="module">
      function sql(strings, ...bindings) {
        const query = strings.map((str, i) => str + (i < bindings.length ? "?" : "")).join("").trim();
        return { query, bindings };
      }

      function lineMode(data) {
        const keyWidth = Math.max(...data.flatMap((obj) => Object.keys(obj).map((k) => k.length)));
        const lines = [];
        for (const obj of data) {
          for (const [k, v] of Object.entries(obj)) {
            lines.push(`${k.padStart(keyWidth)} = ${v}`);
          }
          lines.push("");
        }
        return lines.join("\n");
      }

      function convertSqlJsResult(queryResults) {
        const rows = [];
        for (const { columns, values } of queryResults) {
          for (const row of values) {
            rows.push(Object.fromEntries(columns.map((col, i) => [col, row[i]])));
          }
        }
        return rows;
      }

      const SQL = await initSqlJs({
        locateFile: (file) => `https://cdn.jsdelivr.net/npm/sql.js@1.13.0/dist/${file}`,
      });
      const db = new SQL.Database();

      const createTable = sql`
        CREATE TABLE IF NOT EXISTS posts (
          id         INTEGER PRIMARY KEY AUTOINCREMENT,
          content    TEXT,
          created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
          updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        );
        INSERT INTO posts (content) VALUES ('Hello, World!');
      `;
      db.exec(createTable.query);

      const content = "Hello, World!";
      const insertPost = sql`INSERT INTO posts (content) VALUES (${content})`;
      db.run(insertPost.query, insertPost.bindings);
      db.run(insertPost.query, insertPost.bindings);

      const selectPosts = sql`SELECT * FROM posts`;
      const results = db.exec(selectPosts.query);
      const resultRows = convertSqlJsResult(results);

      const output = lineMode(resultRows);
      document.getElementById("sqlJs").innerText = output;
      db.close();
    </script>
  </body>
</html>
node node-sql-js-gen.ts

node-sqlite-wasm-gen.htmlは空のindex.htmlにコピペして、index.htmlをWebブラウザで開けばNode.jsやnpmをインストールしなくても使えます。

  • Webブラウザ上でSQLデータベースを作成し、クエリを実行できるnpmパッケージ。WebAssemblyでSQLiteをコンパイルしている。
  • 仮想データベースを使用し、変更は保持されないが、SQLiteファイルのインポートやエクスポートが可能。
  • ES Modules形式で配布していないため、Webブラウザ上で動かす際、scriptタグで読み込む必要がある。

クエリ実行時に使用したメソッドは以下のとおりです。

Database.exec(sql: string, params?: initSqlJs.BindParams): initSqlJs.QueryExecResult[]
Database.run(sql: string, params?: initSqlJs.BindParams): Database

node-sqlite-wasm

https://www.npmjs.com/package/@sqlite.org/sqlite-wasm

node-sqlite-wasm-gen.ts
import sqlite3InitModule from "@sqlite.org/sqlite-wasm";

type SqlBinding = null | number | string;

type SqlQuery = {
  query: string;
  bindings: SqlBinding[];
};

function sql(strings: TemplateStringsArray, ...bindings: SqlBinding[]): SqlQuery {
  const query = strings.map((str, i) => str + (i < bindings.length ? "?" : "")).join("").trim();
  return { query, bindings };
}

function lineMode(data: Record<string, string>[]): string {
  const keyWidth = Math.max(...data.flatMap((obj) => Object.keys(obj).map((k) => k.length)));
  const lines: string[] = [];
  for (const obj of data) {
    for (const [k, v] of Object.entries(obj)) {
      lines.push(`${k.padStart(keyWidth)} = ${v}`);
    }
    lines.push("");
  }
  return lines.join("\n");
}

const sqlite3 = await sqlite3InitModule();
const db = new sqlite3.oo1.DB();

const createTable = sql`
  CREATE TABLE IF NOT EXISTS posts (
    id         INTEGER PRIMARY KEY AUTOINCREMENT,
    content    TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  );
  INSERT INTO posts (content) VALUES ('Hello, World!');
`;
db.exec(createTable.query);

const content = "Hello, World!";
const insertPost = sql`INSERT INTO posts (content) VALUES (${content})`;
db.exec({ sql: insertPost.query, bind: insertPost.bindings });
db.exec({ sql: insertPost.query, bind: insertPost.bindings });

const selectPosts = sql`SELECT * FROM posts`;
const resultRows: Record<string, string>[] = [];
db.exec({ sql: selectPosts.query, rowMode: "object", resultRows });

const output = lineMode(resultRows);
console.log(output);
db.close();
node-sqlite-wasm-gen.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>SQLite Wasm</title>
    <meta name="description" content="Zenn">
  </head>
  <body>
    <pre id="sqliteWasm"></pre>
    <script type="module">
      import sqlite3InitModule from "https://cdn.jsdelivr.net/npm/@sqlite.org/sqlite-wasm@3.50.4-build1/sqlite-wasm/jswasm/sqlite3.min.mjs";

      function sql(strings, ...bindings) {
        const query = strings.map((str, i) => str + (i < bindings.length ? "?" : "")).join("").trim();
        return { query, bindings };
      }

      function lineMode(data) {
        const keyWidth = Math.max(...data.flatMap((obj) => Object.keys(obj).map((k) => k.length)));
        const lines = [];
        for (const obj of data) {
          for (const [k, v] of Object.entries(obj)) {
            lines.push(`${k.padStart(keyWidth)} = ${v}`);
          }
          lines.push("");
        }
        return lines.join("\n");
      }

      const sqlite3 = await sqlite3InitModule();
      const db = new sqlite3.oo1.DB();

      const createTable = sql`
        CREATE TABLE IF NOT EXISTS posts (
          id         INTEGER PRIMARY KEY AUTOINCREMENT,
          content    TEXT,
          created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
          updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        );
        INSERT INTO posts (content) VALUES ('Hello, World!');
      `;
      db.exec(createTable.query);

      const content = "Hello, World!";
      const insertPost = sql`INSERT INTO posts (content) VALUES (${content})`;
      db.exec({ sql: insertPost.query, bind: insertPost.bindings });
      db.exec({ sql: insertPost.query, bind: insertPost.bindings });

      const selectPosts = sql`SELECT * FROM posts`;
      const resultRows = [];
      db.exec({ sql: selectPosts.query, rowMode: "object", resultRows });

      const output = lineMode(resultRows);
      document.getElementById("sqliteWasm").innerText = output;
      db.close();
    </script>
  </body>
</html>
node node-sqlite-wasm-gen.ts

node-sqlite-wasm-gen.htmlは空のindex.htmlにコピペして、index.htmlをWebブラウザで開けばNode.jsやnpmをインストールしなくても使えます。

  • sql.jsと異なり、SQLiteの公式WebAssembly版。
  • オフラインサポートや永続的なストレージ機能(localStorageやOPFS)を提供。
  • 現在、Node.jsでは永続化がなくインメモリデータベースのみサポートされている。

Webブラウザ上で動かす際、sqlite3.min.mjsをES Modulesとして読み込むだけでなく、sqlite3.min.jsscriptタグで読み込むこともできます。

--- a/node-sqlite-wasm-gen.html 2025-09-12 07:00:00.000000000 +0000
+++ b/node-sqlite-wasm-gen.html 2025-09-12 07:00:00.000000000 +0000
@@ -10,0 +11,3 @@
+    <script
+      src="https://cdn.jsdelivr.net/npm/@sqlite.org/sqlite-wasm@3.50.4-build1/sqlite-wasm/jswasm/sqlite3.min.js"
+    ></script>
@@ -12,2 +14,0 @@
-      import sqlite3InitModule from "https://cdn.jsdelivr.net/npm/@sqlite.org/sqlite-wasm@3.50.4-build1/sqlite-wasm/jswasm/sqlite3.min.mjs";
-

また、new sqlite3.oo1.JsStorageDb("local")でlocalStorageに永続化できます。

--- a/node-sqlite-wasm-gen.html 2025-09-12 07:00:00.000000000 +0000
+++ b/node-sqlite-wasm-gen.html 2025-09-12 07:00:00.000000000 +0000
@@ -32 +32 @@
-      const db = new sqlite3.oo1.DB();
+      const db = new sqlite3.oo1.JsStorageDb("local");

クエリ実行時に使用したメソッドは以下のとおりです。

Database.exec(sql: FlexibleString, opts?: (ExecBaseOptions & ExecRowModeArrayOptions & ExecReturnThisOptions) & {
  sql?: undefined;
}): Database
Database.exec(opts: (ExecBaseOptions & ExecRowModeArrayOptions & ExecReturnThisOptions) & {
  sql: FlexibleString;
}): Database
Database.exec(opts: ExecBaseOptions & ExecRowModeObjectOptions & ExecReturnThisOptions & {
  sql: FlexibleString;
}): Database

deno-sqlite

https://deno.land/x/sqlite

deno-sqlite-gen.ts
import { DB } from "https://deno.land/x/sqlite@v3.9.1/mod.ts";

type SqlBinding = null | number | string;

type SqlQuery = {
  query: string;
  bindings: SqlBinding[];
};

function sql(strings: TemplateStringsArray, ...bindings: SqlBinding[]): SqlQuery {
  const query = strings.map((str, i) => str + (i < bindings.length ? "?" : "")).join("").trim();
  return { query, bindings };
}

function lineMode(data: Record<string, string>[]): string {
  const keyWidth = Math.max(...data.flatMap((obj) => Object.keys(obj).map((k) => k.length)));
  const lines: string[] = [];
  for (const obj of data) {
    for (const [k, v] of Object.entries(obj)) {
      lines.push(`${k.padStart(keyWidth)} = ${v}`);
    }
    lines.push("");
  }
  return lines.join("\n");
}

const db = new DB("sqlite.db");

const createTable = sql`
  CREATE TABLE IF NOT EXISTS posts (
    id         INTEGER PRIMARY KEY AUTOINCREMENT,
    content    TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  );
  INSERT INTO posts (content) VALUES ('Hello, World!');
`;
db.execute(createTable.query);

const content = "Hello, World!";
const insertPost = sql`INSERT INTO posts (content) VALUES (${content})`;
db.query(insertPost.query, insertPost.bindings);
db.query(insertPost.query, insertPost.bindings);

const selectPosts = sql`SELECT * FROM posts`;
const results = db.queryEntries(selectPosts.query);

const output = lineMode(results as Record<string, string>[]);
console.log(output);
db.close();
curl -fsSL https://deno.land/install.sh | sh -s -- -y
export PATH="$HOME/.deno/bin:$PATH"
deno -RW deno-sqlite-gen.ts
  • Deno用パッケージで、WebAssemblyを使用している。OSやCPUが異なっても簡単に動作する利点がある。
  • クラス名が他のパッケージと異なり、DBPreparedQueryを使用するが、DB.query()DB.queryEntries()で簡単にクエリやバインドが書ける。
  • まだJSRで配布されていないため、https://deno.landからインポートする必要がある。

クエリ実行時に使用したメソッドは以下のとおりです。

DB.execute(sql: string): void
DB.query<Row>(sql: string, params?: QueryParameterSet): Row[]
DB.queryEntries<RowObject>(sql: string, params?: QueryParameterSet): RowObject[]

deno-sqlite3

https://jsr.io/@db/sqlite

deno-sqlite3-gen.ts
import { Database } from "jsr:@db/sqlite@0.12";

type SqlBinding = null | number | string;

type SqlQuery = {
  query: string;
  bindings: SqlBinding[];
};

function sql(strings: TemplateStringsArray, ...bindings: SqlBinding[]): SqlQuery {
  const query = strings.map((str, i) => str + (i < bindings.length ? "?" : "")).join("").trim();
  return { query, bindings };
}

function lineMode(data: Record<string, string>[]): string {
  const keyWidth = Math.max(...data.flatMap((obj) => Object.keys(obj).map((k) => k.length)));
  const lines: string[] = [];
  for (const obj of data) {
    for (const [k, v] of Object.entries(obj)) {
      lines.push(`${k.padStart(keyWidth)} = ${v}`);
    }
    lines.push("");
  }
  return lines.join("\n");
}

const db = new Database("sqlite.db");

const createTable = sql`
  CREATE TABLE IF NOT EXISTS posts (
    id         INTEGER PRIMARY KEY AUTOINCREMENT,
    content    TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  );
  INSERT INTO posts (content) VALUES ('Hello, World!');
`;
db.exec(createTable.query);

const content = "Hello, World!";
const insertPost = sql`INSERT INTO posts (content) VALUES (${content})`;
db.exec(insertPost.query, insertPost.bindings);
db.exec(insertPost.query, insertPost.bindings);

const selectPosts = sql`SELECT * FROM posts`;
const results = db.prepare(selectPosts.query).all();

const output = lineMode(results as Record<string, string>[]);
console.log(output);
db.close();
curl -fsSL https://deno.land/install.sh | sh -s -- -y
export PATH="$HOME/.deno/bin:$PATH"
deno -A deno-sqlite3-gen.ts
  • WebAssembly版Deno用パッケージはWALモードが使えず、書き込みが遅いので、新たにFFIを使ったDeno用パッケージが登場。
  • TypeScript構文を活用し、シンプルで多様なAPIが提供されている。
  • JSRで配布されている。

クエリ実行時に使用したメソッドは以下のとおりです。

Database.exec(sql: string, ...params: RestBindParameters): number
Database.prepare(sql: string): Statement
Statement.all<Record<string, any>>(...args: RestBindParameters): Record<string, any>[]

bun-sqlite

https://bun.com/docs/api/sqlite

bun-sqlite-gen.ts
import { Database } from "bun:sqlite";

type SqlBinding = null | number | string;

type SqlQuery = {
  query: string;
  bindings: SqlBinding[];
};

function sql(strings: TemplateStringsArray, ...bindings: SqlBinding[]): SqlQuery {
  const query = strings.map((str, i) => str + (i < bindings.length ? "?" : "")).join("").trim();
  return { query, bindings };
}

function lineMode(data: Record<string, string>[]): string {
  const keyWidth = Math.max(...data.flatMap((obj) => Object.keys(obj).map((k) => k.length)));
  const lines: string[] = [];
  for (const obj of data) {
    for (const [k, v] of Object.entries(obj)) {
      lines.push(`${k.padStart(keyWidth)} = ${v}`);
    }
    lines.push("");
  }
  return lines.join("\n");
}

const db = new Database("sqlite.db");

const createTable = sql`
  CREATE TABLE IF NOT EXISTS posts (
    id         INTEGER PRIMARY KEY AUTOINCREMENT,
    content    TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  );
  INSERT INTO posts (content) VALUES ('Hello, World!');
`;
db.run(createTable.query);

const content = "Hello, World!";
const insertPost = sql`INSERT INTO posts (content) VALUES (${content})`;
db.run(insertPost.query, insertPost.bindings);
db.run(insertPost.query, insertPost.bindings);

const selectPosts = sql`SELECT * FROM posts`;
const results = db.query(selectPosts.query).all();

const output = lineMode(results as Record<string, string>[]);
console.log(output);
db.close();
curl -fsSL https://bun.sh/install | bash
export PATH="$HOME/.bun/bin:$PATH"
bun bun-sqlite-gen.ts
  • BunはSQLiteドライバーをネイティブ実装している。モジュールのAPIはnode:sqliteと異なる。
  • 読み取りクエリでbetter-sqlite3deno-sqliteより高速。
  • シンプルで同期的なAPIを提供し、better-sqlite3にインスパイアされた設計。

また、BunはBun.SQLというタグ付きテンプレートリテラルを使用した各種リレーショナルデータベース用のネイティブバインディングを提供しています。
Bun v1.2.21からSQLiteをサポートし、PostgreSQLドライバーとMySQLドライバーもネイティブ実装しており、パフォーマンスも高いです。

https://bun.com/docs/api/sql

クエリ実行時に使用したメソッドは以下のとおりです。

Database.run<any[]>(sql: string, ...bindings: any[][]): Changes
Database.run<SqlBinding[]>(sql: string, ...bindings: SqlBinding[][]): Changes
Database.query<unknown, any>(sql: string): Statement<unknown, any>
Statement<unknown, any>.all(...params: any): unknown[]

cloudflare-d1

https://developers.cloudflare.com/d1/get-started/

wrangler.jsonc
{
  "name": "cloudflare-d1-gen",
  "main": "cloudflare-d1-gen.ts",
  "compatibility_date": "2025-09-11",
  "d1_databases": [
    {
      "binding": "DB",
      "database_name": "cloudflare-d1-gen",
      "database_id": ""
    }
  ],
  "observability": {
    "enabled": true
  }
}
cloudflare-d1-gen.ts
type SqlBinding = null | number | string;

type SqlQuery = {
  query: string;
  bindings: SqlBinding[];
};

function sql(strings: TemplateStringsArray, ...bindings: SqlBinding[]): SqlQuery {
  const query = strings.map((str, i) => str + (i < bindings.length ? "?" : "")).join("").trim();
  return { query, bindings };
}

function lineMode(data: Record<string, string>[]): string {
  const keyWidth = Math.max(...data.flatMap((obj) => Object.keys(obj).map((k) => k.length)));
  const lines: string[] = [];
  for (const obj of data) {
    for (const [k, v] of Object.entries(obj)) {
      lines.push(`${k.padStart(keyWidth)} = ${v}`);
    }
    lines.push("");
  }
  return lines.join("\n");
}

export interface Env {
  DB: D1Database;
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);
    if (url.pathname === "/") {
      const createTable = sql`
        CREATE TABLE IF NOT EXISTS posts (id INTEGER PRIMARY KEY AUTOINCREMENT, content TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);
        INSERT INTO posts (content) VALUES ('Hello, World!');
      `;
      await env.DB.exec(createTable.query);

      const content = "Hello, World!";
      const insertPost = sql`INSERT INTO posts (content) VALUES (${content})`;
      await env.DB.prepare(insertPost.query).bind(...insertPost.bindings).run();
      await env.DB.prepare(insertPost.query).bind(...insertPost.bindings).run();

      const selectPosts = sql`SELECT * FROM posts`;
      const { results } = await env.DB.prepare(selectPosts.query).all();

      const output = lineMode(results as Record<string, string>[]);
      return new Response(output);
    }
    return new Response("404 Not Found", { status: 404 });
  },
} satisfies ExportedHandler<Env>;
npx -y wrangler dev

実行後、http://localhost:8787をWebブラウザで開きます。
wrangler.jsoncファイルとcloudflare-d1-gen.tsファイルに基づき、.wrangler/state/v3/d1/miniflare-D1DatabaseObjectにSQLiteデータベースが作成されます。

  • Cloudflare D1は、Cloudflare Workers向けのサーバーレスSQLデータベースサービス。
  • SQLiteをベースにしており、クエリを使ってデータ操作ができる。
  • 高速でスケーラブルなデータベースを提供し、Cloudflareのエッジネットワークで動作するため、低レイテンシで利用可能。

D1Database.exec()はセミコロンではなく改行で分けて複数のクエリを実行するため、コード内にCREATE文を書く時はワンライナーにする必要があります。

https://developers.cloudflare.com/d1/worker-api/d1-database/

The input can be one or multiple queries separated by \n.

クエリ実行時に使用したメソッドは以下のとおりです。

D1Database.exec(query: string): Promise<D1ExecResult>
D1Database.prepare(query: string): D1PreparedStatement
D1PreparedStatement.bind(...values: unknown[]): D1PreparedStatement
D1PreparedStatement.run<Record<string, unknown>>(): Promise<D1Result<Record<string, unknown>>>
D1PreparedStatement.all<Record<string, unknown>>(): Promise<D1Result<Record<string, unknown>>>

cloudflare-durable-objects

https://developers.cloudflare.com/durable-objects/get-started/

wrangler.jsonc
{
  "name": "cloudflare-durable-objects-gen",
  "main": "cloudflare-durable-objects-gen.ts",
  "compatibility_date": "2025-09-11",
  "durable_objects": {
    "bindings": [
      {
        "name": "POSTS_DURABLE_OBJECT",
        "class_name": "PostsDurableObject"
      }
    ]
  },
  "migrations": [
    {
      "tag": "v1",
      "new_sqlite_classes": [
        "PostsDurableObject"
      ]
    }
  ],
  "observability": {
    "enabled": true
  }
}
cloudflare-durable-objects-gen.ts
import { DurableObject } from "cloudflare:workers";

type SqlBinding = null | number | string;

type SqlQuery = {
  query: string;
  bindings: SqlBinding[];
};

function sql(strings: TemplateStringsArray, ...bindings: SqlBinding[]): SqlQuery {
  const query = strings.map((str, i) => str + (i < bindings.length ? "?" : "")).join("").trim();
  return { query, bindings };
}

function lineMode(data: Record<string, string>[]): string {
  const keyWidth = Math.max(...data.flatMap((obj) => Object.keys(obj).map((k) => k.length)));
  const lines: string[] = [];
  for (const obj of data) {
    for (const [k, v] of Object.entries(obj)) {
      lines.push(`${k.padStart(keyWidth)} = ${v}`);
    }
    lines.push("");
  }
  return lines.join("\n");
}

export class PostsDurableObject extends DurableObject<Env> {
  name: string;
  sql: SqlStorage;

  constructor(state: DurableObjectState, env: Env) {
    super(state, env);
    this.name = "";
    this.sql = state.storage.sql;
  }

  async setName(name: string) {
    this.name = name;
  }

  async initializeAndGetPosts(): Promise<string> {
    const createTable = sql`
      CREATE TABLE IF NOT EXISTS posts (
        id         INTEGER PRIMARY KEY AUTOINCREMENT,
        content    TEXT,
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
        updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
      );
      INSERT INTO posts (content) VALUES ('Hello, World!');
    `;
    this.sql.exec(createTable.query);

    const content = "Hello, World!";
    const insertPost = sql`INSERT INTO posts (content) VALUES (${content})`;
    this.sql.exec(insertPost.query, insertPost.bindings);
    this.sql.exec(insertPost.query, insertPost.bindings);

    const selectPosts = sql`SELECT * FROM posts`;
    const results = this.sql.exec(selectPosts.query).toArray();

    const output = lineMode(results as Record<string, string>[]);
    return output;
  }
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);
    if (url.pathname === "/") {
      const name = "post";
      const id = env.POSTS_DURABLE_OBJECT.idFromName(name);
      const stub = env.POSTS_DURABLE_OBJECT.get(id);
      await stub.setName(name);
      const response = await stub.initializeAndGetPosts();
      return new Response(response);
    }
    return new Response("404 Not Found", { status: 404 });
  },
} satisfies ExportedHandler<Env>;
npx -y wrangler dev

実行後、http://localhost:8787をWebブラウザで開きます。
wrangler.jsoncファイルとcloudflare-durable-objects-gen.tsファイルに基づき、.wrangler/state/v3/do/cloudflare-durable-objects-gen-PostsDurableObjectにSQLiteデータベースが作成されます。

  • CloudflareのDurable Objectsは、分散システムでの状態保持を提供するサーバーレス機能。
  • SQLiteをDurable Objectsに組み込むことで、分散環境でもトランザクション対応のデータベースを使用可能。
  • データはDurable Objectsのインスタンス間で保持され、低レイテンシでアクセスでき、スケーラブルなデータベース操作が実現される。

2025/04/07にSQLite-backed Durable Objectsの無料枠が追加されたため、Cloudflare D1と同様に(無料枠の範囲で)無料で使うことができます。

https://developers.cloudflare.com/durable-objects/api/storage-api/

また、Cloudflare D1と異なり、awaitをつけずにクエリを同期実行できます。詳細は以下の記事を参照してください。

https://blog.cloudflare.com/sqlite-in-durable-objects/

クエリ実行時に使用したメソッドは以下のとおりです。

SqlStorage.exec<Record<string, SqlStorageValue>>(query: string, ...bindings: any[]): SqlStorageCursor<Record<string, SqlStorageValue>>
SqlStorageCursor<Record<string, SqlStorageValue>>.toArray(): Record<string, SqlStorageValue>[]

python-sqlite3

https://docs.python.org/ja/3/library/sqlite3.html

SQLタグ付きテンプレートリテラルを使わない、Pythonのコードも置いておきます。

python-sqlite3-gen.py
import sqlite3
from contextlib import closing


def line_mode(data: list[dict[str, str]]) -> str:
    key_width = max(len(k) for row in data for k in row.keys())
    lines = []
    for row in data:
        for k, v in row.items():
            lines.append(f"{k.rjust(key_width)} = {v}")
        lines.append("")
    return "\n".join(lines)


with closing(sqlite3.connect("sqlite.db")) as conn:
    conn.row_factory = sqlite3.Row
    cursor = conn.cursor()

    create_table = """
        CREATE TABLE IF NOT EXISTS posts (
            id         INTEGER PRIMARY KEY AUTOINCREMENT,
            content    TEXT,
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
            updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        );
        INSERT INTO posts (content) VALUES ('Hello, World!');
    """
    cursor.executescript(create_table)

    content = "Hello, World!"
    insert_post = "INSERT INTO posts (content) VALUES (?)"
    cursor.execute(insert_post, (content,))
    cursor.execute(insert_post, (content,))

    conn.commit()

    select_posts = "SELECT * FROM posts"
    cursor.execute(select_posts)
    results = cursor.fetchall()

    result_rows = [dict(result) for result in results]
    output = line_mode(result_rows)
    print(output)
python python-sqlite3-gen.py
  • PythonのSQLiteは標準ライブラリに含まれており、外部パッケージのインストールなしで簡単に利用できる。
  • PythonicなAPI設計で、SQLクエリの記述やデータベース操作が直感的かつシンプルに行える。
  • isolation_levelNoneでない場合、DMLのConnection.commit()が必要になるため、操作の仕方に好みが分かれる。

おわりに

10種類のSQLiteドライバー全部試すのは非常に大変でしたが、SQLタグ付きテンプレートリテラルのおかげで、DatabaseとStatementのクラスに関するソースコードやAPIドキュメントを読みながら簡単に動かすことができました。

Discussion