Skip to content

Bun は高性能な SQLite3 ドライバーをネイティブに実装しています。使用するには、組み込みの bun:sqlite モジュールからインポートします。

ts
import { Database } from "bun:sqlite";

const db = new Database(":memory:");
const query = db.query("select 'Hello world' as message;");
query.get();
txt
{ message: "Hello world" }

API はシンプルで同期的、かつ高速です。bun:sqlite の API は better-sqlite3 とその貢献者にインスピレーションを受けています。

機能には以下が含まれます:

  • トランザクション
  • パラメーター(名前付きおよび位置)
  • プリペアドステートメント
  • データ型変換(BLOBUint8Array になります)
  • ORM なしでクエリ結果をクラスにマップ - query.as(MyClass)
  • JavaScript 向け SQLite ドライバー中最速のパフォーマンス
  • bigint サポート
  • 単一の database.run(query) 呼び出しでのマルチクエリステートメント(例:SELECT 1; SELECT 2;

bun:sqlite モジュールは、読み取りクエリにおいて better-sqlite3 より約 3-6 倍、deno.land/x/sqlite より約 8-9 倍高速です。各ドライバーは Northwind Traders データセットに対してベンチマークされました。ベンチマークソース を表示および実行できます。


データベース

SQLite3 データベースを開くまたは作成するには:

ts
import { Database } from "bun:sqlite";

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

インメモリデータベースを開くには:

ts
import { Database } from "bun:sqlite";

// これらはすべて同じことを行います
const db = new Database(":memory:");
const db = new Database();
const db = new Database("");

readonly モードで開くには:

ts
import { Database } from "bun:sqlite";
const db = new Database("mydb.sqlite", { readonly: true });

ファイルが存在しない場合にデータベースを作成するには:

ts
import { Database } from "bun:sqlite";
const db = new Database("mydb.sqlite", { create: true });

厳密モード

デフォルトでは、bun:sqlite はバインディングパラメーターに $:、または @ プレフィックスを含める必要があり、パラメーターが不足していてもエラーをスローしません。

パラメーターが不足している場合にエラーをスローし、プレフィックスなしでバインディングを許可するには、Database コンストラクターで strict: true を設定します:

ts
import { Database } from "bun:sqlite";

const strict = new Database(":memory:", { strict: true });

// 誤字のためエラーをスロー:
const query = strict.query("SELECT $message;").all({ messag: "Hello world" });

const notStrict = new Database(":memory:");
// エラーをスローしない:
notStrict.query("SELECT $message;").all({ messag: "Hello world" });

ES モジュールインポートによるロード

インポート属性を使用してデータベースをロードすることもできます。

ts
import db from "./mydb.sqlite" with { type: "sqlite" };

console.log(db.query("select * from users LIMIT 1").get());

これは以下と同等です:

ts
import { Database } from "bun:sqlite";
const db = new Database("./mydb.sqlite");

.close(throwOnError: boolean = false)

データベース接続を閉じるが、既存のクエリが完了するのを許可するには、.close(false) を呼び出します:

ts
const db = new Database();
// ... 処理を実行
db.close(false);

データベースを閉じ、保留中のクエリがある場合にエラーをスローするには、.close(true) を呼び出します:

ts
const db = new Database();
// ... 処理を実行
db.close(true);

NOTE

`close(false)` はデータベースがガベージコレクションされるときに自動的に呼び出されます。複数回呼び出しても安全ですが、 最初以降は効果がありません。

using ステートメント

using ステートメントを使用して、using ブロックを抜けたときにデータベース接続が閉じられることを保証できます。

ts
import { Database } from "bun:sqlite";

{
  using db = new Database("mydb.sqlite");
  using query = db.query("select 'Hello world' as message;");
  console.log(query.get());
}
txt
{ message: "Hello world" }

.serialize()

bun:sqlite は、SQLite の組み込みメカニズムであるデータベースの シリアライズ および デシリアライズ をサポートしています。

ts
const olddb = new Database("mydb.sqlite");
const contents = olddb.serialize(); // => Uint8Array
const newdb = Database.deserialize(contents);

内部的には、.serialize()sqlite3_serialize を呼び出します。

.query()

Database インスタンスの db.query() メソッドを使用して、SQL クエリを 準備 します。結果は Database インスタンスにキャッシュされる Statement インスタンスです。クエリは実行されません。

ts
const query = db.query(`select "Hello world" as message`);

NOTE

**「キャッシュ」とはどういう意味ですか?**

キャッシュとは、コンパイルされたプリペアドステートメント(SQL バイトコード)を指し、クエリ結果ではありません。同じ SQL 文字列で db.query() を複数回呼び出すと、Bun は SQL を再コンパイルする代わりに、同じキャッシュされた Statement オブジェクトを返します。

異なるパラメーター値でキャッシュされたステートメントを安全に再利用できます:

ts
const query = db.query("SELECT * FROM users WHERE id = ?");
query.get(1); // ✓ 動作
query.get(2); // ✓ 動作 - パラメーターは毎回新しくバインド
query.get(3); // ✓ まだ動作

ワンタイムクエリでキャッシュを埋めたくない場合など、キャッシュされない新鮮な Statement インスタンスが必要な場合は、.query() の代わりに .prepare() を使用します。

ts
// キャッシュなしでプリペアドステートメントをコンパイル
const query = db.prepare("SELECT * FROM foo WHERE bar = ?");

WAL モード

SQLite は write-ahead log モード(WAL)をサポートしており、特に多くの同時リーダーと単一ライターがある状況でパフォーマンスが劇的に向上します。ほとんどの一般的なアプリケーションで WAL モードを有効にすることが広く推奨されています。

WAL モードを有効にするには、アプリケーションの開始時にこのプラグマクエリを実行します:

ts
db.run("PRAGMA journal_mode = WAL;");

WAL モードとは?

WAL モードでは、データベースへの書き込みは「WAL ファイル」(write-ahead log)と呼ばれる別のファイルに直接書き込まれます。このファイルは後でメインのデータベースファイルに統合されます。保留中の書き込みのバッファーとして考えてください。詳細な概要については SQLite ドキュメント を参照してください。

macOS では、WAL ファイルはデフォルトで永続的になる場合があります。これはバグではなく、macOS のシステムバージョンの SQLite の設定方法です。


ステートメント

Statement準備されたクエリ であり、効率的なバイナリ形式に解析およびコンパイルされています。パフォーマンスの高い方法で複数回実行できます。

Database インスタンスの .query メソッドでステートメントを作成します。

ts
const query = db.query(`select "Hello world" as message`);

クエリにはパラメーターを含めることができます。これらは数値(?1)または名前付き($param または :param または @param)にできます。

ts
const query = db.query(`SELECT ?1, ?2;`);
const query = db.query(`SELECT $param1, $param2;`);

値はクエリ実行時にこれらのパラメーターにバインドされます。Statement はいくつかの異なるメソッドで実行でき、それぞれが異なる形式で結果を返します。

値のバインド

ステートメントに値をバインドするには、.all().get().run()、または .values() メソッドにオブジェクトを渡します。

ts
const query = db.query(`select $message;`);
query.all({ $message: "Hello world" });

位置パラメーターを使用してバインドすることもできます:

ts
const query = db.query(`select ?1;`);
query.all("Hello world");

strict: true を使用すると、プレフィックスなしで値をバインドできます

デフォルトでは、$:、および @ プレフィックスは名前付きパラメーターに値をバインドするときに含まれます。これらのプレフィックスなしでバインドするには、Database コンストラクターで strict オプションを使用します。

ts
import { Database } from "bun:sqlite";

const db = new Database(":memory:", {
  // プレフィックスなしで値をバインド
  strict: true, 
});

const query = db.query(`select $message;`);

// strict: true
query.all({ message: "Hello world" });

// strict: false
// query.all({ $message: "Hello world" });

.all()

.all() を使用してクエリを実行し、結果をオブジェクトの配列として取得します。

ts
const query = db.query(`select $message;`);
query.all({ $message: "Hello world" });
txt
[{ message: "Hello world" }]

内部的には、これは sqlite3_reset を呼び出し、sqlite3_stepSQLITE_DONE を返すまで繰り返し呼び出します。

.get()

.get() を使用してクエリを実行し、最初の結果をオブジェクトとして取得します。

ts
const query = db.query(`select $message;`);
query.get({ $message: "Hello world" });
txt
{ $message: "Hello world" }

内部的には、これは sqlite3_reset を呼び出し、その後 sqlite3_stepSQLITE_ROW を返さなくなるまで呼び出します。クエリが行を返さない場合、undefined が返されます。

.run()

.run() を使用してクエリを実行し、undefined を取得します。これはスキーマ変更クエリ(例:CREATE TABLE)やバルクライティング操作に役立ちます。

ts
const query = db.query(`create table foo;`);
query.run();
txt
{
  lastInsertRowid: 0,
  changes: 0,
}

内部的には、これは sqlite3_reset を呼び出し、sqlite3_step を 1 回呼び出します。結果を気にしない場合、すべての行をステップスルーする必要はありません。

lastInsertRowid プロパティは、データベースに最後に挿入された行の ID を返します。changes プロパティは、クエリによって影響を受けた行数です。

.as(Class) - クエリ結果をクラスにマップ

.as(Class) を使用してクエリを実行し、結果をクラスのインスタンスとして取得します。これにより、結果にメソッドおよびゲッター/セッターを付加できます。

ts
class Movie {
  title: string;
  year: number;

  get isMarvel() {
    return this.title.includes("Marvel");
  }
}

const query = db.query("SELECT title, year FROM movies").as(Movie);
const movies = query.all();
const first = query.get();

console.log(movies[0].isMarvel);
console.log(first.isMarvel);
txt
true
true

パフォーマンスの最適化として、クラスのコンストラクターは呼び出されず、デフォルトの初期化子は実行されず、プライベートフィールドにはアクセスできません。これは new を使用するよりも Object.create を使用するようなものです。クラスのprototype はオブジェクトに割り当てられ、メソッドが付加され、ゲッター/セッターが設定されますが、コンストラクターは呼び出されません。

データベース列はクラスインスタンスのプロパティとして設定されます。

.iterate()@@iterator

.iterate() を使用してクエリを実行し、結果を段階的に返します。これは、すべての結果をメモリにロードせずに 1 行ずつ処理したい大きな結果セットに役立ちます。

ts
const query = db.query("SELECT * FROM foo");
for (const row of query.iterate()) {
  console.log(row);
}

@@iterator プロトコルを使用することもできます:

ts
const query = db.query("SELECT * FROM foo");
for (const row of query) {
  console.log(row);
}

.values()

values() を使用してクエリを実行し、すべての結果を配列の配列として取得します。

ts
const query = db.query(`select $message;`);

query.values({ $message: "Hello world" });
query.values(2);
txt
[
  [ "Iron Man", 2008 ],
  [ "The Avengers", 2012 ],
  [ "Ant-Man: Quantumania", 2023 ],
]

内部的には、これは sqlite3_reset を呼び出し、sqlite3_stepSQLITE_DONE を返すまで繰り返し呼び出します。

.finalize()

.finalize() を使用して Statement を破棄し、それに関連付けられたリソースを解放します。一度 finalize された Statement は再度実行できません。通常、ガベージコレクターがこれを行いますが、明示的な finalize はパフォーマンスに敏感なアプリケーションで役立つ場合があります。

ts
const query = db.query("SELECT title, year FROM movies");
const movies = query.all();
query.finalize();

.toString()

Statement インスタンスで toString() を呼び出すと、展開された SQL クエリが出力されます。これはデバッグに役立ちます。

ts
import { Database } from "bun:sqlite";

// セットアップ
const query = db.query("SELECT $param;");

console.log(query.toString()); // => "SELECT NULL"

query.run(42);
console.log(query.toString()); // => "SELECT 42"

query.run(365);
console.log(query.toString()); // => "SELECT 365"

内部的には、これは sqlite3_expanded_sql を呼び出します。パラメーターは最後にバインドされた値を使用して展開されます。

パラメーター

クエリにはパラメーターを含めることができます。これらは数値(?1)または名前付き($param または :param または @param)にできます。クエリ実行時にこれらのパラメーターに値をバインドします:

ts
const query = db.query("SELECT * FROM foo WHERE bar = $bar");
const results = query.all({
  $bar: "bar",
});
txt
[{ "$bar": "bar" }]

番号付き(位置)パラメーターも動作します:

ts
const query = db.query("SELECT ?1, ?2");
const results = query.all("hello", "goodbye");
txt
[
	{
		"?1": "hello",
		"?2": "goodbye",
	},
];

整数

sqlite は符号付き 64 ビット整数をサポートしていますが、JavaScript は符号付き 52 ビット整数または bigint を使用した任意精度整数のみをサポートしています。

bigint 入力はどこでもサポートされていますが、デフォルトでは bun:sqlite は整数を number 型として返します。2^53 より大きい整数を処理する必要がある場合は、Database インスタンスを作成する際に safeIntegers オプションを true に設定します。これにより、bun:sqlite に渡される bigint が 64 ビットを超えないことも検証されます。

デフォルトでは、bun:sqlite は整数を number 型として返します。2^53 より大きい整数を処理する必要がある場合は、bigint 型を使用できます。

safeIntegers: true

safeIntegerstrue の場合、bun:sqlite は整数を bigint 型として返します:

ts
import { Database } from "bun:sqlite";

const db = new Database(":memory:", { safeIntegers: true });
const query = db.query(`SELECT ${BigInt(Number.MAX_SAFE_INTEGER) + 102n} as max_int`);
const result = query.get();

console.log(result.max_int);
txt
9007199254741093n

safeIntegerstrue の場合、バインドされたパラメーターの bigint 値が 64 ビットを超えると bun:sqlite はエラーをスローします:

ts
import { Database } from "bun:sqlite";

const db = new Database(":memory:", { safeIntegers: true });
db.run("CREATE TABLE test (id INTEGER PRIMARY KEY, value INTEGER)");

const query = db.query("INSERT INTO test (value) VALUES ($value)");

try {
  query.run({ $value: BigInt(Number.MAX_SAFE_INTEGER) ** 2n });
} catch (e) {
  console.log(e.message);
}
txt
BigInt value '81129638414606663681390495662081' is out of range

safeIntegers: false(デフォルト)

safeIntegersfalse の場合、bun:sqlite は整数を number 型として返し、53 ビットを超えたビットを切り捨てます:

ts
import { Database } from "bun:sqlite";

const db = new Database(":memory:", { safeIntegers: false });
const query = db.query(`SELECT ${BigInt(Number.MAX_SAFE_INTEGER) + 102n} as max_int`);
const result = query.get();
console.log(result.max_int);
txt
9007199254741092

トランザクション

トランザクションは、複数のクエリを アトミック に実行するためのメカニズムです。つまり、すべてのクエリが成功するか、すべてが失敗します。db.transaction() メソッドでトランザクションを作成します:

ts
const insertCat = db.prepare("INSERT INTO cats (name) VALUES ($name)");
const insertCats = db.transaction(cats => {
  for (const cat of cats) insertCat.run(cat);
});

この段階では、まだ猫を挿入していません!db.transaction() の呼び出しは、クエリを実行する関数を ラップする 新しい関数(insertCats)を返します。

トランザクションを実行するには、この関数を呼び出します。すべての引数はラップされた関数に渡され、ラップされた関数の戻り値はトランザクション関数によって返されます。ラップされた関数は、トランザクションが実行される場所で定義された this コンテキストにもアクセスできます。

ts
const insert = db.prepare("INSERT INTO cats (name) VALUES ($name)");
const insertCats = db.transaction(cats => {
  for (const cat of cats) insert.run(cat);
  return cats.length;
});

const count = insertCats([{ $name: "Keanu" }, { $name: "Salem" }, { $name: "Crookshanks" }]);

console.log(`Inserted ${count} cats`);

ドライバーは insertCats が呼び出されたときに自動的にトランザクションを begin し、ラップされた関数が戻るときに commit します。例外がスローされた場合、トランザクションはロールバックされます。例外は通常のように伝播します。キャッチされません。

NOTE

**ネストされたトランザクション** — トランザクション関数は他のトランザクション関数内から呼び出すことができます。その場合、内部トランザクションは [セーブポイント](https://www.sqlite.org/lang_savepoint.html) になります。

ネストされたトランザクションの例を表示">

ts
// セットアップ
import { Database } from "bun:sqlite";
const db = Database.open(":memory:");
db.run("CREATE TABLE expenses (id INTEGER PRIMARY KEY AUTOINCREMENT, note TEXT, dollars INTEGER);");
db.run("CREATE TABLE cats (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE, age INTEGER)");
const insertExpense = db.prepare("INSERT INTO expenses (note, dollars) VALUES (?, ?)");
const insert = db.prepare("INSERT INTO cats (name, age) VALUES ($name, $age)");
const insertCats = db.transaction(cats => {
  for (const cat of cats) insert.run(cat);
});

const adopt = db.transaction(cats => {
  insertExpense.run("adoption fees", 20);
  insertCats(cats); // ネストされたトランザクション
});

adopt([
  { $name: "Joey", $age: 2 },
  { $name: "Sally", $age: 4 },
  { $name: "Junior", $age: 1 },
]);

トランザクションには deferredimmediate、および exclusive バージョンもあります。

ts
insertCats(cats); // "BEGIN" を使用
insertCats.deferred(cats); // "BEGIN DEFERRED" を使用
insertCats.immediate(cats); // "BEGIN IMMEDIATE" を使用
insertCats.exclusive(cats); // "BEGIN EXCLUSIVE" を使用

.loadExtension()

SQLite 拡張 をロードするには、Database インスタンスで .loadExtension(name) を呼び出します。

ts
import { Database } from "bun:sqlite";

const db = new Database();
db.loadExtension("myext");

NOTE

**MacOS ユーザー** デフォルトでは、macOS は拡張をサポートしていない Apple の独自ビルドの SQLite を出荷しています。拡張を使用するには、バニラビルドの SQLite をインストールする必要があります。
bash
brew install sqlite
which sqlite # バイナリへのパスを取得

bun:sqlite を新しいビルドに向けるには、Database インスタンスを作成する前に Database.setCustomSQLite(path) を呼び出します(他のオペレーティングシステムでは、これは何もしません)。SQLite の .dylib ファイルへのパスを渡します。実行ファイルではありません。最近の Homebrew バージョンでは、これは /opt/homebrew/Cellar/sqlite/<version>/libsqlite3.dylib のようなものです。

ts
import { Database } from "bun:sqlite";

Database.setCustomSQLite("/path/to/libsqlite.dylib");

const db = new Database();
db.loadExtension("myext");

.fileControl(cmd: number, value: any)

高度な sqlite3_file_control API を使用するには、Database インスタンスで .fileControl(cmd, value) を呼び出します。

ts
import { Database, constants } from "bun:sqlite";

const db = new Database();
// WAL モードが永続的でないことを確認
// これにより、データベースが閉じられた後に wal ファイルが残るのを防ぎます
db.fileControl(constants.SQLITE_FCNTL_PERSIST_WAL, 0);

value は以下にできます:

  • number
  • TypedArray
  • undefined または null

リファレンス

ts
class Database {
  constructor(
    filename: string,
    options?:
      | number
      | {
          readonly?: boolean;
          create?: boolean;
          readwrite?: boolean;
          safeIntegers?: boolean;
          strict?: boolean;
        },
  );

  query<ReturnType, ParamsType>(sql: string): Statement<ReturnType, ParamsType>;
  prepare<ReturnType, ParamsType>(sql: string): Statement<ReturnType, ParamsType>;
  run(sql: string, params?: SQLQueryBindings): { lastInsertRowid: number; changes: number };
  exec = this.run;

  transaction(insideTransaction: (...args: any) => void): CallableFunction & {
    deferred: (...args: any) => void;
    immediate: (...args: any) => void;
    exclusive: (...args: any) => void;
  };

  close(throwOnError?: boolean): void;
}

class Statement<ReturnType, ParamsType> {
  all(...params: ParamsType[]): ReturnType[];
  get(...params: ParamsType[]): ReturnType | null;
  run(...params: ParamsType[]): {
    lastInsertRowid: number;
    changes: number;
  };
  values(...params: ParamsType[]): unknown[][];

  finalize(): void; // ステートメントを破棄してリソースをクリーンアップ
  toString(): string; // SQL にシリアライズ

  columnNames: string[]; // 結果セットの列名
  columnTypes: string[]; // 最初の行の実際の値に基づく型(.get()/.all() を最初に呼び出す)
  declaredTypes: (string | null)[]; // CREATE TABLE スキーマからの型(.get()/.all() を最初に呼び出す)
  paramsCount: number; // ステートメントが予期するパラメーターの数
  native: any; // ステートメントを表すネイティブオブジェクト

  as<T>(Class: new (...args: any[]) => T): Statement<T, ParamsType>;
}

type SQLQueryBindings =
  | string
  | bigint
  | TypedArray
  | number
  | boolean
  | null
  | Record<string, string | bigint | TypedArray | number | boolean | null>;

データ型

JavaScript 型SQLite 型
stringTEXT
numberINTEGER または DECIMAL
booleanINTEGER (1 または 0)
Uint8ArrayBLOB
BufferBLOB
bigintINTEGER
nullNULL

Bun by www.bunjs.com.cn 編集