執行環境套件管理器打包器測試執行器程式碼檢查格式化指南參考部落格意見回饋

SQLite

Bun 內建了高效能的 SQLite3 驅動。

Bun 原生實現了高效能的 SQLite3 驅動。要使用它,請從內建的 bun:sqlite 模組匯入。

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

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

API 是同步的,並且速度很快。感謝 better-sqlite3 及其貢獻者為 bun:sqlite 的 API 提供了啟發。

功能包括:

  • 事務
  • 引數(命名引數和位置引數)
  • 預編譯語句
  • 資料型別轉換(BLOB 轉換為 Uint8Array)
  • 無需 ORM 即可將查詢結果對映到類 - query.as(MyClass)
  • 在 JavaScript 的所有 SQLite 驅動中擁有最快的效能
  • 支援 bigint
  • 多查詢語句(例如 SELECT 1; SELECT 2;)可在一次呼叫 database.run(query) 中執行

bun:sqlite 在讀取查詢上比 better-sqlite3 快約 3-6 倍,比 deno.land/x/sqlite 快約 8-9 倍。各驅動均基於 Northwind Traders 資料集進行基準測試。檢視並執行基準測試原始碼。

Bun、better-sqlite3 與 deno.land/x/sqlite 的 SQLite 基準測試


資料庫

開啟或建立 SQLite3 資料庫:

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

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

開啟記憶體資料庫:

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

const db = new Database(":memory:");
// 以下方式效果相同
const db = new Database();
const db = new Database("");

以只讀模式開啟:

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

如果檔案不存在則建立資料庫:

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

嚴格模式

預設情況下,bun:sqlite 繫結引數時需要包含 $、: 或 @ 字首,且如果缺少引數不會丟擲錯誤。

如果希望缺少引數時丟擲錯誤,並且允許繫結時不使用字首,可以在 Database 建構函式中設定 strict: true:

db.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 模組匯入屬性載入

你還可以使用匯入屬性載入資料庫。

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

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

此用法等同於:

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

.close(throwOnError: boolean = false)

要關閉資料庫連線,同時讓通過 .prepare() 建立的語句繼續工作,直到它們被終結或垃圾回收,請呼叫 .close(false):

db.ts
const db = new Database();
// ... 執行操作
db.close(false);

Database 擁有通過 .query() 建立的語句,並會立即將它們終結。最後一個未完成的 .prepare() 語句被終結後,Bun 會釋放底層連線(以及資料庫檔案控制代碼)。

要終結所有未完成的語句、立即釋放連線,並在 SQLite 報告關閉錯誤時丟擲異常,請呼叫 .close(true):

db.ts
const db = new Database();
// ... 執行操作
db.close(true);

使用被 close() 終結的語句會丟擲 Database has closed。有兩個例外:toString() 會傳回空字串,finalize() 仍可安全呼叫。

using 語句

using 語句會在程式碼塊退出時關閉資料庫連線。

db.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());
}
Text
{ message: "Hello world" }

.serialize()

bun:sqlite 支援 SQLite 內建的資料庫序列化和反序列化機制,詳見 serialize 和 deserialize。

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

內部呼叫了 sqlite3_serialize。

.query()

在 Database 例項上使用 db.query() 方法來預處理 SQL 查詢。該方法傳回一個快取在 Database 例項上的 Statement 例項。查詢不會被執行。

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

WAL 模式

SQLite 支援預寫式日誌模式(WAL),它能顯著提升效能,尤其是在有多個並行讀取者和單個寫入者時。我們建議大多數應用啟用 WAL 模式。

啟用 WAL 模式,請在程式開始時執行此 pragma 查詢:

db.ts
db.run("PRAGMA journal_mode = WAL;");
什麼是 WAL 模式?

在 WAL 模式下,對資料庫的寫入會直接寫入一個名為“WAL 檔案”(-wal)的獨立檔案。SQLite 還會建立一個共享記憶體索引檔案(-shm)以協調讀取。之後,SQLite 會將 WAL 檔案中的內容整合到主資料庫檔案中。可以把 WAL 檔案看作待處理寫入的緩衝區。有關更詳細的概述,請參閱 SQLite 文件。

WAL 附屬檔案清理

使用基於檔案的資料庫啟用 WAL 模式時,SQLite 會在資料庫旁建立兩個附屬檔案:預寫式日誌(-wal)和共享記憶體索引(-shm)。SQLite 是否會在 .close() 後自動刪除這些檔案,取決於你的平臺:

  • macOS:Bun 使用系統提供的 SQLite,Apple 建置時啟用了持久化 WAL。-wal 和 -shm 檔案在關閉後會保留。這不是 bug——這是 Apple 設定系統 SQLite 的方式。
  • Linux 和 Windows:Bun 靜態連結了自己的 SQLite 建置版本,該版本遵循上游預設設定。沒有其他連線處於開啟狀態時,SQLite 通常會在關閉後刪除附屬檔案。

若要確保在所有平臺上都清理附屬檔案,請在關閉前停用 WAL 持久化並執行截斷檢查點:

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

const db = new Database("mydb.sqlite");
db.run("PRAGMA journal_mode = WAL;");

// ... 使用資料庫 ...

// 停用持久化 WAL(macOS 上需要)
db.fileControl(constants.SQLITE_FCNTL_PERSIST_WAL, 0);
// 執行檢查點並截斷 WAL 檔案
db.run("PRAGMA wal_checkpoint(TRUNCATE);");
db.close();
// 僅保留 mydb.sqlite —— 沒有 -wal 或 -shm 檔案

語句

Statement 是一個_預編譯查詢_,也就是說,它已經被解析並編譯成高效的二進位制形式。它可以被執行多次。

通過 Database 例項的 .query 方法建立:

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

查詢中可以包含引數,既可以是數字(例如 ?1),也可以是命名引數($param、:param 或 @param)。

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

執行查詢時,將值繫結到這些引數。你可以通過幾種不同的方法執行 Statement,每種方法都會以不同形式傳回結果。

繫結引數值

通過向 .all()、.get()、.run() 或 .values() 方法傳入物件繫結引數值。

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

也可以使用位置引數繫結:

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

strict: true 允許無字首繫結

預設繫結命名引數時需要包含 $、: 或 @ 字首。啟用 Database 建構函式中的 strict 選項可允許不帶字首繫結:

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

const db = new Database(":memory:", {
  // 繫結時省略字首
  strict: true, // [!code ++]
});

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

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

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

.all()

使用 .all() 執行查詢,傳回所有結果陣列。

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

內部呼叫 sqlite3_reset,並重復呼叫 sqlite3_step 直到傳回 SQLITE_DONE。

.get()

使用 .get() 執行查詢,並獲取第一條結果物件。

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

內部會先呼叫 sqlite3_reset,隨後重複呼叫 sqlite3_step,直到它不再傳回 SQLITE_ROW。如果查詢沒有傳回任何行,結果為 null。

.run()

使用 .run() 執行查詢,並傳回一個包含執行後設資料的物件。這對於修改架構的查詢(例如 CREATE TABLE)或批次寫入操作非常有用。

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

內部呼叫 sqlite3_reset 並呼叫一次 sqlite3_step,不遍歷所有結果行,提升效能。

lastInsertRowid 屬性是最後插入資料庫的行的 ID。changes 屬性是查詢影響的行數。

.as(Class) - 將結果對映為類例項

使用 .as(Class) 執行查詢,並將結果作為類的例項傳回。類的方法、獲取器和設定器在每一行上都可用。

db.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);
Text
true
true

作為一項效能最佳化,Bun 不會呼叫類建構函式或執行預設初始化器,並且無法訪問私有欄位。這更像是 Object.create,而不是 new:Bun 會將類的原型指定給物件,因此其方法、獲取器和設定器都能正常工作。

Bun 會將資料庫列設為類例項上的屬性。

.iterate() (@@iterator)

使用 .iterate() 執行查詢並逐條傳回結果。適合處理巨大結果集,避免一次性載入大量資料。

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

也可以使用 @@iterator 協議:

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

.values()

使用 .values() 執行查詢,傳回結果的二維陣列。

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

query.values({ $message: "Hello world" });
query.values(2);
Text
[ [ "Hello world" ] ]
[ [ 2 ] ]

內部呼叫 sqlite3_reset,並重復呼叫 sqlite3_step 直到傳回 SQLITE_DONE。

.finalize()

使用 .finalize() 銷燬 Statement 並釋放與其關聯的所有資源。完成銷燬後,Statement 無法再次執行。通常情況下,垃圾回收器會替你完成此操作,但在對效能敏感的應用中,顯式銷燬可能很有用。

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

.toString()

對 Statement 呼叫 toString() 會打印出代入引數後的完整 SQL 語句,方便除錯。

db.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)。執行查詢時繫結引數值:

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

數字(位置)引數同樣支援:

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

整數

SQLite 支援有符號 64 位整數,但 JavaScript 只支援有符號 53 位整數或使用 bigint 表示的任意精度整數。

所有位置都支援輸入 bigint,但預設情況下,bun:sqlite 會將整數作為 number 型別傳回。如果需要處理大於 2^53 的整數,請在建立 Database 例項時將 safeIntegers 選項設為 true。該選項還會驗證傳遞給 bun:sqlite 的 bigint 值是否超出 64 位範圍。

safeIntegers: true

當 safeIntegers 為 true 時,bun:sqlite 將整數傳回為 bigint 型別:

db.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);
Text
9007199254741093n

當 safeIntegers 為 true 時,如果繫結引數中的 bigint 值超過 64 位,bun:sqlite 會丟擲錯誤:

db.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);
}
Text
BigInt value '81129638414606663681390495662081' is out of range

safeIntegers: false(預設)

當 safeIntegers 為 false 時,bun:sqlite 會將整數作為 number 型別傳回,並將超出 53 位的整數四捨五入到最接近的可表示 number:

db.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);
Text
9007199254741092

事務

事務以_原子方式_執行多個查詢:要麼全部成功,要麼一個也不成功。使用 db.transaction() 方法建立事務:

db.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 上下文。

db.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 時,驅動會自動開始事務,並在被包裝的函式傳回時提交事務。如果丟擲異常,驅動會回滾事務。異常會照常傳播;驅動不會捕獲它。

事務函式還提供 deferred、immediate、exclusive 等版本:

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

.loadExtension()

要載入 SQLite 擴充套件,請在 Database 例項上呼叫 .loadExtension(name):

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

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

.fileControl(cmd: number, value: any)

要使用高階的 sqlite3_file_control API,請在 Database 例項上呼叫 .fileControl(cmd, value)。實際範例請參見 WAL 附屬檔案清理。

db.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

參考

型別參考
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