Bun

SQL

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 注入

事务

命名和位置参数

连接池

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 模式描述
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_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

路线图

还有一些我们尚未完成的事情。

  • 通过 --db-preconnect Bun CLI 标志预加载连接
  • MySQL 支持:我们正在努力
  • SQLite 支持:已计划,但尚未开始。理想情况下,我们本地实现它,而不是包装 bun:sqlite
  • 列名转换(例如,snake_casecamelCase)。这主要受阻于在 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 中使用。它们是很棒的选择。

两个原因

  1. 我们认为对于开发者来说,拥有一个内置于 Bun 的数据库驱动程序更简单。你花在库选择上的时间本可以用来构建你的应用程序。
  2. 我们利用了一些 JavaScriptCore 引擎内部机制,以更快地创建在库中难以实现的对象

致谢

非常感谢 @porsagerpostgres.js 为 API 接口提供了灵感。