运行时包管理器打包器测试运行器代码检查格式化指南参考博客反馈

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.

db.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 和明文身份验证
  • 连接超时
  • 将行作为数据对象、数组的数组或 Buffer 返回
  • 二进制协议支持,可提升速度
  • TLS 支持(以及身份验证模式)
  • 使用环境变量自动配置
  • PostgreSQL LISTEN / NOTIFY

数据库支持

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

PostgreSQL

Bun uses PostgreSQL when:

  • 连接字符串不符合 SQLite 或 MySQL 格式(作为默认适配器)
  • 明确使用 postgres:// 或 postgresql:// 协议的连接字符串
  • 无连接字符串,但环境变量指向 PostgreSQL
db.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+:

db.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 连接字符串格式

MySQL 支持多种 URL 格式作为连接字符串:

TypeScript
// 标准 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 连接;如果你信任网络环境(例如本地开发数据库),也可以显式选择启用:

TypeScript
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 中,使用相同的标签模板字面量接口:

TypeScript
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 格式作为连接字符串:

TypeScript
// 标准 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 支持额外配置选项:

TypeScript
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 会处理转义。

TypeScript
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 ... 语句。

TypeScript
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) 选择要插入的列。每一列都必须在对象中定义。

TypeScript
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() 方法将每一行返回为值数组,顺序与查询中的列顺序相同。

TypeScript
const rows = await sql`SELECT * FROM users`.values();
console.log(rows);

行的形式如下:

TypeScript
[
  ["Alice", "alice@example.com"],
  ["Bob", "bob@example.com"],
];

当查询返回重复的列名时,sql``.values() 非常有用。对于对象(默认格式),由于列名是键,最后一个列会覆盖前面的列。使用 sql``.values() 时,每一列都会存在于数组中,因此可以通过索引读取重复列。

sql``.raw() 格式

.raw() 方法将行返回为 Buffer 对象数组。二进制数据处理或追求性能时可以使用它。

TypeScript
const rows = await sql`SELECT * FROM users`.raw();
console.log(rows); // [[Buffer, Buffer], [Buffer, Buffer], [Buffer, Buffer]]

SQL 片段

Bun 可以根据运行时条件动态构建查询,同时避免 SQL 注入风险。

动态表名

要动态引用表或模式,请使用 sql() 辅助函数,它会对其进行转义:

TypeScript
// 安全动态引用表名
await sql`SELECT * FROM ${sql("users")}`;

// 带模式限定
await sql`SELECT * FROM ${sql("public.users")}`;

条件查询

使用 sql() 辅助函数构建包含条件子句的查询:

TypeScript
// 可选 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.

TypeScript
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 查询。你还可以传入对象数组,并指定用于构建列表的键名。

TypeScript
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 数组字面量:

TypeScript
// 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():

TypeScript
// 一次执行多条语句
await sql`
  SELECT 1;
  SELECT 2;
`.simple();

简单查询适用于数据库迁移和设置脚本。

简单查询不能使用参数(${value})。如果需要使用参数,请将查询拆分为多个独立语句。

文件查询

sql.file 从文件中读取查询并执行。如果文件使用 $1 和 $2 之类的占位符,可以向查询传递参数。不传递参数时,文件可以包含多个命令。

TypeScript
const result = await sql.file("query.sql", [1, 2, 3]);

使用 SQLite 适配器时,参数也可以是命名参数对象(:name、$name 或 @name 占位符),形式与下文 sql.unsafe 接受的对象相同。

不安全查询

sql.unsafe 执行原始 SQL 字符串。请谨慎使用:它不会转义用户输入。不传递参数时,字符串可以包含多个命令。

TypeScript
// 多条命令无参数
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,此时允许使用不带前缀的键:

TypeScript
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()。

TypeScript
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 协议(兼容别名)
TypeScript
// 以下均自动使用 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:... — 无斜杠的文件协议
TypeScript
// 以下均自动使用 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:

bash
# 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:

bash
# 优先使用的连接 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_HOSTlocalhost数据库主机
MYSQL_PORT3306端口
MYSQL_USERroot用户名
MYSQL_PASSWORD空字符串密码
MYSQL_DATABASEmysql数据库名
MYSQL_URL空字符串MySQL 主要连接 URL
TLS_MYSQL_DATABASE_URL空字符串启用 SSL/TLS 的连接 URL

PostgreSQL 环境变量

以下环境变量用于定义 PostgreSQL 连接:

环境变量说明
POSTGRES_URLPostgreSQL 主要连接 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数据库端口
PGUSERNAMEPGUSER, USER, USERNAMEpostgres数据库用户
PGPASSWORD-(空)数据库密码
PGDATABASE-username数据库名称
PGSSLMODE-disableSSL 模式(disable、allow、prefer、require、verify-ca、verify-full)

SQLite 环境变量

You can configure SQLite connections with DATABASE_URL when it contains a SQLite-compatible URL:

bash
# 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,因此首次查询无需承担连接延迟。

bash
# 启用 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 选项

TypeScript
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 选项

TypeScript
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 选项

TypeScript
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 会在连接时调用该函数以获取密码。

TypeScript
import { SQL } from "bun";

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

SQLite 特有功能

查询执行

SQLite 同步执行查询,与使用异步 I/O 的 PostgreSQL 不同。API 仍然返回 Promise:

TypeScript
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 行为:

TypeScript
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 灵活:

TypeScript
// 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.

基本事务

TypeScript
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`;

  // 没有抛错则自动提交
  // 抛错则回滚
});

要在事务中对查询进行流水线处理,请从回调中返回查询数组:

TypeScript
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)

保存点会在事务中创建中间检查点,因此可以回滚其中一部分,而不会中止整个事务。

TypeScript
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 事务。

分布式事务期间未捕获的异常会回滚所有更改。否则,你可以在之后提交或回滚该事务。

TypeScript
// 开始分布式事务
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 模式控制是否需要安全连接,以及执行何种程度的证书验证。

TypeScript
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 模式:

TypeScript
// 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.

TypeScript
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.

TypeScript
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() 从连接池中获取一个连接,并返回一个封装该连接的客户端,以便你可以在隔离的连接上运行查询。

TypeScript
// 保留独占连接
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.

TypeScript
// 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 的触发器完成)、分发小型事件。

TypeScript
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() 都能够送达。它所完成得到的订阅对象也是异步可释放对象:

TypeScript
{
  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 without sql.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:
TypeScript
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-side LISTEN. Each callback receives every notification, and each handle's unlisten() 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:

TypeScript
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:

TypeScript
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

禁用预处理语句可能会减慢使用不同参数频繁执行的查询,因为服务器需要对每次查询都从头开始解析和规划。


错误处理

客户端为不同的失败场景提供了类型化错误。错误具有数据库特定性,并继承自基础错误类:

错误类示例

TypeScript
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_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 签名 Base64 编码错误
ERR_POSTGRES_SASL_SIGNATURE_MISMATCHSASL 签名验证失败

查询错误

错误码说明
ERR_POSTGRES_SYNTAX_ERRORSQL 语法错误(继承自 SyntaxError)
ERR_POSTGRES_SERVER_ERRORPostgreSQL 服务器中的一般错误
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_CONSTRAINT19约束冲突(唯一、检查、非空等)
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 库
SQLITE_AUTH23访问权限被拒绝

错误处理示例:

TypeScript
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:

TypeScript
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:

TypeScript
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_case to camelCase). This is mostly blocked on a unicode-aware implementation of changing the case in C++ using WebKit's WTF::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 对所有参数化查询使用服务端预处理语句:

TypeScript
// 自动创建服务端预处理语句
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 支持多语句查询返回多个结果集:

TypeScript
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 发送客户端信息以便进行监控:

TypeScript
// 自动发送以下属性:
// _client_name: "Bun"
// _client_version: <bun 版本号>
// 可以在 MySQL performance_schema.session_connect_attrs 中查看

类型处理

Bun converts MySQL types to JavaScript types:

MySQL 类型JavaScript 类型说明
INT, TINYINT, MEDIUMINTnumber在安全整数范围内
BIGINTstring, number or BigInt如果值适合 i32/u32,则为 number,否则根据 bigint 选项为 string 或 BigInt
DECIMAL, NUMERICstring用于保留精度
FLOAT, DOUBLEnumber
DATEDateJavaScript Date object
DATETIME, TIMESTAMPDateDecoded as UTC (see note below); 0000-00-00 becomes an Invalid Date
TIMEstringFormatted as HH:MM:SS (HHH:MM:SS above 99 hours); prepared queries drop fractional seconds
YEARnumber
CHAR, VARCHAR, VARSTRING, STRINGstring
TINY TEXT, MEDIUM TEXT, TEXT, LONG TEXTstring
TINY BLOB, MEDIUM BLOB, BLOB, LONG BLOBBufferSame wire types as TEXT; Bun returns a Buffer when the column uses the binary character set
JSONobject/arrayAutomatically parsed
BIT(1)booleanBIT(1) in MySQL
GEOMETRYBufferBinary 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 是统一的,但行为有所不同:

  1. 参数占位符:MySQL 内部使用 ?,Bun 自动转换 $1、$2 风格占位符
  2. RETURNING 语法:MySQL 不支持 RETURNING,使用 result.lastInsertRowid 或单独 SELECT
  3. 数组类型:MySQL 无原生数组类型,区别于 PostgreSQL

MySQL 特有功能

我们尚未实现对 LOAD DATA INFILE 的支持。

PostgreSQL 特有功能

尚未实现:

  • COPY 支持

未实现的一些较少用特性:

  • GSSAPI 认证
  • SCRAM-SHA-256-PLUS 支持
  • Point 和 PostGIS 类型
  • 多维整数数组(仅支持部分类型)

常见模式与最佳实践

处理 MySQL 结果集

TypeScript
// 获取插入 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 错误处理

TypeScript
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 字符。

常见问题

为什么是这个 `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 参数和多个结果集:

TypeScript
// 调用存储过程
const results = await mysql`CALL GetUserStats(${userId}, @total_orders)`;

// 获取 OUT 参数
const outParam = await mysql`SELECT @total_orders as total`;
可以使用 MySQL 特有语法吗?

当然,可以使用所有 MySQL 专属语法:

TypeScript
// 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 包。它们都是很好的选择。

但有两个原因:

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

鸣谢

特别感谢 @porsager 的 postgres.js 对 API 接口设计的启发。