Skip to main content
接口设计简单且高性能,使用标签模板字面量进行查询,并提供连接池、事务和预处理语句等功能。
https://mintcdn.com/teemo/2s-4Z6VdGqiCeBNX/icons/typescript.svg?fit=max&auto=format&n=2s-4Z6VdGqiCeBNX&q=85&s=087b260066909db1cd3e9c7292bc34b2db.ts
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 和明文
  • 连接超时
  • 以数据对象、数组或缓冲区的形式返回行
  • 二进制协议支持使其更快
  • TLS 支持(和认证模式)
  • 使用环境变量自动配置

数据库支持

Bun.SQL 为多个数据库系统提供统一 API:

PostgreSQL

在以下情况下使用 PostgreSQL:
  • 连接字符串不匹配 SQLite 或 MySQL 模式(它是后备适配器)
  • 连接字符串明确使用 postgres://postgresql:// 协议
  • 没有提供连接字符串且环境变量指向 PostgreSQL
https://mintcdn.com/teemo/2s-4Z6VdGqiCeBNX/icons/typescript.svg?fit=max&auto=format&n=2s-4Z6VdGqiCeBNX&q=85&s=087b260066909db1cd3e9c7292bc34b2db.ts
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+:
https://mintcdn.com/teemo/2s-4Z6VdGqiCeBNX/icons/typescript.svg?fit=max&auto=format&n=2s-4Z6VdGqiCeBNX&q=85&s=087b260066909db1cd3e9c7292bc34b2db.ts
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 接受各种 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_native_password、caching_sha2_password(MySQL 8.0 默认)和 sha256_password
  • SSL/TLS 连接: 可配置的 SSL 模式,类似于 PostgreSQL
  • 连接属性: 发送到服务器的客户端信息以供监控
  • 查询管道: 执行多个预处理语句而无需等待响应

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 接受各种 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");

// 特殊 :memory: 数据库
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"); // 读写创建模式(默认)
没有协议的简单文件名(如 "myapp.db")需要明确指定 { adapter: "sqlite" } 以避免与 PostgreSQL 混淆。
SQLite 数据库支持额外的配置选项:
const sql = new SQL({
  adapter: "sqlite",
  filename: "app.db",

  // SQLite 特定选项
  readonly: false, // 以只读模式打开
  create: true, // 如果不存在则创建数据库
  readwrite: true, // 以读写模式打开

  // 额外的 Bun:sqlite 选项
  strict: true, // 启用严格模式
  safeIntegers: false, // 使用 JavaScript 数字表示整数
});
URL 中的查询参数被解析以设置这些选项:
  • ?mode=roreadonly: true
  • ?mode=rwreadonly: false, create: false
  • ?mode=rwcreadonly: false, create: true(默认)

插入数据

您可以直接将 JavaScript 值传递给 SQL 模板字面量,转义将为您处理。
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')

批量插入

您也可以将对象数组传递给 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")}`;
// 只插入 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``}
`;

更新中的动态列

您可以使用 sql(object, ...string) 来选择要更新的列。每个列都必须在对象上定义。如果未指定列,则将使用对象的所有键来更新行。
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 辅助器从 JavaScript 数组创建 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.array 仅适用于 PostgreSQL。多维数组和 NULL 元素可能尚未支持。

sql``.simple()

PostgreSQL 线协议支持两种类型的查询:“简单”和”扩展”。简单查询可以包含多个语句但不支持参数,而扩展查询(默认)支持参数但只允许一个语句。 要在单个查询中运行多个语句,请使用 sql``.simple()
// 一个查询中的多个语句
await sql`
  SELECT 1;
  SELECT 2;
`.simple();
简单查询通常对数据库迁移和设置脚本很有用。 请注意,简单查询不能使用参数 (${value})。如果需要参数,必须将查询拆分为单独的语句。

文件中的查询

您可以使用 sql.file 方法从文件读取查询并执行它,如果文件包含 11、2 等,您可以向查询传递参数。如果没有使用参数,它可以在每个文件中执行多个命令。
const result = await sql.file("query.sql", [1, 2, 3]);

不安全查询

您可以使用 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]);

执行和取消查询

Bun 的 SQL 是惰性的,这意味着它只在等待或使用 .execute() 执行时才开始执行。 您可以通过在查询对象上调用 cancel() 方法来取消当前正在执行的查询。
const query = sql`SELECT * FROM users`.execute();
setTimeout(() => query.cancel(), 100);
await query;

数据库环境变量

sql 连接参数可以使用环境变量配置。客户端按特定的优先级顺序检查这些变量,并根据连接字符串格式自动检测数据库类型。

自动数据库检测

使用不带参数的 Bun.sql() 或带连接字符串的 new SQL() 时,适配器根据 URL 格式自动检测:

MySQL 自动检测

当连接字符串匹配这些模式时,自动选择 MySQL:
  • mysql://... - MySQL 协议 URL
  • mysql2://... - MySQL2 协议 URL(兼容性别名)
// 这些都自动使用 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 自动检测

当连接字符串匹配这些模式时,自动选择 SQLite:
  • :memory: - 内存数据库
  • sqlite://... - SQLite 协议 URL
  • sqlite:... - 无斜杠的 SQLite 协议
  • file://... - 文件协议 URL
  • 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 自动检测

PostgreSQL 是连接字符串的默认选择,这些字符串不匹配 MySQL 或 SQLite 模式:
# 检测到这些模式为 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 环境变量

MySQL 连接可以通过环境变量配置:
# 主连接 URL(首先检查)
MYSQL_URL="mysql://user:pass@localhost:3306/mydb"

# 替代方案:带 MySQL 协议的 DATABASE_URL
DATABASE_URL="mysql://user:pass@localhost:3306/mydb"
DATABASE_URL="mysql2://user:pass@localhost:3306/mydb"
如果没有提供连接 URL,MySQL 检查这些单独的参数:
环境变量默认值描述
MYSQL_HOSTlocalhost数据库主机
MYSQL_PORT3306数据库端口
MYSQL_USERroot数据库用户
MYSQL_PASSWORD(empty)数据库密码
MYSQL_DATABASEmysql数据库名称
MYSQL_URL(empty)MySQL 的主连接 URL
TLS_MYSQL_DATABASE_URL(empty)SSL/TLS 启用的连接 URL

PostgreSQL 环境变量

以下环境变量可用于定义 PostgreSQL 连接:
环境变量描述
POSTGRES_URLPostgreSQL 的主连接 URL
DATABASE_URL替代连接 URL(自动检测)
PGURL替代连接 URL
PG_URL替代连接 URL
TLS_POSTGRES_DATABASE_URLSSL/TLS 启用的连接 URL
TLS_DATABASE_URL替代 SSL/TLS 启用的连接 URL
如果没有提供连接 URL,系统会检查以下单独的参数:
环境变量后备变量默认值描述
PGHOST-localhost数据库主机
PGPORT-5432数据库端口
PGUSERNAMEPGUSER, USER, USERNAMEpostgres数据库用户
PGPASSWORD-(empty)数据库密码
PGDATABASE-username数据库名称

SQLite 环境变量

DATABASE_URL 包含 SQLite 兼容 URL 时,可以通过环境变量配置 SQLite 连接:
# 这些都被识别为 SQLite
DATABASE_URL=":memory:"
DATABASE_URL="sqlite://./app.db"
DATABASE_URL="file:///absolute/path/to/db.sqlite"
注意: 使用 SQLite 时会忽略 PostgreSQL 特定的环境变量(POSTGRES_URLPGHOST 等)。

运行时预连接

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
--sql-preconnect 标志将在启动时使用您配置的环境变量自动建立 PostgreSQL 连接。如果连接失败,它不会崩溃您的应用程序 - 错误将被优雅处理。

连接选项

您可以通过向 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({
  // SQLite 必需
  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 不使用连接池,因为它是基于文件的数据库。每个 SQL 实例代表单个连接。
  • 事务: SQLite 通过保存点支持嵌套事务,类似于 PostgreSQL。
  • 并发访问: SQLite 通过文件锁定处理并发访问。使用 WAL 模式以获得更好的并发性。
  • 内存数据库: 使用 :memory: 创建仅存在于连接生命周期内的临时数据库。

动态密码

当客户端需要使用替代认证方案(如访问令牌)或连接到具有轮换密码的数据库时,请提供同步或异步函数,该函数将在连接时解析动态密码值。
import { SQL } from "bun";

const sql = new SQL(url, {
  // 其他连接配置
  ...
  // 数据库用户的密码函数
  password: async () => await signer.getAuthToken(),
});

SQLite 特定功能

查询执行

SQLite 同步执行查询,而 PostgreSQL 使用异步 I/O。但是,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 Pragmas

您可以使用 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         -- 二进制数据
  )
`;

// JavaScript 值自动转换
await sqlite`INSERT INTO flexible VALUES (${1}, ${"text"}, ${123.45}, ${Buffer.from("binary")})`;

事务

要开始新事务,请使用 sql.begin。此方法适用于 PostgreSQL 和 SQLite。对于 PostgreSQL,它从池中保留专用连接。对于 SQLite,它在单个连接上开始事务。 BEGIN 命令会自动发送,包括您指定的任何可选配置。如果事务期间发生错误,将触发 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`,
  ];
});

保存点

SQL 中的保存点在事务内创建中间检查点,允许部分回滚而不影响整个操作。它们在复杂事务中很有用,允许错误恢复并保持一致的结果。
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("回滚到保存点");
    }
  });

  // 即使保存点回滚,继续事务
  await tx`INSERT INTO audit_log (action) VALUES ('user_created')`;
});

分布式事务

两阶段提交 (2PC) 是一种分布式事务协议,其中第一阶段协调器通过确保数据已写入并准备提交来准备节点,而第二阶段根据协调器的决定最终确定节点的提交或回滚。此过程确保数据持久性和适当的锁管理。 在 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 支持 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最安全的模式。验证证书和主机名匹配。防止不受信任的证书和中间人攻击。

使用连接字符串

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 的 SQL 客户端自动管理连接池,这是一个用于多个查询的数据库连接池。这有助于减少为每个查询建立和关闭连接的开销,还有助于管理到数据库的并发连接数。
const sql = new SQL({
  // 池配置
  max: 20, // 最大 20 个并发连接
  idleTimeout: 30, // 30 秒后关闭空闲连接
  maxLifetime: 3600, // 最大连接生存时间 1 小时
  connectionTimeout: 10, // 连接超时 10 秒
});
在进行查询之前不会建立任何连接。
const sql = Bun.SQL(); // 不创建连接

await sql`...`; // 池在达到最大值之前启动,使用第一个可用连接
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 }); // 立即关闭池中的所有连接

预留连接

Bun 允许您从池中预留连接,并返回包装单个连接的客户端。这可用于在隔离连接上运行查询。
// 从池中获取独占连接
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`;
} // 自动释放

预处理语句

默认情况下,Bun 的 SQL 客户端为可以推断查询是静态的查询自动创建命名预处理语句。这提供更好的性能。但是,您可以通过在连接选项中设置 prepare: false 来更改此行为:
const sql = new SQL({
  // ... 其他选项 ...
  prepare: false, // 禁用在服务器上持久化命名预处理语句
});
当设置 prepare: false 时: 查询仍使用”扩展”协议执行,但它们使用未命名预处理语句执行,未命名预处理语句仅持续到发出指定未命名语句作为目标的下一个 Parse 语句。
  • 参数绑定仍然对 SQL 注入安全
  • 每个查询都由服务器从头开始解析和规划
  • 查询不会流水线化
您可能希望在以下情况下使用 prepare: false
  • 在事务模式下使用 PGBouncer(尽管自 PGBouncer 1.21.0 起,当正确配置时,协议级命名预处理语句已受支持)
  • 调试查询执行计划
  • 使用动态 SQL,其中查询计划需要频繁重新生成
  • 每个查询将不支持多个命令(除非您使用 sql``.simple()
请注意,禁用预处理语句可能会影响频繁使用不同参数执行的查询的性能,因为服务器需要从头开始解析和规划每个查询。

错误处理

客户端为不同的失败场景提供类型化的错误。错误是数据库特定的,从基础错误类扩展:

错误类

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); // 来自 PostgreSQL 的有用提示
  } 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 连接错误

连接错误描述
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_BASE64无效的 SASL 签名编码
ERR_POSTGRES_SASL_SIGNATURE_MISMATCHSASL 签名验证失败

查询错误

查询错误描述
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无效事务状态

SQLite 特定错误

SQLite 错误提供与 SQLite 标准错误代码对应的错误代码和数字:
错误代码errno描述
SQLITE_CONSTRAINT19约束违反(UNIQUE、CHECK、NOT NULL 等)
SQLITE_BUSY5数据库被锁定
SQLITE_LOCKED6数据库中的表被锁定
SQLITE_READONLY8尝试写入只读数据库
SQLITE_IOERR10磁盘 I/O 错误
SQLITE_CORRUPT11数据库磁盘映像是格式错误的
SQLITE_FULL13数据库或磁盘已满
SQLITE_CANTOPEN14无法打开数据库文件
SQLITE_PROTOCOL15数据库锁定协议错误
SQLITE_SCHEMA17数据库模式已更改
SQLITE_TOOBIG18字符串或 BLOB 超出大小限制
SQLITE_MISMATCH20数据类型不匹配
SQLITE_MISUSE21库使用不正确
SQLITE_AUTH23授权被拒绝
错误处理示例:
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')`; // 重复 ID
} catch (error) {
  if (error instanceof SQL.SQLiteError) {
    if (error.code === "SQLITE_CONSTRAINT") {
      console.log("约束违反:", error.message);
      // 处理唯一约束违反
    }
  }
}

数字和 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 标志进行连接预加载
  • 列名转换(例如,snake_casecamelCase)。这主要受 C++ 中使用 WebKit 的 WTF::String 实现区分大小写的 Unicode 感知实现的阻碍。
  • 列类型转换

数据库特定功能

认证方法

MySQL 支持多种认证插件,这些插件会自动协商:
  • mysql_native_password - 传统的 MySQL 认证,广泛兼容
  • caching_sha2_password - MySQL 8.0+ 中的默认值,使用 RSA 密钥交换更安全
  • sha256_password - 基于 SHA-256 的认证
客户端在服务器请求时自动处理认证插件切换,包括非 SSL 连接上的安全密码交换。

预处理语句和性能

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 支持,包括表情符号。这是现代 MySQL 应用程序推荐的字符集。

连接属性

Bun 自动向 MySQL 发送客户端信息以供更好的监控:
// 这些属性自动发送:
// _client_name: "Bun"
// _client_version: <bun version>
// 您可以在 MySQL 的 performance_schema.session_connect_attrs 中看到这些

类型处理

MySQL 类型自动转换为 JavaScript 类型:
MySQL 类型JavaScript 类型注意事项
INT, TINYINT, MEDIUMINTnumber在安全整数范围内
BIGINTstring, number 或 BigInt如果值适合 i32/u32 大小则为 number,否则为 string 或 BigInt 基于 bigint 选项
DECIMAL, NUMERICstring以保持精度
FLOAT, DOUBLEnumber
DATEDateJavaScript Date 对象
DATETIME, TIMESTAMPDate带时区处理
TIMEnumber总的微秒数
YEARnumber
CHAR, VARCHAR, VARSTRING, STRINGstring
TINY TEXT, MEDIUM TEXT, TEXT, LONG TEXTstring
TINY BLOB, MEDIUM BLOB, BLOG, LONG BLOBstringBLOB 类型是 TEXT 类型的别名
JSONobject/array自动解析
BIT(1)booleanMySQL 中的 BIT(1)
GEOMETRYstring几何数据

与 PostgreSQL 的差异

虽然 API 是统一的,但有一些行为差异:
  1. 参数占位符: MySQL 内部使用 ?,但 Bun 自动转换 $1, $2 样式
  2. RETURNING 子句: MySQL 不支持 RETURNING;使用 result.lastInsertRowid 或单独的 SELECT
  3. 数组类型: MySQL 没有像 PostgreSQL 那样的原生数组类型

MySQL 特定功能

我们尚未实现 LOAD DATA INFILE 支持

PostgreSQL 特定功能

我们尚未实现这些:
  • COPY 支持
  • LISTEN 支持
  • NOTIFY 支持
我们也没有实现一些不太常见的功能,如:
  • GSSAPI 认证
  • SCRAM-SHA-256-PLUS 支持
  • Point & PostGIS 类型
  • 所有多维整数数组类型(仅支持几种类型)

常见模式和最佳实践

使用 MySQL 结果集

// INSERT 后获取插入 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 性能提示

  1. 使用连接池: 根据工作负载设置适当的 max 池大小
  2. 启用预处理语句: 它们默认启用并提高性能
  3. 在批量操作中使用事务: 在事务中分组相关查询
  4. 正确索引: MySQL 严重依赖索引进行查询性能
  5. 使用 utf8mb4 字符集: 它默认设置并处理所有 Unicode 字符

常见问题

计划将来添加更多数据库驱动程序。现在添加了 MySQL 支持,此统一 API 支持 PostgreSQL、MySQL 和 SQLite。
适配器从连接字符串自动检测:
  • mysql://mysql2:// 开头的 URL 使用 MySQL
  • 匹配 SQLite 模式(:memory:sqlite://file://)的 URL 使用 SQLite
  • 其他所有内容默认为 PostgreSQL
是的,完全支持存储过程,包括 OUT 参数和多结果集:
// 调用存储过程
const results = await mysql`CALL GetUserStats(${userId}, @total_orders)`;

// 获取 OUT 参数
const outParam = await mysql`SELECT @total_orders as total`;
是的,您可以使用任何 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}`;

为什么不只使用现有库?

像 postgres.js、pg 和 node-postgres 这样的 npm 包也可以在 Bun 中使用。它们是很好的选择。 两个原因:
  1. 我们认为对开发人员来说,在 Bun 中内置数据库驱动程序更简单。您花在库购物上的时间可以用来构建您的应用。
  2. 我们利用一些 JavaScriptCore 引擎内部来创建库难以实现的对象

致谢

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