Skip to content

該接口設計簡單且高性能,使用標記模板字面量進行查詢,並提供連接池、事務和預准備語句等功能。

ts
import { sql, SQL } from "bun";

// PostgreSQL(默認)
const users = await sql`
  SELECT * FROM users
  WHERE active = ${true}
  LIMIT ${10}
`;

// 使用 MySQL
const mysql = new SQL("mysql://user:pass@localhost:3306/mydb");
const mysqlResults = await mysql`
  SELECT * FROM users 
  WHERE active = ${true}
`;

// 使用 SQLite
const sqlite = new SQL("sqlite://myapp.db");
const sqliteResults = await sqlite`
  SELECT * FROM users 
  WHERE active = ${1}
`;

特性

  • 標記模板字面量防止 SQL 注入
  • 事務
  • 命名和位置參數
  • 連接池
  • BigInt 支持
  • SASL 認證支持(SCRAM-SHA-256)、MD5 和明文
  • 連接超時
  • 返回行作為數據對象、數組數組或 Buffer
  • 二進制協議支持使其更快
  • TLS 支持(和認證模式)
  • 使用環境變量自動配置

數據庫支持

Bun.SQL 為多個數據庫系統提供統一的 API:

PostgreSQL

當出現以下情況時使用 PostgreSQL:

  • 連接字符串不匹配 SQLite 或 MySQL 模式(它是回退適配器)
  • 連接字符串顯式使用 postgres://postgresql:// 協議
  • 未提供連接字符串且環境變量指向 PostgreSQL
ts
import { sql } from "bun";
// 如果 DATABASE_URL 未設置或是 PostgreSQL URL 則使用 PostgreSQL
await sql`SELECT ...`;

import { SQL } from "bun";
const pg = new SQL("postgres://user:pass@localhost:5432/mydb");
await pg`SELECT ...`;

MySQL

MySQL 支持內置於 Bun.SQL,提供相同的標記模板字面量接口,與 MySQL 5.7+ 和 MySQL 8.0+ 完全兼容:

ts
import { SQL } from "bun";

// MySQL 連接
const mysql = new SQL("mysql://user:password@localhost:3306/database");
const mysql2 = new SQL("mysql2://user:password@localhost:3306/database"); // mysql2 協議也適用

// 使用選項對象
const mysql3 = new SQL({
  adapter: "mysql",
  hostname: "localhost",
  port: 3306,
  database: "myapp",
  username: "dbuser",
  password: "secretpass",
});

// 使用參數 - 自動使用預准備語句
const users = await mysql`SELECT * FROM users WHERE id = ${userId}`;

// 事務與 PostgreSQL 工作方式相同
await mysql.begin(async tx => {
  await tx`INSERT INTO users (name) VALUES (${"Alice"})`;
  await tx`UPDATE accounts SET balance = balance - 100 WHERE user_id = ${userId}`;
});

// 批量插入
const newUsers = [
  { name: "Alice", email: "alice@example.com" },
  { name: "Bob", email: "bob@example.com" },
];
await mysql`INSERT INTO users ${mysql(newUsers)}`;

MySQL 連接字符串格式

MySQL 接受各種 URL 格式作為連接字符串:

ts
// 標准 mysql:// 協議
new SQL("mysql://user:pass@localhost:3306/database");
new SQL("mysql://user:pass@localhost/database"); // 默認端口 3306

// mysql2:// 協議(與 mysql2 npm 包兼容)
new SQL("mysql2://user:pass@localhost:3306/database");

// 帶查詢參數
new SQL("mysql://user:pass@localhost/db?ssl=true");

// Unix socket 連接
new SQL("mysql://user:pass@/database?socket=/var/run/mysqld/mysqld.sock");

MySQL 特定功能

MySQL 數據庫支持:

  • 預准備語句:自動為參數化查詢創建,帶語句緩存
  • 二進制協議:為預准備語句和准確的類型處理提供更好的性能
  • 多結果集:支持存儲過程返回多個結果集
  • 認證插件:支持 mysql_native_password、caching_sha2_password(MySQL 8.0 默認)和 sha256_password
  • SSL/TLS 連接:與 PostgreSQL 類似的可配置 SSL 模式
  • 連接屬性:客戶端信息發送到服務器用於監控
  • 查詢流水線:執行多個預准備語句而無需等待響應

SQLite

SQLite 支持內置於 Bun.SQL,提供相同的標記模板字面量接口:

ts
import { SQL } from "bun";

// 內存數據庫
const memory = new SQL(":memory:");
const memory2 = new SQL("sqlite://:memory:");

// 基於文件的數據庫
const sql1 = new SQL("sqlite://myapp.db");

// 使用選項對象
const sql2 = new SQL({
  adapter: "sqlite",
  filename: "./data/app.db",
});

// 對於簡單的文件名,顯式指定適配器
const sql3 = new SQL("myapp.db", { adapter: "sqlite" });

SQLite 連接字符串格式

SQLite 接受各種 URL 格式作為連接字符串:

ts
// 標准 sqlite:// 協議
new SQL("sqlite://path/to/database.db");
new SQL("sqlite:path/to/database.db"); // 不帶斜槓

// file:// 協議(也被識別為 SQLite)
new SQL("file://path/to/database.db");
new SQL("file:path/to/database.db");

// 特殊 :memory: 數據庫
new SQL(":memory:");
new SQL("sqlite://:memory:");
new SQL("file://:memory:");

// 相對和絕對路徑
new SQL("sqlite://./local.db"); // 相對於當前目錄
new SQL("sqlite://../parent/db.db"); // 父目錄
new SQL("sqlite:///absolute/path.db"); // 絕對路徑

// 帶查詢參數
new SQL("sqlite://data.db?mode=ro"); // 只讀模式
new SQL("sqlite://data.db?mode=rw"); // 讀寫模式(不創建)
new SQL("sqlite://data.db?mode=rwc"); // 讀寫創建模式(默認)

NOTE

不帶協議的簡單文件名(如 `"myapp.db"`)需要顯式指定 `{ adapter: "sqlite" }` 以避免與 PostgreSQL 混淆。

SQLite 特定選項

SQLite 數據庫支持額外的配置選項:

ts
const sql = new SQL({
  adapter: "sqlite",
  filename: "app.db",

  // SQLite 特定選項
  readonly: false, // 以只讀模式打開
  create: true, // 如果數據庫不存在則創建
  readwrite: true, // 以讀寫模式打開

  // 額外的 Bun:sqlite 選項
  strict: true, // 啟用嚴格模式
  safeIntegers: false, // 對整數使用 JavaScript 數字
});

URL 中的查詢參數被解析以設置這些選項:

  • ?mode=roreadonly: true
  • ?mode=rwreadonly: false, create: false
  • ?mode=rwcreadonly: false, create: true(默認)

插入數據

你可以直接將 JavaScript 值傳遞給 SQL 模板字面量,轉義將為你處理。

ts
import { sql } from "bun";

// 使用直接值的基本插入
const [user] = await sql`
  INSERT INTO users (name, email) 
  VALUES (${name}, ${email})
  RETURNING *
`;

// 使用對象輔助器獲得更簡潔的語法
const userData = {
  name: "Alice",
  email: "alice@example.com",
};

const [newUser] = await sql`
  INSERT INTO users ${sql(userData)}
  RETURNING *
`;
// 展開為:INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com')

批量插入

你也可以將對象數組傳遞給 SQL 模板字面量,它將被展開為 INSERT INTO ... VALUES ... 語句。

ts
const users = [
  { name: "Alice", email: "alice@example.com" },
  { name: "Bob", email: "bob@example.com" },
  { name: "Charlie", email: "charlie@example.com" },
];

await sql`INSERT INTO users ${sql(users)}`;

選擇要插入的列

你可以使用 sql(object, ...string) 來選擇要插入的列。每個列必須在對象上定義。

ts
const user = {
  name: "Alice",
  email: "alice@example.com",
  age: 25,
};

await sql`INSERT INTO users ${sql(user, "name", "email")}`;
// 僅插入 name 和 email 列,忽略其他字段

查詢結果

默認情況下,Bun 的 SQL 客戶端將查詢結果返回為對象數組,其中每個對象代表一行,列名作為鍵。但是,在某些情況下,你可能希望以不同格式獲取數據。客戶端為此提供了兩種額外方法。

sql``.values() 格式

sql``.values() 方法將行作為值數組而不是對象返回。每行變成一個數組,其中值的順序與查詢中的列順序相同。

ts
const rows = await sql`SELECT * FROM users`.values();
console.log(rows);

這返回類似以下內容:

ts
[
  ["Alice", "alice@example.com"],
  ["Bob", "bob@example.com"],
];

sql``.values() 特別有用,如果查詢結果中返回重復列名。當使用對象(默認)時,最後一個列名用作對象中的鍵,這意味著重復列名會相互覆蓋——但當使用 sql``.values() 時,每列都存在於數組中,因此你可以通過索引訪問重復列的值。

sql``.raw() 格式

.raw() 方法將行作為 Buffer 對象數組返回。這對於處理二進制數據或出於性能原因可能有用。

ts
const rows = await sql`SELECT * FROM users`.raw();
console.log(rows); // [[Buffer, Buffer], [Buffer, Buffer], [Buffer, Buffer]]

SQL 片段

數據庫應用程序中的一個常見需求是根據運行時條件動態構建查詢。Bun 提供安全的方法來實現這一點,而不會冒 SQL 注入風險。

動態表名

當你需要動態引用表或模式時,使用 sql() 輔助器確保正確轉義:

ts
// 安全地動態引用表
await sql`SELECT * FROM ${sql("users")}`;

// 帶模式限定
await sql`SELECT * FROM ${sql("public.users")}`;

條件查詢

你可以使用 sql() 輔助器構建帶有條件子句的查詢。這允許你創建適應應用程序需求的靈活查詢:

ts
// 可選 WHERE 子句
const filterAge = true;
const minAge = 21;
const ageFilter = sql`AND age > ${minAge}`;
await sql`
  SELECT * FROM users
  WHERE active = ${true}
  ${filterAge ? ageFilter : sql``}
`;

更新中的動態列

你可以使用 sql(object, ...string) 來選擇要更新的列。每個列必須在對象上定義。如果未通知列,將使用所有鍵來更新行。

ts
await sql`UPDATE users SET ${sql(user, "name", "email")} WHERE id = ${user.id}`;
// 使用對象中的所有鍵來更新行
await sql`UPDATE users SET ${sql(user)} WHERE id = ${user.id}`;

動態值和 where in

值列表也可以動態創建,使 where in 查詢也變得簡單。你可以選擇傳遞對象數組並指定使用哪個鍵來創建列表。

ts
await sql`SELECT * FROM users WHERE id IN ${sql([1, 2, 3])}`;

const users = [
  { id: 1, name: "Alice" },
  { id: 2, name: "Bob" },
  { id: 3, name: "Charlie" },
];
await sql`SELECT * FROM users WHERE id IN ${sql(users, "id")}`;

sql.array 輔助器

sql.array 輔助器從 JavaScript 數組創建 PostgreSQL 數組字面量:

ts
// 為 PostgreSQL 創建數組字面量
await sql`INSERT INTO tags (items) VALUES (${sql.array(["red", "blue", "green"])})`;
// 生成:INSERT INTO tags (items) VALUES (ARRAY['red', 'blue', 'green'])

// 也適用於數字數組
await sql`SELECT * FROM products WHERE ids = ANY(${sql.array([1, 2, 3])})`;
// 生成:SELECT * FROM products WHERE ids = ANY(ARRAY[1, 2, 3])

NOTE

`sql.array` 僅適用於 PostgreSQL。多維數組和 NULL 元素可能尚不支持。

sql``.simple()

PostgreSQL 線路協議支持兩種類型的查詢:"simple"和"extended"。簡單查詢可以包含多個語句但不支持參數,而擴展查詢(默認)支持參數但只允許一個語句。

要在單個查詢中運行多個語句,使用 sql``.simple()

ts
// 一個查詢中的多個語句
await sql`
  SELECT 1;
  SELECT 2;
`.simple();

簡單查詢通常對數據庫遷移和設置腳本有用。

注意,簡單查詢不能使用參數(${value})。如果你需要參數,你必須將查詢拆分為單獨的語句。

文件中的查詢

你可以使用 sql.file 方法從文件讀取查詢並執行它,如果文件包含 $1, $2 等,你可以傳遞參數給查詢。如果未使用參數,每個文件可以執行多個命令。

ts
const result = await sql.file("query.sql", [1, 2, 3]);

不安全查詢

你可以使用 sql.unsafe 函數執行原始 SQL 字符串。謹慎使用,因為它不會轉義用戶輸入。如果未使用參數,每個查詢允許執行多個命令。

ts
// 不帶參數的多個命令
const result = await sql.unsafe(`
  SELECT ${userColumns} FROM users;
  SELECT ${accountColumns} FROM accounts;
`);

// 使用參數(只允許一個命令)
const result = await sql.unsafe("SELECT " + dangerous + " FROM users WHERE id = $1", [id]);

執行和取消查詢

Bun 的 SQL 是惰性的,這意味著它只會在被 await 或使用 .execute() 執行時才開始執行。 你可以通過調用查詢對象上的 cancel() 方法來取消當前正在執行的查詢。

ts
const query = sql`SELECT * FROM users`.execute();
setTimeout(() => query.cancel(), 100);
await query;

數據庫環境變量

sql 連接參數可以使用環境變量配置。客戶端按特定優先級順序檢查這些變量,並基於連接字符串格式自動檢測數據庫類型。

自動數據庫檢測

當不帶參數使用 Bun.sql() 或帶連接字符串使用 new SQL() 時,適配器基於 URL 格式自動檢測:

MySQL 自動檢測

當連接字符串匹配以下模式時自動選擇 MySQL:

  • mysql://... - MySQL 協議 URL
  • mysql2://... - MySQL2 協議 URL(兼容別名)
ts
// 這些都自動使用 MySQL(不需要適配器)
const sql1 = new SQL("mysql://user:pass@localhost/mydb");
const sql2 = new SQL("mysql2://user:pass@localhost:3306/mydb");

// 適用於 DATABASE_URL 環境變量
DATABASE_URL="mysql://user:pass@localhost/mydb" bun run app.js
DATABASE_URL="mysql2://user:pass@localhost:3306/mydb" bun run app.js

SQLite 自動檢測

當連接字符串匹配以下模式時自動選擇 SQLite:

  • :memory: - 內存數據庫
  • sqlite://... - SQLite 協議 URL
  • sqlite:... - 不帶斜槓的 SQLite 協議
  • file://... - 文件協議 URL
  • file:... - 不帶斜槓的文件協議
ts
// 這些都自動使用 SQLite(不需要適配器)
const sql1 = new SQL(":memory:");
const sql2 = new SQL("sqlite://app.db");
const sql3 = new SQL("file://./database.db");

// 適用於 DATABASE_URL 環境變量
DATABASE_URL=":memory:" bun run app.js
DATABASE_URL="sqlite://myapp.db" bun run app.js
DATABASE_URL="file://./data/app.db" bun run app.js

PostgreSQL 自動檢測

對於不匹配 MySQL 或 SQLite 模式的連接字符串,PostgreSQL 是默認:

bash
# 為這些模式檢測 PostgreSQL
DATABASE_URL="postgres://user:pass@localhost:5432/mydb" bun run app.js
DATABASE_URL="postgresql://user:pass@localhost:5432/mydb" bun run app.js

# 或不匹配 MySQL 或 SQLite 模式的任何 URL
DATABASE_URL="localhost:5432/mydb" bun run app.js

MySQL 環境變量

MySQL 連接可以通過環境變量配置:

bash
# 主連接 URL(首先檢查)
MYSQL_URL="mysql://user:pass@localhost:3306/mydb"

# 備選:帶 MySQL 協議的 DATABASE_URL
DATABASE_URL="mysql://user:pass@localhost:3306/mydb"
DATABASE_URL="mysql2://user:pass@localhost:3306/mydb"

如果未提供連接 URL,MySQL 檢查這些單獨參數:

環境變量默認值說明
MYSQL_HOSTlocalhost數據庫主機
MYSQL_PORT3306數據庫端口
MYSQL_USERroot數據庫用戶
MYSQL_PASSWORD(空)數據庫密碼
MYSQL_DATABASEmysql數據庫名稱
MYSQL_URL(空)MySQL 主連接 URL
TLS_MYSQL_DATABASE_URL(空)SSL/TLS 連接 URL

PostgreSQL 環境變量

以下環境變量可用於定義 PostgreSQL 連接:

環境變量說明
POSTGRES_URLPostgreSQL 主連接 URL
DATABASE_URL備選連接 URL(自動檢測)
PGURL備選連接 URL
PG_URL備選連接 URL
TLS_POSTGRES_DATABASE_URLSSL/TLS 連接 URL
TLS_DATABASE_URL備選 SSL/TLS 連接 URL

如果未提供連接 URL,系統檢查以下單獨參數:

環境變量回退變量默認值說明
PGHOST-localhost數據庫主機
PGPORT-5432數據庫端口
PGUSERNAMEPGUSER, USER, USERNAMEpostgres數據庫用戶
PGPASSWORD-(空)數據庫密碼
PGDATABASE-用戶名數據庫名稱

SQLite 環境變量

DATABASE_URL 包含 SQLite 兼容 URL 時,可以通過它配置 SQLite 連接:

bash
# 這些都被識別為 SQLite
DATABASE_URL=":memory:"
DATABASE_URL="sqlite://./app.db"
DATABASE_URL="file:///absolute/path/to/db.sqlite"

注意: 使用 SQLite 時,PostgreSQL 特定環境變量(POSTGRES_URLPGHOST 等)被忽略。


運行時預連接

Bun 可以在啟動時預連接到 PostgreSQL,通過在應用程序代碼運行之前建立數據庫連接來提高性能。這對於減少第一個數據庫查詢的連接延遲很有用。

bash
# 啟用 PostgreSQL 預連接
bun --sql-preconnect index.js

# 適用於 DATABASE_URL 環境變量
DATABASE_URL=postgres://user:pass@localhost:5432/db bun --sql-preconnect index.js

# 可以與其他運行時標志組合
bun --sql-preconnect --hot index.js

--sql-preconnect 標志將在啟動時使用配置的環境變量自動建立 PostgreSQL 連接。如果連接失敗,它不會使應用程序崩潰——錯誤將被優雅地處理。


連接選項

你可以通過傳遞選項給 SQL 構造函數來手動配置數據庫連接。選項根據數據庫適配器而異:

MySQL 選項

ts
import { SQL } from "bun";

const sql = new SQL({
  // 使用選項對象時 MySQL 必需
  adapter: "mysql",

  // 連接詳情
  hostname: "localhost",
  port: 3306,
  database: "myapp",
  username: "dbuser",
  password: "secretpass",

  // Unix socket 連接(hostname/port 的替代)
  // socket: "/var/run/mysqld/mysqld.sock",

  // 連接池設置
  max: 20, // 池中最大連接數(默認:10)
  idleTimeout: 30, // 30 秒後關閉空閒連接
  maxLifetime: 0, // 連接生命周期秒數(0 = 永久)
  connectionTimeout: 30, // 建立新連接時的超時

  // SSL/TLS 選項
  ssl: "prefer", // 或 "disable", "require", "verify-ca", "verify-full"
  // tls: {
  //   rejectUnauthorized: true,
  //   ca: "path/to/ca.pem",
  //   key: "path/to/key.pem",
  //   cert: "path/to/cert.pem",
  // },

  // 回調
  onconnect: client => {
    console.log("已連接到 MySQL");
  },
  onclose: (client, err) => {
    if (err) {
      console.error("MySQL 連接錯誤:", err);
    } else {
      console.log("MySQL 連接已關閉");
    }
  },
});

PostgreSQL 選項

ts
import { SQL } from "bun";

const sql = new SQL({
  // 連接詳情(適配器自動檢測為 PostgreSQL)
  url: "postgres://user:pass@localhost:5432/dbname",

  // 備選連接參數
  hostname: "localhost",
  port: 5432,
  database: "myapp",
  username: "dbuser",
  password: "secretpass",

  // 連接池設置
  max: 20, // 池中最大連接數
  idleTimeout: 30, // 30 秒後關閉空閒連接
  maxLifetime: 0, // 連接生命周期秒數(0 = 永久)
  connectionTimeout: 30, // 建立新連接時的超時

  // SSL/TLS 選項
  tls: true,
  // tls: {
  //   rejectUnauthorized: true,
  //   requestCert: true,
  //   ca: "path/to/ca.pem",
  //   key: "path/to/key.pem",
  //   cert: "path/to/cert.pem",
  //   checkServerIdentity(hostname, cert) {
  //     ...
  //   },
  // },

  // 回調
  onconnect: client => {
    console.log("已連接到 PostgreSQL");
  },
  onclose: client => {
    console.log("PostgreSQL 連接已關閉");
  },
});

SQLite 選項

ts
import { SQL } from "bun";

const sql = new SQL({
  // SQLite 必需
  adapter: "sqlite",
  filename: "./data/app.db", // 或 ":memory:" 用於內存數據庫

  // SQLite 特定訪問模式
  readonly: false, // 以只讀模式打開
  create: true, // 如果數據庫不存在則創建
  readwrite: true, // 允許讀寫操作

  // SQLite 數據處理
  strict: true, // 啟用嚴格模式以獲得更好的類型安全
  safeIntegers: false, // 對超出 JS 數字范圍的整數使用 BigInt

  // 回調
  onconnect: client => {
    console.log("SQLite 數據庫已打開");
  },
  onclose: client => {
    console.log("SQLite 數據庫已關閉");
  },
});

SQLite 連接說明

  • 連接池:SQLite 不使用連接池,因為它是基於文件的數據庫。每個 SQL 實例代表單個連接。
  • 事務:SQLite 通過保存點支持嵌套事務,與 PostgreSQL 類似。
  • 並發訪問:SQLite 通過文件鎖處理並發訪問。使用 WAL 模式獲得更好的並發性。
  • 內存數據庫:使用 :memory: 創建僅在連接生命周期內存在的臨時數據庫。

動態密碼

當客戶端需要使用替代認證方案(如訪問令牌或連接到密碼輪換的數據庫)時,提供同步或異步函數,在連接時解析動態密碼值。

ts
import { SQL } from "bun";

const sql = new SQL(url, {
  // 其他連接配置
  ...
  // 數據庫用戶的密碼函數
  password: async () => await signer.getAuthToken(),
});

SQLite 特定功能

查詢執行

SQLite 同步執行查詢,與使用異步 I/O 的 PostgreSQL 不同。但是,API 保持一致使用 Promise:

ts
const sqlite = new SQL("sqlite://app.db");

// 與 PostgreSQL 工作方式相同,但在底層同步執行
const users = await sqlite`SELECT * FROM users`;

// 參數工作方式相同
const user = await sqlite`SELECT * FROM users WHERE id = ${userId}`;

SQLite Pragmas

你可以使用 PRAGMA 語句配置 SQLite 行為:

ts
const sqlite = new SQL("sqlite://app.db");

// 啟用外鍵
await sqlite`PRAGMA foreign_keys = ON`;

// 將日志模式設置為 WAL 以獲得更好的並發性
await sqlite`PRAGMA journal_mode = WAL`;

// 檢查完整性
const integrity = await sqlite`PRAGMA integrity_check`;

數據類型差異

SQLite 具有比 PostgreSQL 更靈活的類型系統:

ts
// SQLite 將數據存儲在 5 個存儲類中:NULL、INTEGER、REAL、TEXT、BLOB
const sqlite = new SQL("sqlite://app.db");

// SQLite 對類型更寬松
await sqlite`
  CREATE TABLE flexible (
    id INTEGER PRIMARY KEY,
    data TEXT,        -- 可以將數字存儲為字符串
    value NUMERIC,    -- 可以存儲整數、實數或文本
    blob BLOB         -- 二進制數據
  )
`;

// JavaScript 值自動轉換
await sqlite`INSERT INTO flexible VALUES (${1}, ${"text"}, ${123.45}, ${Buffer.from("binary")})`;

事務

要開始新事務,使用 sql.begin。此方法適用於 PostgreSQL 和 SQLite。對於 PostgreSQL,它從池中保留專用連接。對於 SQLite,它在單個連接上開始事務。

BEGIN 命令自動發送,包括你指定的任何可選配置。如果事務期間發生錯誤,將觸發 ROLLBACK 以確保進程順利繼續。

基本事務

ts
await sql.begin(async tx => {
  // 此函數中的所有查詢都在事務中運行
  await tx`INSERT INTO users (name) VALUES (${"Alice"})`;
  await tx`UPDATE accounts SET balance = balance - 100 WHERE user_id = 1`;

  // 如果沒有拋出錯誤,事務自動提交
  // 如果發生任何錯誤則回滾
});

如果需要,也可以通過從回調函數返回帶查詢的數組來流水線化事務中的請求:

ts
await sql.begin(async tx => {
  return [
    tx`INSERT INTO users (name) VALUES (${"Alice"})`,
    tx`UPDATE accounts SET balance = balance - 100 WHERE user_id = 1`,
  ];
});

保存點

SQL 中的保存點在事務內創建中間檢查點,允許部分回滾而不影響整個操作。它們在復雜事務中很有用,允許錯誤恢復和保持一致的結果。

ts
await sql.begin(async tx => {
  await tx`INSERT INTO users (name) VALUES (${"Alice"})`;

  await tx.savepoint(async sp => {
    // 這部分可以單獨回滾
    await sp`UPDATE users SET status = 'active'`;
    if (someCondition) {
      throw new Error("回滾到保存點");
    }
  });

  // 即使保存點回滾也繼續事務
  await tx`INSERT INTO audit_log (action) VALUES ('user_created')`;
});

分布式事務

兩階段提交(2PC)是一種分布式事務協議,其中第一階段協調者通過確保數據已寫入並准備提交來准備節點,而第二階段根據協調者的決定最終確定節點提交或回滾。此過程確保數據持久性和正確的鎖管理。

在 PostgreSQL 和 MySQL 中,分布式事務持久化超出其原始會話,允許特權用戶或協調者稍後提交或回滾它們。這支持健壯的分布式事務、恢復過程和管理操作。

每個數據庫系統以不同方式實現分布式事務:

PostgreSQL 通過預准備事務原生支持它們,而 MySQL 使用 XA 事務。

如果分布式事務期間發生任何異常且未被捕獲,系統將自動回滾所有更改。當一切正常進行時,你保留稍後提交或回滾事務的靈活性。

ts
// 開始分布式事務
await sql.beginDistributed("tx1", async tx => {
  await tx`INSERT INTO users (name) VALUES (${"Alice"})`;
});

// 稍後,提交或回滾
await sql.commitDistributed("tx1");
// 或
await sql.rollbackDistributed("tx1");

認證

Bun 支持 SCRAM-SHA-256(SASL)、MD5 和明文認證。推薦 SASL 以獲得更好的安全性。查看 Postgres SASL 認證 獲取更多信息。

SSL 模式概述

PostgreSQL 支持不同的 SSL/TLS 模式來控制如何建立安全連接。這些模式決定連接時的行為和執行的證書驗證級別。

ts
const sql = new SQL({
  hostname: "localhost",
  username: "user",
  password: "password",
  ssl: "disable", // | "prefer" | "require" | "verify-ca" | "verify-full"
});
SSL 模式說明
disable不使用 SSL/TLS。如果服務器需要 SSL 則連接失敗。
prefer首先嘗試 SSL,如果 SSL 失敗則回退到非 SSL。如果未指定則為默認模式。
require需要 SSL 但不驗證證書。如果無法建立 SSL 則失敗。
verify-ca驗證服務器證書由受信任的 CA 簽名。如果驗證失敗則失敗。
verify-full最安全的模式。驗證證書和主機名匹配。防止不受信任的證書和 MITM 攻擊。

與連接字符串一起使用

SSL 模式也可以在連接字符串中指定:

ts
// 使用 prefer 模式
const sql = new SQL("postgres://user:password@localhost/mydb?sslmode=prefer");

// 使用 verify-full 模式
const sql = new SQL("postgres://user:password@localhost/mydb?sslmode=verify-full");

連接池

Bun 的 SQL 客戶端自動管理連接池,這是一個用於多個查詢重用的數據庫連接池。這有助於減少為每個查詢建立和關閉連接的開銷,還有助於管理到數據庫的並發連接數。

ts
const sql = new SQL({
  // 池配置
  max: 20, // 最大 20 個並發連接
  idleTimeout: 30, // 30 秒後關閉空閒連接
  maxLifetime: 3600, // 最大連接生命周期 1 小時
  connectionTimeout: 10, // 連接超時 10 秒
});

在發出查詢之前不會建立連接。

ts
const sql = Bun.SQL(); // 不創建連接

await sql`...`; // 池啟動直到達到最大值(如果可能),使用第一個可用連接
await sql`...`; // 重用之前的連接

// 現在同時使用兩個連接
await Promise.all([
  sql`INSERT INTO users ${sql({ name: "Alice" })}`,
  sql`UPDATE users SET name = ${user.name} WHERE id = ${user.id}`,
]);

await sql.close(); // 等待所有查詢完成並關閉池中所有連接
await sql.close({ timeout: 5 }); // 等待 5 秒並關閉池中所有連接
await sql.close({ timeout: 0 }); // 立即關閉池中所有連接

保留連接

Bun 允許你從池中保留連接,並返回包裝單個連接的客戶端。這可用於在隔離連接上運行查詢。

ts
// 從池獲取獨佔連接
const reserved = await sql.reserve();

try {
  await reserved`INSERT INTO users (name) VALUES (${"Alice"})`;
} finally {
  // 重要:將連接釋放回池
  reserved.release();
}

// 或使用 Symbol.dispose
{
  using reserved = await sql.reserve();
  await reserved`SELECT 1`;
} // 自動釋放

預准備語句

默認情況下,Bun 的 SQL 客戶端自動為可以推斷查詢是靜態的查詢創建命名預准備語句。這提供更好的性能。但是,你可以通過在連接選項中設置 prepare: false 來更改此行為:

ts
const sql = new SQL({
  // ... 其他選項 ...
  prepare: false, // 禁用服務器上持久化的命名預准備語句
});

當設置 prepare: false 時:

查詢仍然使用"extended"協議執行,但它們使用 未命名預准備語句 執行,未命名預准備語句僅持續到下一個指定未命名語句為目標的 Parse 語句發出。

  • 參數綁定仍然安全防止 SQL 注入
  • 每個查詢由服務器從頭開始解析和計劃
  • 查詢不會 流水線化

你可能希望使用 prepare: false 當:

  • 在事務模式下使用 PGBouncer(盡管自 PGBouncer 1.21.0 起,當正確配置時支持協議級命名預准備語句)
  • 調試查詢執行計劃
  • 使用需要頻繁重新生成查詢計劃的動態 SQL
  • 每個查詢不支持多個命令(除非你使用 sql``.simple()

注意,禁用預准備語句可能會影響頻繁使用不同參數執行的查詢的性能,因為服務器需要從頭開始解析和計劃每個查詢。


錯誤處理

客戶端為不同的失敗場景提供類型化錯誤。錯誤是特定於數據庫的並擴展自基本錯誤類:

錯誤類

ts
import { SQL } from "bun";

try {
  await sql`SELECT * FROM users`;
} catch (error) {
  if (error instanceof SQL.PostgresError) {
    // PostgreSQL 特定錯誤
    console.log(error.code); // PostgreSQL 錯誤代碼
    console.log(error.detail); // 詳細錯誤消息
    console.log(error.hint); // PostgreSQL 的有用提示
  } else if (error instanceof SQL.SQLiteError) {
    // SQLite 特定錯誤
    console.log(error.code); // SQLite 錯誤代碼(例如 "SQLITE_CONSTRAINT")
    console.log(error.errno); // SQLite 錯誤號
    console.log(error.byteOffset); // SQL 語句中的字節偏移(如果可用)
  } else if (error instanceof SQL.SQLError) {
    // 通用 SQL 錯誤(基類)
    console.log(error.message);
  }
}

PostgreSQL 特定錯誤代碼

PostgreSQL 連接錯誤

連接錯誤說明
ERR_POSTGRES_CONNECTION_CLOSED連接被終止或從未建立
ERR_POSTGRES_CONNECTION_TIMEOUT在超時期限內無法建立連接
ERR_POSTGRES_IDLE_TIMEOUT連接因不活動而關閉
ERR_POSTGRES_LIFETIME_TIMEOUT連接超過最大生命周期
ERR_POSTGRES_TLS_NOT_AVAILABLESSL/TLS 連接不可用
ERR_POSTGRES_TLS_UPGRADE_FAILED無法將連接升級到 SSL/TLS

認證錯誤

認證錯誤說明
ERR_POSTGRES_AUTHENTICATION_FAILED_PBKDF2密碼認證失敗
ERR_POSTGRES_UNKNOWN_AUTHENTICATION_METHOD服務器請求未知認證方法
ERR_POSTGRES_UNSUPPORTED_AUTHENTICATION_METHOD服務器請求不支持的認證方法
ERR_POSTGRES_INVALID_SERVER_KEY認證期間服務器密鑰無效
ERR_POSTGRES_INVALID_SERVER_SIGNATURE服務器簽名無效
ERR_POSTGRES_SASL_SIGNATURE_INVALID_BASE64無效 SASL 簽名編碼
ERR_POSTGRES_SASL_SIGNATURE_MISMATCHSASL 簽名驗證失敗

查詢錯誤

查詢錯誤說明
ERR_POSTGRES_SYNTAX_ERROR無效 SQL 語法(擴展自 SyntaxError
ERR_POSTGRES_SERVER_ERRORPostgreSQL 服務器的一般錯誤
ERR_POSTGRES_INVALID_QUERY_BINDING無效參數綁定
ERR_POSTGRES_QUERY_CANCELLED查詢被取消
ERR_POSTGRES_NOT_TAGGED_CALL查詢調用時不帶標記調用

數據類型錯誤

數據類型錯誤說明
ERR_POSTGRES_INVALID_BINARY_DATA無效二進制數據格式
ERR_POSTGRES_INVALID_BYTE_SEQUENCE無效字節序列
ERR_POSTGRES_INVALID_BYTE_SEQUENCE_FOR_ENCODING編碼錯誤
ERR_POSTGRES_INVALID_CHARACTER數據中的無效字符
ERR_POSTGRES_OVERFLOW數字溢出
ERR_POSTGRES_UNSUPPORTED_BYTEA_FORMAT不支持的二進制格式
ERR_POSTGRES_UNSUPPORTED_INTEGER_SIZE不支持的整數大小
ERR_POSTGRES_MULTIDIMENSIONAL_ARRAY_NOT_SUPPORTED_YET尚不支持多維數組
ERR_POSTGRES_NULLS_IN_ARRAY_NOT_SUPPORTED_YET尚不支持數組中的 NULL 值

協議錯誤

協議錯誤說明
ERR_POSTGRES_EXPECTED_REQUEST期望客戶端請求
ERR_POSTGRES_EXPECTED_STATEMENT期望預准備語句
ERR_POSTGRES_INVALID_BACKEND_KEY_DATA無效後端密鑰數據
ERR_POSTGRES_INVALID_MESSAGE無效協議消息
ERR_POSTGRES_INVALID_MESSAGE_LENGTH無效消息長度
ERR_POSTGRES_UNEXPECTED_MESSAGE意外消息類型

事務錯誤

事務錯誤說明
ERR_POSTGRES_UNSAFE_TRANSACTION檢測到不安全的事務操作
ERR_POSTGRES_INVALID_TRANSACTION_STATE無效事務狀態

SQLite 特定錯誤

SQLite 錯誤提供與 SQLite 標准錯誤代碼對應的錯誤代碼和編號:

常見 SQLite 錯誤代碼

錯誤代碼errno說明
SQLITE_CONSTRAINT19約束違反(UNIQUE、CHECK、NOT NULL 等)
SQLITE_BUSY5數據庫被鎖定
SQLITE_LOCKED6數據庫中的表被鎖定
SQLITE_READONLY8嘗試寫入只讀數據庫
SQLITE_IOERR10磁盤 I/O 錯誤
SQLITE_CORRUPT11數據庫磁盤映像損壞
SQLITE_FULL13數據庫或磁盤已滿
SQLITE_CANTOPEN14無法打開數據庫文件
SQLITE_PROTOCOL15數據庫鎖協議錯誤
SQLITE_SCHEMA17數據庫模式已更改
SQLITE_TOOBIG18字符串或 BLOB 超出大小限制
SQLITE_MISMATCH20數據類型不匹配
SQLITE_MISUSE21庫使用不正確
SQLITE_AUTH23授權被拒絕

錯誤處理示例:

ts
const sqlite = new SQL("sqlite://app.db");

try {
  await sqlite`INSERT INTO users (id, name) VALUES (1, 'Alice')`;
  await sqlite`INSERT INTO users (id, name) VALUES (1, 'Bob')`; // 重復 ID
} catch (error) {
  if (error instanceof SQL.SQLiteError) {
    if (error.code === "SQLITE_CONSTRAINT") {
      console.log("約束違反:", error.message);
      // 處理唯一約束違反
    }
  }
}

數字和 BigInt

Bun 的 SQL 客戶端包括對超出 53 位整數范圍的大數字的特殊處理。工作原理如下:

ts
import { sql } from "bun";

const [{ x, y }] = await sql`SELECT 9223372036854777 as x, 12345 as y`;

console.log(typeof x, x); // "string" "9223372036854777"
console.log(typeof y, y); // "number" 12345

使用 BigInt 而不是字符串

如果你需要大數字作為 BigInt 而不是字符串,你可以通過在初始化 SQL 客戶端時將 bigint 選項設置為 true 來啟用:

ts
const sql = new SQL({
  bigint: true,
});

const [{ x }] = await sql`SELECT 9223372036854777 as x`;

console.log(typeof x, x); // "bigint" 9223372036854777n

路線圖

還有一些事情我們尚未完成。

  • 通過 --db-preconnect Bun CLI 標志進行連接預加載
  • 列名轉換(例如 snake_casecamelCase)。這主要受限於使用 WebKit 的 WTF::String 在 C++ 中更改大小寫的 unicode 感知實現。
  • 列類型轉換

數據庫特定功能

認證方法

MySQL 支持多種自動協商的認證插件:

  • mysql_native_password - 傳統 MySQL 認證,廣泛兼容
  • caching_sha2_password - MySQL 8.0+ 默認,更安全帶 RSA 密鑰交換
  • sha256_password - 基於 SHA-256 的認證

客戶端自動處理服務器請求時的認證插件切換,包括非 SSL 連接上的安全密碼交換。

預准備語句和性能

MySQL 對所有參數化查詢使用服務器端預准備語句:

ts
// 這自動在服務器上創建預准備語句
const user = await mysql`SELECT * FROM users WHERE id = ${userId}`;

// 預准備語句被緩存並重復用於相同查詢
for (const id of userIds) {
  // 相同的預准備語句被重用
  await mysql`SELECT * FROM users WHERE id = ${id}`;
}

// 查詢流水線 - 多個語句發送而無需等待
const [users, orders, products] = await Promise.all([
  mysql`SELECT * FROM users WHERE active = ${true}`,
  mysql`SELECT * FROM orders WHERE status = ${"pending"}`,
  mysql`SELECT * FROM products WHERE in_stock = ${true}`,
]);

多結果集

MySQL 可以從多語句查詢返回多個結果集:

ts
const mysql = new SQL("mysql://user:pass@localhost/mydb");

// 使用 simple() 方法的多語句查詢
const multiResults = await mysql`
  SELECT * FROM users WHERE id = 1;
  SELECT * FROM orders WHERE user_id = 1;
`.simple();

字符集和排序規則

Bun.SQL 自動為 MySQL 連接使用 utf8mb4 字符集,確保完整的 Unicode 支持包括表情符號。這是現代 MySQL 應用程序推薦的字符集。

連接屬性

Bun 自動發送客戶端信息到 MySQL 以更好地監控:

ts
// 這些屬性自動發送:
// _client_name: "Bun"
// _client_version: <bun 版本>
// 你可以在 MySQL 的 performance_schema.session_connect_attrs 中看到這些

類型處理

MySQL 類型自動轉換為 JavaScript 類型:

MySQL 類型JavaScript 類型說明
INT, TINYINT, MEDIUMINTnumber在安全整數范圍內
BIGINTstring, number 或 BigInt如果值適合 i32/u32 大小則為 number,否則為 string 或 BigInt,基於 bigint 選項
DECIMAL, NUMERICstring保留精度
FLOAT, DOUBLEnumber
DATEDateJavaScript Date 對象
DATETIME, TIMESTAMPDate帶時區處理
TIMEnumber微秒總數
YEARnumber
CHAR, VARCHAR, VARSTRING, STRINGstring
TINY TEXT, MEDIUM TEXT, TEXT, LONG TEXTstring
TINY BLOB, MEDIUM BLOB, BLOG, LONG BLOBstringBLOB 類型是 TEXT 類型的別名
JSONobject/array自動解析
BIT(1)booleanMySQL 中的 BIT(1)
GEOMETRYstring幾何數據

與 PostgreSQL 的差異

雖然 API 是統一的,但有一些行為差異:

  1. 參數佔位符:MySQL 內部使用 ? 但 Bun 自動轉換 $1, $2 樣式
  2. RETURNING 子句:MySQL 不支持 RETURNING;使用 result.lastInsertRowid 或單獨的 SELECT
  3. 數組類型:MySQL 沒有像 PostgreSQL 那樣的原生數組類型

MySQL 特定功能

我們尚未實現 LOAD DATA INFILE 支持

PostgreSQL 特定功能

我們尚未實現這些:

  • COPY 支持
  • LISTEN 支持
  • NOTIFY 支持

我們也尚未實現一些更不常見的功能:

  • GSSAPI 認證
  • SCRAM-SHA-256-PLUS 支持
  • Point 和 PostGIS 類型
  • 所有多維整數數組類型(僅支持少數類型)

常見模式和最佳實踐

使用 MySQL 結果集

ts
// INSERT 後獲取插入 ID
const result = await mysql`INSERT INTO users (name) VALUES (${"Alice"})`;
console.log(result.lastInsertRowid); // MySQL 的 LAST_INSERT_ID()

// 處理受影響的行
const updated = await mysql`UPDATE users SET active = ${false} WHERE age < ${18}`;
console.log(updated.affectedRows); // 更新的行數

// 使用 MySQL 特定函數
const now = await mysql`SELECT NOW() as current_time`;
const uuid = await mysql`SELECT UUID() as id`;

MySQL 錯誤處理

ts
try {
  await mysql`INSERT INTO users (email) VALUES (${"duplicate@email.com"})`;
} catch (error) {
  if (error.code === "ER_DUP_ENTRY") {
    console.log("檢測到重復條目");
  } else if (error.code === "ER_ACCESS_DENIED_ERROR") {
    console.log("訪問被拒絕");
  } else if (error.code === "ER_BAD_DB_ERROR") {
    console.log("數據庫不存在");
  }
  // MySQL 錯誤代碼與 mysql/mysql2 包兼容
}

MySQL 性能提示

  1. 使用連接池:根據你的工作負載設置適當的 max 池大小
  2. 啟用預准備語句:它們默認啟用並提高性能
  3. 對批量操作使用事務:在事務中分組相關查詢
  4. 正確索引:MySQL 嚴重依賴索引進行查詢性能
  5. 使用 utf8mb4 字符集:它默認設置並處理所有 Unicode 字符

常見問題

為什麼這是 `Bun.sql` 而不是 `Bun.postgres`?
	計劃是將來添加更多數據庫驅動。現在添加了 MySQL 支持,這個統一的 API 支持 PostgreSQL、MySQL 和 SQLite。
我如何知道使用哪個數據庫適配器?
	適配器從連接字符串自動檢測:

	- 以 `mysql://` 或 `mysql2://` 開頭的 URL 使用 MySQL
	- 匹配 SQLite 模式(`:memory:`、`sqlite://`、`file://`)的 URL 使用 SQLite
	- 其他所有內容默認為 PostgreSQL

支持 MySQL 存儲過程嗎?
	是的,存儲過程完全支持包括 OUT 參數和多個結果集:

	```ts
	// 調用存儲過程
	const results = await mysql`CALL GetUserStats(${userId}, @total_orders)`;

	// 獲取 OUT 參數
	const outParam = await mysql`SELECT @total_orders as total`;
	```

我可以使用 MySQL 特定的 SQL 語法嗎?
	是的,你可以使用任何 MySQL 特定語法:

	```ts
	// MySQL 特定語法工作正常
	await mysql`SET @user_id = ${userId}`;
	await mysql`SHOW TABLES`;
	await mysql`DESCRIBE users`;
	await mysql`EXPLAIN SELECT * FROM users WHERE id = ${id}`;
	```

為什麼不直接使用現有庫?

像 postgres.js、pg 和 node-postgres 這樣的 npm 包也可以在 Bun 中使用。它們是很好的選擇。

有兩個原因:

  1. 我們認為開發者有一個內置於 Bun 的數據庫驅動更簡單。你花在庫購物上的時間本可以用來構建你的應用程序。
  2. 我們利用一些 JavaScriptCore 引擎內部來使其更快地創建對象,這在庫中很難實現

致謝

非常感謝 @porsagerpostgres.js 為 API 接口提供的靈感。

Bun學習網由www.bunjs.com.cn整理維護