Bun

SQL

Bun 提供原生綁定,透過現代、基於 Promise 的 API 來使用 PostgreSQL 資料庫。此介面設計簡潔且高效能,使用標籤樣板字串來進行查詢,並提供連線池、交易和預先編譯語句等功能。

import { sql } from "bun";

const users = await sql`
  SELECT * FROM users
  WHERE active = ${true}
  LIMIT ${10}
`;

// Select with multiple conditions
const activeUsers = await sql`
  SELECT * 
  FROM users 
  WHERE active = ${true} 
  AND age >= ${18}
`;

功能特色

標籤樣板字串,以防止 SQL 注入

交易

具名與位置參數

連線池

BigInt 支援

SASL 驗證支援 (SCRAM-SHA-256)、MD5 和明文

連線逾時

以資料物件、陣列的陣列或 Buffer 形式傳回列

二進制協定支援使其速度更快

TLS 支援 (和驗證模式)

透過環境變數自動設定

插入資料

您可以將 JavaScript 值直接傳遞給 SQL 樣板字串,系統會為您處理跳脫字元。

import { sql } from "bun";

// Basic insert with direct values
const [user] = await sql`
  INSERT INTO users (name, email) 
  VALUES (${name}, ${email})
  RETURNING *
`;

// Using object helper for cleaner syntax
const userData = {
  name: "Alice",
  email: "alice@example.com",
};

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

大量插入

您也可以將物件陣列傳遞給 SQL 樣板字串,它將會展開為 INSERT INTO ... VALUES ... 陳述式。

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) 來選取要插入的欄位。每個欄位都必須在物件上定義。

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

await sql`INSERT INTO users ${sql(user, "name", "email")}`;
// Only inserts name and email columns, ignoring other fields

查詢結果

預設情況下,Bun 的 SQL 用戶端會將查詢結果以物件陣列的形式傳回,其中每個物件代表一列,欄位名稱作為鍵。但是,在某些情況下,您可能希望資料採用不同的格式。用戶端為此目的提供了兩種額外的方法。

sql``.values() 格式

sql``.values() 方法會將列以值陣列而非物件的形式傳回。每一列都會變成一個陣列,其中的值與查詢中的欄位順序相同。

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

這會傳回類似這樣的結果

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

如果查詢結果中傳回重複的欄位名稱,sql``.values() 特別有用。當使用物件 (預設值) 時,最後一個欄位名稱會用作物件中的鍵,這表示重複的欄位名稱會互相覆寫 — 但當使用 sql``.values() 時,每個欄位都會出現在陣列中,因此您可以透過索引存取重複欄位的值。

sql``.raw() 格式

.raw() 方法會將列以 Buffer 物件陣列的形式傳回。這對於處理二進制資料或基於效能考量可能很有用。

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

SQL 片段

資料庫應用程式中常見的需求是能夠根據執行階段條件動態建構查詢。Bun 提供了安全的方式來執行此操作,而不會有 SQL 注入的風險。

動態表格名稱

當您需要動態參考表格或結構描述時,請使用 sql() 輔助程式來確保正確的跳脫字元

// Safely reference tables dynamically
await sql`SELECT * FROM ${sql("users")}`;

// With schema qualification
await sql`SELECT * FROM ${sql("public.users")}`;

條件式查詢

您可以使用 sql() 輔助程式來建置具有條件子句的查詢。這讓您可以建立彈性的查詢,以適應您的應用程式需求

// Optional WHERE clauses
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) 來選取要更新的欄位。每個欄位都必須在物件上定義。如果未告知欄位,則所有鍵都將用於更新列。

await sql`UPDATE users SET ${sql(user, "name", "email")} WHERE id = ${user.id}`;
// uses all keys from the object to update the row
await sql`UPDATE users SET ${sql(user)} WHERE id = ${user.id}`;

動態值和 where in

值清單也可以動態建立,讓 where in 查詢也很簡單。您可以選擇性地傳遞物件陣列,並告知要使用哪個鍵來建立清單。

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``.simple()

PostgreSQL 線路協定支援兩種查詢類型:「簡單」和「擴充」。簡單查詢可以包含多個陳述式,但不支援參數,而擴充查詢 (預設值) 支援參數,但只允許一個陳述式。

若要在單一查詢中執行多個陳述式,請使用 sql``.simple()

// Multiple statements in one query
await sql`
  SELECT 1;
  SELECT 2;
`.simple();

簡單查詢通常適用於資料庫遷移和設定腳本。

請注意,簡單查詢無法使用參數 (${value})。如果您需要參數,則必須將查詢分割為個別的陳述式。

檔案中的查詢

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

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

不安全的查詢

您可以使用 sql.unsafe 函式來執行原始 SQL 字串。請謹慎使用此功能,因為它不會跳脫使用者輸入。如果未使用參數,則允許每個查詢執行多個命令。

// Multiple commands without parameters
const result = await sql.unsafe(`
  SELECT ${userColumns} FROM users;
  SELECT ${accountColumns} FROM accounts;
`);

// Using parameters (only one command is allowed)
const result = await sql.unsafe(
  "SELECT " + dangerous + " FROM users WHERE id = $1",
  [id],
);

什麼是 SQL 注入?

執行與取消查詢

Bun 的 SQL 是延遲載入的,這表示只有在等待或使用 .execute() 執行時才會開始執行。 您可以呼叫查詢物件上的 cancel() 方法來取消目前正在執行的查詢。

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

資料庫環境變數

sql 連線參數可以使用環境變數設定。用戶端會依特定的優先順序檢查這些變數。

以下環境變數可用於定義連線 URL

環境變數描述
POSTGRES_URLPostgreSQL 的主要連線 URL
DATABASE_URL替代連線 URL
PGURL替代連線 URL
PG_URL替代連線 URL
TLS_POSTGRES_DATABASE_URL啟用 SSL/TLS 的連線 URL
TLS_DATABASE_URL替代啟用 SSL/TLS 的連線 URL

如果未提供連線 URL,系統會檢查以下個別參數

環境變數後備變數預設值描述
PGHOST-localhost資料庫主機
PGPORT-5432資料庫埠
PGUSERNAMEPGUSERUSERUSERNAMEpostgres資料庫使用者
PGPASSWORD-(空白)資料庫密碼
PGDATABASE-username資料庫名稱

連線選項

您可以透過將選項傳遞至 SQL 建構函式來手動設定資料庫連線

import { SQL } from "bun";

const db = new SQL({
  // Required
  url: "postgres://user:pass@localhost:5432/dbname",

  // Optional configuration
  hostname: "localhost",
  port: 5432,
  database: "myapp",
  username: "dbuser",
  password: "secretpass",

  // Connection pool settings
  max: 20, // Maximum connections in pool
  idleTimeout: 30, // Close idle connections after 30s
  maxLifetime: 0, // Connection lifetime in seconds (0 = forever)
  connectionTimeout: 30, // Timeout when establishing new connections

  // SSL/TLS options
  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) {
  //     ...
  //   },
  // },

  // Callbacks
  onconnect: client => {
    console.log("Connected to database");
  },
  onclose: client => {
    console.log("Connection closed");
  },
});

動態密碼

當用戶端需要使用替代驗證方案 (例如存取權杖) 或連線至具有輪換密碼的資料庫時,請提供同步或非同步函式,該函式將在連線時解析動態密碼值。

import { SQL } from "bun";

const sql = new SQL(url, {
  // Other connection config
  ...
  // Password function for the database user
  password: async () => await signer.getAuthToken(),
});

交易

若要開始新的交易,請使用 sql.begin。此方法會為交易持續時間保留專用連線,並提供範圍限定的 sql 執行個體以在回呼函式中使用。回呼完成後,sql.begin 會解析為回呼的傳回值。

BEGIN 命令會自動傳送,包括您指定的任何選用設定。如果在交易期間發生錯誤,則會觸發 ROLLBACK 以釋放保留的連線並確保程序順利繼續。

基本交易

await sql.begin(async tx => {
  // All queries in this function run in a transaction
  await tx`INSERT INTO users (name) VALUES (${"Alice"})`;
  await tx`UPDATE accounts SET balance = balance - 100 WHERE user_id = 1`;

  // Transaction automatically commits if no errors are thrown
  // Rolls back if any error occurs
});

如果需要,也可以透過從回呼函式傳回包含查詢的陣列來在交易中管線化請求,如下所示

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 中的儲存點會在交易內建立中繼檢查點,從而能夠進行部分回滾,而不會影響整個作業。它們在複雜的交易中很有用,允許錯誤復原並維持一致的結果。

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

  await tx.savepoint(async sp => {
    // This part can be rolled back separately
    await sp`UPDATE users SET status = 'active'`;
    if (someCondition) {
      throw new Error("Rollback to savepoint");
    }
  });

  // Continue with transaction even if savepoint rolled back
  await tx`INSERT INTO audit_log (action) VALUES ('user_created')`;
});

分散式交易

兩階段提交 (2PC) 是一種分散式交易協定,其中階段 1 讓協調器準備節點,方法是確保資料已寫入且準備好提交,而階段 2 根據協調器的決策完成節點的提交或回滾。此程序可確保資料持久性和適當的鎖定管理。

在 PostgreSQL 和 MySQL 中,分散式交易會持續存在於其原始會話之外,允許具有權限的使用者或協調器稍後提交或回滾它們。這支援穩健的分散式交易、復原程序和管理作業。

每個資料庫系統以不同的方式實作分散式交易

PostgreSQL 透過預先編譯交易原生支援它們,而 MySQL 則使用 XA 交易。

如果在分散式交易期間發生任何未捕獲的例外狀況,系統將自動回滾所有變更。當一切正常進行時,您可以保持彈性,以便稍後提交或回滾交易。

// Begin a distributed transaction
await sql.beginDistributed("tx1", async tx => {
  await tx`INSERT INTO users (name) VALUES (${"Alice"})`;
});

// Later, commit or rollback
await sql.commitDistributed("tx1");
// or
await sql.rollbackDistributed("tx1");

驗證

Bun 支援 SCRAM-SHA-256 (SASL)、MD5 和明文驗證。建議使用 SASL 以獲得更好的安全性。請查看 Postgres SASL 驗證 以取得更多資訊。

SSL 模式概觀

PostgreSQL 支援不同的 SSL/TLS 模式,以控制如何建立安全連線。這些模式決定了連線時的行為以及執行的憑證驗證層級。

const sql = new SQL({
  hostname: "localhost",
  username: "user",
  password: "password",
  ssl: "disable", // | "prefer" | "require" | "verify-ca" | "verify-full"
});
SSL 模式描述
停用不使用 SSL/TLS。如果伺服器需要 SSL,則連線失敗。
偏好先嘗試 SSL,如果 SSL 失敗,則回復為非 SSL。如果未指定模式,則為預設模式。
要求需要 SSL,但不進行憑證驗證。如果無法建立 SSL,則失敗。
verify-ca驗證伺服器憑證是否由受信任的 CA 簽署。如果驗證失敗,則失敗。
verify-full最安全的模式。驗證憑證和主機名稱是否相符。防止不受信任的憑證和 MITM 攻擊。

搭配連線字串使用

SSL 模式也可以在連線字串中指定

// Using prefer mode
const sql = new SQL("postgres://user:password@localhost/mydb?sslmode=prefer");

// Using verify-full mode
const sql = new SQL(
  "postgres://user:password@localhost/mydb?sslmode=verify-full",
);

連線池

Bun 的 SQL 用戶端會自動管理連線池,連線池是資料庫連線的集合,可重複用於多個查詢。這有助於減少為每個查詢建立和關閉連線的額外負荷,並且還有助於管理與資料庫的並行連線數。

const db = new SQL({
  // Pool configuration
  max: 20, // Maximum 20 concurrent connections
  idleTimeout: 30, // Close idle connections after 30s
  maxLifetime: 3600, // Max connection lifetime 1 hour
  connectionTimeout: 10, // Connection timeout 10s
});

在建立查詢之前,不會建立連線。

const sql = Bun.sql(); // no connection are created

await sql`...`; // pool is started until max is reached (if possible), first available connection is used
await sql`...`; // previous connection is reused

// two connections are used now at the same time
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 all queries to finish and close all connections from the pool
await sql.close({ timeout: 5 }); // wait 5 seconds and close all connections from the pool
await sql.close({ timeout: 0 }); // close all connections from the pool immediately

保留連線

Bun 讓您可以從池中保留連線,並傳回包裝單一連線的用戶端。這可用於在隔離的連線上執行查詢。

// Get exclusive connection from pool
const reserved = await sql.reserve();

try {
  await reserved`INSERT INTO users (name) VALUES (${"Alice"})`;
} finally {
  // Important: Release connection back to pool
  reserved.release();
}

// Or using Symbol.dispose
{
  using reserved = await sql.reserve();
  await reserved`SELECT 1`;
} // Automatically released

預先編譯語句

預設情況下,Bun 的 SQL 用戶端會為可以推斷查詢為靜態的查詢自動建立具名的預先編譯語句。這提供了更好的效能。但是,您可以透過在連線選項中設定 prepare: false 來變更此行為

const sql = new SQL({
  // ... other options ...
  prepare: false, // Disable persisting named prepared statements on the server
});

當設定 prepare: false

查詢仍然使用「擴充」協定執行,但它們是使用 未命名的預先編譯語句 執行的,未命名的預先編譯語句僅持續到下一個指定未命名語句作為目的地的 Parse 陳述式發出為止。

  • 參數綁定仍然可以安全地防止 SQL 注入
  • 伺服器會從頭開始剖析和規劃每個查詢
  • 查詢將不會 管線化

在以下情況下,您可能想要使用 prepare: false

  • 在交易模式中使用 PGBouncer (雖然自 PGBouncer 1.21.0 起,在正確設定時支援協定層級的具名預先編譯語句)
  • 偵錯查詢執行計畫
  • 使用需要頻繁重新產生查詢計畫的動態 SQL
  • 每個查詢將不支援多個命令 (除非您使用 sql``.simple())

請注意,停用預先編譯語句可能會影響經常使用不同參數執行的查詢的效能,因為伺服器需要從頭開始剖析和規劃每個查詢。

錯誤處理

用戶端為不同的失敗情況提供類型化的錯誤

連線錯誤

連線錯誤描述
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_BASE64SASL 簽名編碼無效
ERR_POSTGRES_SASL_SIGNATURE_MISMATCHSASL 簽名驗證失敗

查詢錯誤

查詢錯誤描述
ERR_POSTGRES_SYNTAX_ERRORSQL 語法無效 (延伸 SyntaxError)
ERR_POSTGRES_SERVER_ERROR來自 PostgreSQL 伺服器的一般錯誤
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交易狀態無效

數字和 BigInt

Bun 的 SQL 用戶端包含針對超出 53 位元整數範圍的大數字的特殊處理。以下說明其運作方式

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 來啟用此功能

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

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

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

Roadmap

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

  • 透過 --db-preconnect Bun CLI 旗標預先載入連線
  • MySQL 支援:我們正在努力
  • SQLite 支援:已規劃,但尚未開始。理想情況下,我們會以原生方式實作它,而不是包裝 bun:sqlite
  • 欄位名稱轉換 (例如 snake_casecamelCase)。這主要受到 Unicode 感知的實作的阻礙,該實作使用 WebKit 的 WTF::String 在 C++ 中變更大小寫。
  • 欄位類型轉換

Postgres 專屬功能

我們尚未實作這些功能

  • COPY 支援
  • LISTEN 支援
  • NOTIFY 支援

我們也尚未實作一些較不常見的功能,例如

  • GSSAPI 驗證
  • SCRAM-SHA-256-PLUS 支援
  • Point 和 PostGIS 類型
  • 所有多維整數陣列類型 (僅支援幾種類型)

常見問題

為什麼這是 Bun.sql 而不是 Bun.postgres

計畫是在未來新增更多資料庫驅動程式。

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

npm 套件 (例如 postgres.js、pg 和 node-postgres) 也可以在 Bun 中使用。它們是很棒的選擇。

兩個原因如下

  1. 我們認為,對於開發人員來說,在 Bun 中內建資料庫驅動程式更簡單。您花在程式庫選購上的時間就是您可以建置應用程式的時間。
  2. 我們利用一些 JavaScriptCore 引擎內部元件來更快地建立物件,這些物件很難在程式庫中實作

致謝

非常感謝 @porsagerpostgres.js,它為 API 介面提供了靈感。