Bun

SQLite

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

import { Database } from "bun:sqlite";

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

API 介面簡單、同步且快速。感謝 better-sqlite3 及其貢獻者啟發了 bun:sqlite 的 API 設計。

功能包含

  • 交易
  • 參數(具名 & 定位)
  • 預先處理的陳述式
  • 資料類型轉換 (BLOB 轉換為 Uint8Array)
  • 將查詢結果對應到類別,無需 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 資料集進行了效能評測。檢視並執行效能評測來源碼

SQLite benchmarks for Bun, better-sqlite3, and deno.land/x/sqlite
效能評測於 M1 MacBook Pro (64GB) 上執行,macOS 版本為 12.3.1

資料庫

開啟或建立 SQLite3 資料庫

import { Database } from "bun:sqlite";

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

開啟記憶體內資料庫

import { Database } from "bun:sqlite";

// all of these do the same thing
const db = new Database(":memory:");
const db = new Database();
const db = new Database("");

readonly 模式開啟

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

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

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

嚴格模式

Bun v1.1.14 版本新增

預設情況下,bun:sqlite 要求繫結參數必須包含 $:@ 前綴,並且在缺少參數時不會拋出錯誤。

若要在缺少參數時拋出錯誤,並允許在沒有前綴的情況下進行繫結,請在 Database 建構函式中設定 strict: true

import { Database } from "bun:sqlite";

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

// throws error because of the typo:
const query = strict
  .query("SELECT $message;")
  .all({ message: "Hello world" });

const notStrict = new Database(
  ":memory:"
);
// does not throw error:
notStrict
  .query("SELECT $message;")
  .all({ message: "Hello world" });

透過 ES 模組匯入載入

您也可以使用 import attribute 來載入資料庫。

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

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

這等同於以下程式碼

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

.close(throwOnError: boolean = false)

若要關閉資料庫連線,但允許現有的查詢完成,請呼叫 .close(false)

const db = new Database();
// ... do stuff
db.close(false);

若要關閉資料庫,並在有任何待處理查詢時拋出錯誤,請呼叫 .close(true)

const db = new Database();
// ... do stuff
db.close(true);

注意:當資料庫被垃圾回收時,會自動呼叫 close(false)。可以安全地多次呼叫,但第一次之後就沒有效果。

using 陳述式

您可以使用 using 陳述式來確保在退出 using 區塊時關閉資料庫連線。

import { Database } from "bun:sqlite";

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

.serialize()

bun:sqlite 支援 SQLite 的內建機制,用於序列化反序列化資料庫到記憶體或從記憶體載入。

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

在內部,.serialize() 呼叫 sqlite3_serialize

.query()

在您的 Database 實例上使用 db.query() 方法來預先處理 SQL 查詢。結果是一個 Statement 實例,它將被快取在 Database 實例上。查詢將不會被執行。

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

注意 — 使用 .prepare() 方法來預先處理查詢,而不要將其快取在 Database 實例上。

// compile the prepared statement
const query = db.prepare("SELECT * FROM foo WHERE bar = ?");

WAL 模式

SQLite 支援 預寫式日誌模式 (WAL),這可以顯著提高效能,尤其是在多個並行讀取器和單個寫入器的情況下。強烈建議在大多數典型應用程式中啟用 WAL 模式。

若要啟用 WAL 模式,請在您的應用程式開頭執行此 pragma 查詢

db.exec("PRAGMA journal_mode = WAL;");

什麼是 WAL 模式

陳述式

Statement 是一個預先處理的查詢,這表示它已被解析並編譯成高效的二進制形式。它可以高效地多次執行。

使用您的 Database 實例上的 .query 方法建立陳述式。

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

查詢可以包含參數。這些參數可以是數字 (?1) 或具名 ($param:param@param)。

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

值在查詢執行時繫結到這些參數。Statement 可以使用幾種不同的方法執行,每種方法都以不同的形式傳回結果。

繫結值

若要將值繫結到陳述式,請將物件傳遞給 .all().get().run().values() 方法。

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

您也可以使用位置參數進行繫結

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

strict: true 讓您可以在沒有前綴的情況下繫結值

Bun v1.1.14 版本新增

預設情況下,當將值繫結到具名參數時,會包含 $:@ 前綴。若要在沒有這些前綴的情況下進行繫結,請在 Database 建構函式中使用 strict 選項。

import { Database } from "bun:sqlite";

const db = new Database(":memory:", {
  // bind values without prefixes
  strict: true,
});

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

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

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

.all()

使用 .all() 執行查詢,並以物件陣列的形式取回結果。

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

在內部,這會呼叫 sqlite3_reset 並重複呼叫 sqlite3_step 直到它傳回 SQLITE_DONE

.get()

使用 .get() 執行查詢,並以物件的形式取回第一個結果。

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

在內部,這會呼叫 sqlite3_reset,然後呼叫 sqlite3_step,直到不再傳回 SQLITE_ROW。如果查詢未傳回任何列,則會傳回 undefined

.run()

使用 .run() 執行查詢並取回 undefined。這對於修改結構描述的查詢 (例如 CREATE TABLE) 或批量寫入操作很有用。

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

在內部,這會呼叫 sqlite3_reset 並呼叫 sqlite3_step 一次。當您不關心結果時,無需逐步瀏覽所有列。

自 Bun v1.1.14 起,.run() 傳回一個具有兩個屬性的物件:lastInsertRowidchanges

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

.as(Class) - 將查詢結果對應到類別

Bun v1.1.14 版本新增

使用 .as(Class) 執行查詢,並以類別實例的形式取回結果。這讓您可以將方法 & getter/setter 附加到結果。

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); // => true
console.log(first.isMarvel); // => true

作為效能最佳化,不會呼叫類別建構函式,不會執行預設初始化器,並且無法存取私有欄位。這更像是使用 Object.create 而不是 new。類別的原型被分配給物件,方法被附加,並且設定 getter/setter,但不會呼叫建構函式。

資料庫欄位設定為類別實例上的屬性。

.iterate() (@@iterator)

使用 .iterate() 執行查詢並以增量方式傳回結果。這對於您想要一次處理一列而無需將所有結果載入記憶體的大型結果集很有用。

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

您也可以使用 @@iterator 協定

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

此功能在 Bun v1.1.31 中新增。

.values()

使用 values() 執行查詢,並以陣列的陣列形式取回所有結果。

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

query.values(2);
// [
//   [ "Iron Man", 2008 ],
//   [ "The Avengers", 2012 ],
//   [ "Ant-Man: Quantumania", 2023 ],
// ]

在內部,這會呼叫 sqlite3_reset 並重複呼叫 sqlite3_step 直到它傳回 SQLITE_DONE

.finalize()

使用 .finalize() 銷毀 Statement 並釋放與其關聯的任何資源。一旦完成,Statement 將無法再次執行。通常,垃圾回收器會為您執行此操作,但顯式完成在效能敏感的應用程式中可能很有用。

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

.toString()

Statement 實例上呼叫 toString() 會印出展開的 SQL 查詢。這對於偵錯很有用。

import { Database } from "bun:sqlite";

// setup
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)。在執行查詢時將值繫結到這些參數

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

編號 (位置) 參數也適用

查詢
結果
查詢
const query = db.query("SELECT ?1, ?2");
const results = query.all("hello", "goodbye");
結果
[
  {
    "?1": "hello",
    "?2": "goodbye"
  }
]

整數

sqlite 支援帶符號的 64 位元整數,但 JavaScript 僅支援帶符號的 52 位元整數或具有 bigint 的任意精度整數。

bigint 輸入在任何地方都受支援,但預設情況下 bun:sqlite 將整數傳回為 number 類型。如果您需要處理大於 2^53 的整數,請在建立 Database 實例時將 safeIntegers 選項設定為 true。這也會驗證傳遞給 bun:sqlitebigint 是否未超過 64 位元。

預設情況下,bun:sqlite 將整數傳回為 number 類型。如果您需要處理大於 2^53 的整數,您可以使用 bigint 類型。

safeIntegers: true

Bun v1.1.14 版本新增

safeIntegerstrue 時,bun:sqlite 將以 bigint 類型傳回整數

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); // => 9007199254741093n

safeIntegerstrue 時,如果繫結參數中的 bigint 值超過 64 位元,bun:sqlite 將拋出錯誤

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); // => BigInt value '81129638414606663681390495662081' is out of range
}

safeIntegers: false (預設)

safeIntegersfalse 時,bun:sqlite 將以 number 類型傳回整數,並截斷任何超出 53 位元的位元

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); // => 9007199254741092

交易

交易是一種以原子方式執行多個查詢的機制;也就是說,所有查詢都成功,或者都不成功。使用 db.transaction() 方法建立交易

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

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 它。如果拋出異常,交易將會回滾。異常將照常傳播;它不會被捕獲。

巢狀交易 — 交易函數可以從其他交易函數內部呼叫。執行此操作時,內部交易將變為儲存點

檢視巢狀交易範例

交易也提供 deferredimmediateexclusive 版本。

insertCats(cats); // uses "BEGIN"
insertCats.deferred(cats); // uses "BEGIN DEFERRED"
insertCats.immediate(cats); // uses "BEGIN IMMEDIATE"
insertCats.exclusive(cats); // uses "BEGIN EXCLUSIVE"

.loadExtension()

若要載入 SQLite 擴充功能,請在您的 Database 實例上呼叫 .loadExtension(name)

import { Database } from "bun:sqlite";

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

針對 macOS 使用者

.fileControl(cmd: number, value: any)

若要使用進階的 sqlite3_file_control API,請在您的 Database 實例上呼叫 .fileControl(cmd, value)

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

const db = new Database();
// Ensure WAL mode is NOT persistent
// this prevents wal files from lingering after the database is closed
db.fileControl(constants.SQLITE_FCNTL_PERSIST_WAL, 0);

value 可以是

  • number
  • TypedArray
  • undefinednull

參考

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

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

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

  finalize(): void; // destroy statement and clean up resources
  toString(): string; // serialize to SQL

  columnNames: string[]; // the column names of the result set
  paramsCount: number; // the number of parameters expected by the statement
  native: any; // the native object representing the statement

  as(Class: new () => ReturnType): this;
}

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

資料類型

JavaScript 類型SQLite 類型
字串TEXT
numberINTEGERDECIMAL
布林值INTEGER (1 或 0)
Uint8ArrayBLOB
BufferBLOB
bigintINTEGER
nullNULL