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 接口设计的启发。