SQL
Bun 提供原生繫結,通過統一的基於 Promise 的 API 來操作 SQL 資料庫,支援 PostgreSQL、MySQL 和 SQLite。
You write queries as tagged template literals, and the client supports connection pooling, transactions, and prepared statements.
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 支援(以及身份驗證模式)
- 使用環境變數自動設定
- PostgreSQL
LISTEN/NOTIFY
資料庫支援
Bun.SQL 提供多資料庫系統的統一 API:
PostgreSQL
Bun uses PostgreSQL when:
- 連線字串不符合 SQLite 或 MySQL 格式(作為預設介面卡)
- 明確使用
postgres://或postgresql://協議的連線字串 - 無連線字串,但環境變數指向 PostgreSQL
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+:
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 格式作為連線字串:
// 標準 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 套接字連線
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 連線:SSL 模式設定類似 PostgreSQL
- 連線屬性:客戶端資訊傳送到伺服器便於監控
- 查詢流水線:無需等待響應即可執行多個預處理語句
MySQL 8 在明文 TCP 上進行身份驗證(allowPublicKeyRetrieval)
MySQL 8 賬戶預設使用 caching_sha2_password 外掛。使用者的首次連線需要完整身份驗證:在未加密的連線上,客戶端必須下載伺服器的 RSA 公鑰來加密密碼。Bun 預設拒絕這樣做(與 mysql2 和 Connector/J 的預設設定相同),因為網路路徑上的攻擊者可能替換自己的金鑰並獲取密碼。連線會失敗,並顯示 ERR_MYSQL_PUBLIC_KEY_RETRIEVAL_NOT_ALLOWED。
請使用 TLS 連線;如果你信任網路環境(例如本地開發資料庫),也可以顯式選擇啟用:
const mysql = new SQL({
adapter: "mysql",
hostname: "localhost",
username: "root",
password: "secret",
database: "myapp",
allowPublicKeyRetrieval: true, // permit RSA public key retrieval without TLS
});
SQLite
SQLite 內置於 Bun.SQL 中,使用相同的標籤模板字面量介面:
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 格式作為連線字串:
// 標準 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");
// 特殊記憶體資料庫
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"); // 讀寫並允許建立(預設)
SQLite 特定選項
SQLite 支援額外設定選項:
const sql = new SQL({
adapter: "sqlite",
filename: "app.db",
// SQLite 特定選項
readonly: false, // 只讀開啟
create: true, // 不存在則建立資料庫
readwrite: true, // 讀寫訪問
// 額外 Bun:sqlite 選項
strict: true, // 開啟嚴格模式
safeIntegers: false, // 使用 JS 數字處理整數
});
Bun parses query parameters in the URL to set these options:
?mode=ro→readonly: true?mode=rw→readonly: false, create: false?mode=rwc→readonly: false, create: true(預設)
插入資料
將 JavaScript 值直接傳遞給 SQL 模板字面量;Bun 會處理轉義。
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')
批次插入
你也可以傳遞物件陣列,Bun 會將其展開為 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")}`;
// 僅插入 name 和 email 列,其餘欄位忽略
查詢結果
預設情況下,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() 輔助函式,它會對其進行轉義:
// 安全動態引用表名
await sql`SELECT * FROM ${sql("users")}`;
// 帶模式限定
await sql`SELECT * FROM ${sql("public.users")}`;
條件查詢
使用 sql() 輔助函式建置包含條件子句的查詢:
// 可選 WHERE 子句
const filterAge = true;
const minAge = 21;
const ageFilter = sql`AND age > ${minAge}`;
await sql`
SELECT * FROM users
WHERE active = ${true}
${filterAge ? ageFilter : sql``}
`;
更新時動態列
Use sql(object, ...string) to pick which columns to update. Each column must be defined on the object. If you don't list any columns, Bun uses all keys on the object.
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 查詢。你還可以傳入物件陣列,並指定用於建置列表的鍵名。
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 用於將 JS 陣列轉換為 PostgreSQL 陣列字面量:
// 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])
sql``.simple()
PostgreSQL 協議支援兩類查詢:“簡單”和“擴充套件”查詢。簡單查詢支援多條語句但不支援引數,擴充套件查詢支援引數但只能一條語句。
要在一次查詢中執行多條語句,使用 sql``.simple():
// 一次執行多條語句
await sql`
SELECT 1;
SELECT 2;
`.simple();
簡單查詢適用於資料庫遷移和設定指令碼。
簡單查詢不能使用引數(${value})。如果需要使用引數,請將查詢拆分為多個獨立語句。
檔案查詢
sql.file 從檔案中讀取查詢並執行。如果檔案使用 $1 和 $2 之類的佔位符,可以向查詢傳遞引數。不傳遞引數時,檔案可以包含多個命令。
const result = await sql.file("query.sql", [1, 2, 3]);
使用 SQLite 介面卡時,引數也可以是命名引數物件(:name、$name 或 @name 佔位符),形式與下文 sql.unsafe 接受的物件相同。
不安全查詢
sql.unsafe 執行原始 SQL 字串。請謹慎使用:它不會轉義使用者輸入。不傳遞引數時,字串可以包含多個命令。
// 多條命令無引數
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]);
使用 SQLite 介面卡時,引數也可以是使用 :name、$name 或 @name 佔位符的命名引數物件。物件鍵會保留字首({ ":id": 1 }),除非連線設定了 strict: true,此時允許使用不帶字首的鍵:
const sql = new SQL({ adapter: "sqlite", filename: "myapp.db", strict: true });
const result = await sql.unsafe("SELECT * FROM users WHERE id = :id", { id: 1 });
執行和取消查詢
查詢是惰性的:只有在使用 await 或通過 .execute() 執行環境才會開始執行。
要取消正在執行的查詢,請在查詢物件上呼叫 cancel()。
const query = sql`SELECT * FROM users`.execute();
setTimeout(() => query.cancel(), 100);
await query;
資料庫環境變數
你可以使用環境變數設定 sql 連線引數。客戶端會按照優先順序順序檢查這些變數,並根據連線字串的格式檢測資料庫型別。
自動資料庫檢測
當你不帶引數使用 Bun.sql(),或使用帶連線字串的 new SQL() 時,Bun 會根據 URL 格式檢測介面卡:
MySQL 自動檢測
Bun selects MySQL when the connection string matches these patterns:
mysql://...— MySQL 協議mysql2://...— MySQL2 協議(相容別名)
// 以下均自動使用 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 自動檢測
Bun selects SQLite when the connection string matches these patterns:
:memory:— 記憶體資料庫sqlite://...— SQLite 協議sqlite:...— 無斜槓的 SQLite 協議file://...— 檔案協議file:...— 無斜槓的檔案協議
// 以下均自動使用 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:
# 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 環境變數
You can configure MySQL connections with environment variables:
# 優先使用的連線 URL
MYSQL_URL="mysql://user:pass@localhost:3306/mydb"
# 相容 DATABASE_URL 格式(MySQL 協議)
DATABASE_URL="mysql://user:pass@localhost:3306/mydb"
DATABASE_URL="mysql2://user:pass@localhost:3306/mydb"
Without a connection URL, Bun checks these individual parameters:
| 環境變數 | 預設值 | 說明 |
|---|---|---|
MYSQL_HOST | localhost | 資料庫主機 |
MYSQL_PORT | 3306 | 埠 |
MYSQL_USER | root | 使用者名稱 |
MYSQL_PASSWORD | 空字串 | 密碼 |
MYSQL_DATABASE | mysql | 資料庫名 |
MYSQL_URL | 空字串 | MySQL 主要連線 URL |
TLS_MYSQL_DATABASE_URL | 空字串 | 啟用 SSL/TLS 的連線 URL |
PostgreSQL 環境變數
以下環境變數用於定義 PostgreSQL 連線:
| 環境變數 | 說明 |
|---|---|
POSTGRES_URL | PostgreSQL 主要連線 URL |
DATABASE_URL | 相容的另一個連線 URL |
PGURL | 相容連線 URL |
PG_URL | 相容連線 URL |
TLS_POSTGRES_DATABASE_URL | 啟用 SSL/TLS 的連線 URL |
TLS_DATABASE_URL | 另一個啟用 SSL/TLS 的連線 URL |
Without a connection URL, Bun checks these individual parameters:
| 環境變數 | 備用變數 | 預設值 | 說明 |
|---|---|---|---|
PGHOST | - | localhost | 資料庫主機 |
PGPORT | - | 5432 | 資料庫埠 |
PGUSERNAME | PGUSER, USER, USERNAME | postgres | 資料庫使用者 |
PGPASSWORD | - | (空) | 資料庫密碼 |
PGDATABASE | - | username | 資料庫名稱 |
PGSSLMODE | - | disable | SSL 模式(disable、allow、prefer、require、verify-ca、verify-full) |
SQLite 環境變數
You can configure SQLite connections with DATABASE_URL when it contains a SQLite-compatible URL:
# SQLite 連線範例
DATABASE_URL=":memory:"
DATABASE_URL="sqlite://./app.db"
DATABASE_URL="file:///absolute/path/to/db.sqlite"
Note: Bun ignores PostgreSQL-specific environment variables such as POSTGRES_URL and PGHOST when you use SQLite.
執行環境預連線
Bun 可以在應用程式碼執行之前,於啟動時預先連線到 PostgreSQL,因此首次查詢無需承擔連線延遲。
# 啟用 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
The --sql-preconnect flag establishes a PostgreSQL connection at startup using your configured environment variables. If the connection fails, Bun handles the error without crashing your application.
連線選項
你可以通過向 SQL 建構函式傳遞選項來手動設定連線。選項因介面卡而異:
MySQL 選項
import { SQL } from "bun";
const sql = new SQL({
// MySQL 必須指定
adapter: "mysql",
// 連線設定
hostname: "localhost",
port: 3306,
database: "myapp",
username: "dbuser",
password: "secretpass",
// Unix 套接字連線(替代 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 選項
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 選項
import { SQL } from "bun";
const sql = new SQL({
// 必須指定介面卡
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:建立臨時資料庫,僅存活於連線生命週期內。
動態密碼
對於訪問令牌等替代身份驗證方案,或使用輪換密碼的資料庫,請將 password 設定為同步或非同步函式。Bun 會在連線時呼叫該函式以獲取密碼。
import { SQL } from "bun";
const sql = new SQL(url, {
// 其他連線設定
...
// 用於獲取資料庫使用者密碼的函式
password: async () => await signer.getAuthToken(),
});
SQLite 特有功能
查詢執行
SQLite 同步執行查詢,與使用非同步 I/O 的 PostgreSQL 不同。API 仍然傳回 Promise:
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 PRAGMA 語句
使用 PRAGMA 語句設定 SQLite 行為:
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 靈活:
// 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 -- 二進位制資料
)
`;
// JS 值會自動轉換
await sqlite`INSERT INTO flexible VALUES (${1}, ${"text"}, ${123.45}, ${Buffer.from("binary")})`;
事務
用 sql.begin 啟動事務,支援 PostgreSQL 和 SQLite。PostgreSQL 會從連線池中保留連線,SQLite 則在單一連線上開始事務。
Bun sends the BEGIN command automatically, including any optional configurations you specify. If an error occurs during the transaction, Bun issues a ROLLBACK.
基本事務
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`;
// 沒有拋錯則自動提交
// 拋錯則回滾
});
要在事務中對查詢進行流水線處理,請從回撥中傳回查詢陣列:
await sql.begin(async tx => {
return [
tx`INSERT INTO users (name) VALUES (${"Alice"})`,
tx`UPDATE accounts SET balance = balance - 100 WHERE user_id = 1`,
];
});
儲存點(Savepoints)
儲存點會在事務中建立中間檢查點,因此可以回滾其中一部分,而不會中止整個事務。
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("Rollback to savepoint");
}
});
// 儲存點回滾後繼續事務
await tx`INSERT INTO audit_log (action) VALUES ('user_created')`;
});
分散式事務
Two-Phase Commit (2PC) is a distributed transaction protocol. In phase 1, the coordinator prepares each node, making sure its data is written and ready to commit. In phase 2, the nodes commit or roll back based on the coordinator's decision.
在 PostgreSQL 和 MySQL 中,分散式事務會在其原始會話結束後繼續存在,因此具有相應許可權的使用者或協調者可以在之後提交或回滾這些事務。PostgreSQL 將其實現為預備事務;MySQL 使用 XA 事務。
分散式事務期間未捕獲的異常會回滾所有更改。否則,你可以在之後提交或回滾該事務。
// 開始分散式事務
await sql.beginDistributed("tx1", async tx => {
await tx`INSERT INTO users (name) VALUES (${"Alice"})`;
});
// 後續提交或回滾
await sql.commitDistributed("tx1");
// 或
await sql.rollbackDistributed("tx1");
認證
Bun supports SCRAM-SHA-256 (SASL), MD5, and Clear Text authentication. We recommend SASL for better security. See Postgres SASL Authentication.
SSL 模式概述
PostgreSQL 的 SSL/TLS 模式控制是否需要安全連線,以及執行何種程度的證書驗證。
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 | 最安全的模式。驗證證書及主機名是否匹配。可防止不受信任的證書和中間人攻擊。 |
連線字串中設定
你也可以在連線字串中設定 SSL 模式:
// 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's SQL client manages a connection pool. The pool reuses database connections across queries instead of opening and closing one for each query, and it caps the number of concurrent connections.
const sql = new SQL({
max: 20, // 最大連線數 20
idleTimeout: 30, // 空閒 30 秒關閉連線
maxLifetime: 3600, // 連線最大存活 1 小時
connectionTimeout: 10, // 連線超時 10 秒
});
Bun doesn't open a connection until you run a query.
const sql = Bun.SQL(); // 未連線
await sql`...`; // 連線池啟動,最多建立 max 連線,複用空閒連線
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 }); // 立即關閉所有連線
保留連線
sql.reserve() 從連線池中獲取一個連線,並傳回一個封裝該連線的客戶端,以便你可以在隔離的連線上執行查詢。
// 保留獨佔連線
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`;
} // 自動釋放
When every connection is busy, reserve() waits until one frees up. Pass an AbortSignal to stop waiting: the promise rejects with signal.reason and no connection is taken from the pool. Aborting after the promise resolved has no effect, so a connection you received must still be released.
// Give up if no connection frees up within 5 seconds
using reserved = await sql.reserve({ signal: AbortSignal.timeout(5000) });
LISTEN / NOTIFY(PostgreSQL)
sql.listen() 訂閱 PostgreSQL 通知頻道,sql.notify() 向頻道釋出通知。共享資料庫的行程可以將其用作輕量級訊息匯流排:快取失效、在插入行時喚醒工作行程(通常由呼叫 pg_notify 的觸發器完成)、分發小型事件。
import { sql } from "bun";
const subscription = await sql.listen("orders", payload => {
console.log("new order", JSON.parse(payload));
});
await sql.notify("orders", JSON.stringify({ id: 42 }));
await subscription.unlisten();
listen() 會在 PostgreSQL 確認 LISTEN 後完成,因此在它完成後發出的 notify() 都能夠送達。它所完成得到的訂閱物件也是非同步可釋放物件:
{
await using subscription = await sql.listen("orders", handleOrder);
await doWork();
} // 在此處取消監聽
工作原理
- All subscriptions on a client share one dedicated connection. The first
listen()opens it, and removing the last subscription closes it. As a result, a client that never listens never pays for the connection, and unlistening everything lets the process exit withoutsql.close(). - While anything is subscribed, that connection keeps the process alive, like a listening server.
- If the connection drops, Bun re-establishes it with exponential backoff (250ms doubling to 32s, with jitter) and re-subscribes every channel. PostgreSQL only delivers to connected listeners, so notifications sent in between are lost. The optional third argument to
listen()runs on the initial subscribe and after every reconnect, which is the place to catch up:
await sql.listen("orders", handleOrder, async () => {
for (const order of await sql`SELECT * FROM orders WHERE processed = false`) handleOrder(order);
});
- Every
listen()call is its own subscription. Several on one channel share a single server-sideLISTEN. Each callback receives every notification, and each handle'sunlisten()removes only what its own call registered. A callback (either argument) that throws is reported as an uncaught exception and stays subscribed. - Bun quotes channel names as identifiers for you. Like any PostgreSQL identifier, they are limited to 63 bytes, and Bun rejects longer names rather than silently truncating them. PostgreSQL limits payloads to 8000 bytes by default.
notify()
notify() is an ordinary query (SELECT pg_notify($1, $2)) on whichever handle you call it through. On sql it uses the pool. Inside sql.begin() it runs in the transaction, so PostgreSQL delivers it on COMMIT and drops it on ROLLBACK. Calling notify() inside a transaction is how to announce a change only once it is visible:
await sql.begin(async tx => {
const [order] = await tx`INSERT INTO orders ${sql(data)} RETURNING id`;
await tx.notify("orders", String(order.id)); // 在事務提交時送達
});
負載是可選的:sql.notify("cache-invalidated") 就是 PostgreSQL 的裸 NOTIFY。保留連線和事務控制代碼也具有 listen();它始終使用客戶端共享的監聽連線。
LISTEN/NOTIFY 僅支援 PostgreSQL;在 MySQL 和 SQLite 上呼叫這些方法會被拒絕。
預處理語句
By default, Bun's SQL client creates named prepared statements for queries it can infer are static, which is faster. To disable named prepared statements, set prepare: false in the connection options:
const sql = new SQL({
// 其他設定...
prepare: false, // 停用伺服器端持久命名預處理語句
});
設定 prepare: false 後:
查詢仍使用“擴充套件”協議,但會作為未命名預處理語句執行。未命名預處理語句只會持續到下一條指定將未命名語句作為目標的 Parse 語句發出為止。
-
Parameter binding is still safe against SQL injection
-
The server parses and plans each query from scratch
-
Queries are not pipelined
-
Only one command per query is supported (unless you use
sql``.simple()) -
使用 PGBouncer 事務模式(1.21.0 以後的版本如果設定正確已支援命名語句)
-
除錯查詢執行計劃
-
動態 SQL 需頻繁重新規劃查詢
-
不支援多條語句(除非用
.simple()) -
Using PGBouncer in transaction mode (though since PGBouncer 1.21.0, protocol-level named prepared statements are supported when configured properly)
-
Debugging query execution plans
-
Working with dynamic SQL where query plans need to be regenerated frequently
停用預處理語句可能會減慢使用不同引數頻繁執行的查詢,因為伺服器需要對每次查詢都從頭開始解析和規劃。
錯誤處理
客戶端為不同的失敗場景提供了型別化錯誤。錯誤具有資料庫特定性,並繼承自基礎錯誤類:
錯誤類範例
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); // 提示
} 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_FAILED | 連線已被接受,但在握手完成前就被關閉(例如伺服器仍在啟動中)。在查詢等待期間會持續重試並進行退避,直到 connectionTimeout。注意:伺服器在啟動期間傳送的錯誤(如 57P03)會表現為 ERR_POSTGRES_SERVER_ERROR |
ERR_POSTGRES_CONNECTION_REFUSED | 由於地址上沒有服務在監聽,連線被拒絕。會立即失敗且不會重試 |
ERR_POSTGRES_CONNECTION_TIMEOUT | 在超時時間內未能建立連線 |
ERR_POSTGRES_IDLE_TIMEOUT | 因空閒而關閉連線 |
ERR_POSTGRES_LIFETIME_TIMEOUT | 連線超過最大存活時間 |
ERR_POSTGRES_TLS_NOT_AVAILABLE | SSL/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 簽名 Base64 編碼錯誤 |
ERR_POSTGRES_SASL_SIGNATURE_MISMATCH | SASL 簽名驗證失敗 |
查詢錯誤
| 錯誤碼 | 說明 |
|---|---|
ERR_POSTGRES_SYNTAX_ERROR | SQL 語法錯誤(繼承自 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 |
The client raises these errors while it decodes a result, after the server ran the query. Only that query rejects. The connection stays open, and the other queries on it continue. An INSERT, UPDATE or DELETE ... RETURNING can already be stored at that point, so do not retry a write blindly. To read the value, cast the column to text or use .raw().
Protocol Errors
| 錯誤碼 | 說明 |
|---|---|
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_CONSTRAINT | 19 | 約束衝突(唯一、檢查、非空等) |
SQLITE_BUSY | 5 | 資料庫正被鎖定 |
SQLITE_LOCKED | 6 | 表被鎖 |
SQLITE_READONLY | 8 | 試圖寫入只讀資料庫 |
SQLITE_IOERR | 10 | 磁碟 I/O 錯誤 |
SQLITE_CORRUPT | 11 | 資料庫檔案損壞 |
SQLITE_FULL | 13 | 磁碟空間或資料庫已滿 |
SQLITE_CANTOPEN | 14 | 無法開啟資料庫檔案 |
SQLITE_PROTOCOL | 15 | 資料庫鎖協議錯誤 |
SQLITE_SCHEMA | 17 | 資料庫結構已更改 |
SQLITE_TOOBIG | 18 | 字串或 BLOB 過長 |
SQLITE_MISMATCH | 20 | 資料型別不匹配 |
SQLITE_MISUSE | 21 | 錯誤使用 SQLite 庫 |
SQLITE_AUTH | 23 | 訪問許可權被拒絕 |
錯誤處理範例:
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')`; // 主鍵重複
} catch (error) {
if (error instanceof SQL.SQLiteError) {
if (error.code === "SQLITE_CONSTRAINT") {
console.log("約束衝突:", error.message);
// 處理唯一約束衝突
}
}
}
數字與 BigInt
Bun returns PostgreSQL bigint (int8) values, and MySQL BIGINT values outside the 32-bit range, as strings:
import { sql } from "bun";
const [{ x, y }] = await sql`SELECT 9223372036854777 as x, 12345 as y`;
console.log(typeof x, x); // "字串" "9223372036854777"
console.log(typeof y, y); // "數字" 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
路線圖
我們尚未完成的事項:
- Column name transforms (for example,
snake_casetocamelCase). This is mostly blocked on a unicode-aware implementation of changing the case in C++ using WebKit'sWTF::String. - Column type transforms
資料庫特有功能
認證方式
MySQL 支援多種認證外掛,自動協商:
MySQL supports multiple authentication plugins, which the client negotiates automatically:
客戶端自動處理伺服器請求切換認證外掛,包括非 SSL 下的密碼安全交換。
The client automatically handles authentication plugin switching when the server requests it.
MySQL 對所有引數化查詢使用伺服器端預處理語句:
// 自動建立伺服器端預處理語句
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 支援多語句查詢傳回多個結果集:
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 字元。
連線屬性
Bun 會向 MySQL 傳送客戶端資訊以便進行監控:
// 自動傳送以下屬性:
// _client_name: "Bun"
// _client_version: <bun 版本號>
// 可以在 MySQL performance_schema.session_connect_attrs 中檢視
型別處理
Bun converts MySQL types to JavaScript types:
| MySQL 型別 | JavaScript 型別 | 說明 |
|---|---|---|
| INT, TINYINT, MEDIUMINT | number | 在安全整數範圍內 |
| BIGINT | string, number or BigInt | 如果值適合 i32/u32,則為 number,否則根據 bigint 選項為 string 或 BigInt |
| DECIMAL, NUMERIC | string | 用於保留精度 |
| FLOAT, DOUBLE | number | |
| DATE | Date | JavaScript Date object |
| DATETIME, TIMESTAMP | Date | Decoded as UTC (see note below); 0000-00-00 becomes an Invalid Date |
| TIME | string | Formatted as HH:MM:SS (HHH:MM:SS above 99 hours); prepared queries drop fractional seconds |
| YEAR | number | |
| CHAR, VARCHAR, VARSTRING, STRING | string | |
| TINY TEXT, MEDIUM TEXT, TEXT, LONG TEXT | string | |
| TINY BLOB, MEDIUM BLOB, BLOB, LONG BLOB | Buffer | Same wire types as TEXT; Bun returns a Buffer when the column uses the binary character set |
| JSON | object/array | Automatically parsed |
| BIT(1) | boolean | BIT(1) in MySQL |
| GEOMETRY | Buffer | Binary character set; the bytes are a 4-byte SRID followed by WKB |
DATETIME and TIMESTAMP values have no timezone on the wire, so Bun reads them back as UTC. The Date you get has the same UTC wall-clock that was stored, regardless of the machine's timezone. Reading as UTC matches how Bun writes values (a bound Date stores its UTC components). The same applies to PostgreSQL's timestamp (without time zone); timestamptz carries an explicit offset and is unaffected.
Bun also sets time_zone = '+00:00' on every MySQL connection it opens. The server converts TIMESTAMP columns through the session time zone, so this keeps the stored instant equal to the Date you bound, and it makes server-side time functions such as NOW() and CURRENT_TIMESTAMP evaluate in UTC. There is no option to pick a different session time zone yet: a manual SET time_zone = ... reaches only the one pooled connection that runs it (#40254 tracks a per-connection option).
與 PostgreSQL 的差異
API 是統一的,但行為有所不同:
- 引數佔位符:MySQL 內部使用
?,Bun 自動轉換$1、$2風格佔位符 - RETURNING 語法:MySQL 不支援 RETURNING,使用
result.lastInsertRowid或單獨 SELECT - 陣列型別:MySQL 無原生陣列型別,區別於 PostgreSQL
MySQL 特有功能
我們尚未實現對 LOAD DATA INFILE 的支援。
PostgreSQL 特有功能
尚未實現:
COPY支援
未實現的一些較少用特性:
- GSSAPI 認證
SCRAM-SHA-256-PLUS支援- Point 和 PostGIS 型別
- 多維整數陣列(僅支援部分型別)
常見模式與最佳實踐
處理 MySQL 結果集
// 獲取插入 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 錯誤處理
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 效能最佳化建議
- 使用連線池:根據負載設定合理的
max連線數 - 啟用預處理語句:預設開啟,提升效能
- 批次操作用事務包裹:關聯查詢放在同個事務裡
- 合理建立索引:MySQL 查詢效能相依索引策略
- 使用
utf8mb4字元集:預設啟用,支援完整 Unicode 字元。
常見問題
為什麼是這個 `Bun.sql`,而不是 `Bun.postgres`?
最初的計劃是新增更多資料庫驅動。統一 API 現在支援 PostgreSQL、MySQL 和 SQLite。
How do I know which database adapter is being used?
Bun detects the adapter automatically from the connection string:
- 以
mysql://或mysql2://開頭為 MySQL - 匹配 SQLite 格式(
:memory:、sqlite://、file://)為 SQLite - 其他預設使用 PostgreSQL
支援 MySQL 儲存過程嗎?
支援,包括 OUT 引數和多個結果集:
// 呼叫儲存過程
const results = await mysql`CALL GetUserStats(${userId}, @total_orders)`;
// 獲取 OUT 引數
const outParam = await mysql`SELECT @total_orders as total`;
可以使用 MySQL 特有語法嗎?
當然,可以使用所有 MySQL 專屬語法:
// MySQL 特殊語法範例
await mysql`SET @user_id = ${userId}`;
await mysql`SHOW TABLES`;
await mysql`DESCRIBE users`;
await mysql`EXPLAIN SELECT * FROM users WHERE id = ${id}`;
為什麼不直接用已有庫?
你也可以在 Bun 中使用 postgres.js、pg 和 node-postgres 等 npm 包。它們都是很好的選擇。
但有兩個原因:
- 我們認為,對於開發者來說,將資料庫驅動內建到 Bun 中會更加簡單。你花在挑選庫上的時間,本可以用來建置應用。
- 我們利用了一些 JavaScriptCore 引擎內部機制來更快地建立物件,而這些機制很難在庫中實現。
鳴謝
特別感謝 @porsager 的 postgres.js 對 API 介面設計的啟發。