Bun 提供了用于处理 PostgreSQL 数据库的本地绑定,并具有基于 Promise 的现代 API。该接口设计得简单且高性能,使用标记模板字面量进行查询,并提供连接池、事务和预处理语句等功能。
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 注入 | |
事务 | |
命名和位置参数 | |
连接池 | |
| |
SASL 身份验证支持 (SCRAM-SHA-256)、MD5 和明文 | |
连接超时 | |
将行作为数据对象、数组的数组或 | |
二进制协议支持使其速度更快 | |
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_URL | PostgreSQL 的主连接 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 | 数据库端口 |
PGUSERNAME | PGUSER 、USER 、USERNAME | postgres | 数据库用户 |
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 模式 | 描述 |
---|---|
disable | 不使用 SSL/TLS。如果服务器需要 SSL,连接将失败。 |
prefer | 首先尝试 SSL,如果 SSL 失败,则回退到非 SSL。如果未指定模式,则为默认模式。 |
require | 需要 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_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 签名编码无效 |
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 值 |
协议错误
协议错误 | 描述 |
---|---|
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
路线图
还有一些我们尚未完成的事情。
- 通过
--db-preconnect
Bun CLI 标志预加载连接 - MySQL 支持:我们正在努力
- SQLite 支持:已计划,但尚未开始。理想情况下,我们本地实现它,而不是包装
bun:sqlite
。 - 列名转换(例如,
snake_case
到camelCase
)。这主要受阻于在 C++ 中使用 WebKit 的WTF::String
进行 Unicode 感知的大小写转换实现。 - 列类型转换
Postgres 特定功能
我们尚未实现这些功能
COPY
支持LISTEN
支持NOTIFY
支持
我们也尚未实现一些更不常见的功能,例如
- GSSAPI 身份验证
SCRAM-SHA-256-PLUS
支持- Point & PostGIS 类型
- 所有多维整数数组类型(仅支持几种类型)
常见问题
为什么是 Bun.sql
而不是 Bun.postgres
?
计划在未来添加更多数据库驱动程序。
为什么不直接使用现有的库?
像 postgres.js、pg 和 node-postgres 这样的 npm 包也可以在 Bun 中使用。它们是很棒的选择。
两个原因
- 我们认为对于开发者来说,拥有一个内置于 Bun 的数据库驱动程序更简单。你花在库选择上的时间本可以用来构建你的应用程序。
- 我们利用了一些 JavaScriptCore 引擎内部机制,以更快地创建在库中难以实现的对象
致谢
非常感谢 @porsager 的 postgres.js 为 API 接口提供了灵感。