Bun

SQL

Bun 通过统一的基于 Promise 的 API 提供与 SQL 数据库交互的原生绑定,支持 PostgreSQL、MySQL 和 SQLite。该接口设计简洁高效,使用带标签的模板字面量进行查询,并提供连接池、事务和预处理语句等功能。

import { sql, SQL } from "bun";

// PostgreSQL (default)
const users = await sql`
  SELECT * FROM users
  WHERE active = ${true}
  LIMIT ${10}
`;

// With MySQL
const mysql = new SQL("mysql://user:pass@localhost:3306/mydb");
const mysqlResults = await mysql`
  SELECT * FROM users 
  WHERE active = ${true}
`;

// With 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 支持(和认证模式)

通过环境变量自动配置

数据库支持

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

PostgreSQL

在以下情况下使用 PostgreSQL:

  • 连接字符串不符合 SQLite 或 MySQL 模式(它是备用适配器)
  • 连接字符串明确使用 postgres://postgresql:// 协议
  • 未提供连接字符串且环境变量指向 PostgreSQL
import { sql } from "bun";
// Uses PostgreSQL if DATABASE_URL is not set or is a PostgreSQL URL
await sql`SELECT ...`;

import { SQL } from "bun";
const pg = new SQL("postgres://user:pass@localhost:5432/mydb");
await pg`SELECT ...`;

MySQL

Bun.SQL 内置了 MySQL 支持,提供相同的带标签的模板字面量接口,完全兼容 MySQL 5.7+ 和 MySQL 8.0+

import { SQL } from "bun";

// MySQL connection
const mysql = new SQL("mysql://user:password@localhost:3306/database");
const mysql2 = new SQL("mysql2://user:password@localhost:3306/database"); // mysql2 protocol also works

// Using options object
const mysql3 = new SQL({
  adapter: "mysql",
  hostname: "localhost",
  port: 3306,
  database: "myapp",
  username: "dbuser",
  password: "secretpass",
});

// Works with parameters - automatically uses prepared statements
const users = await mysql`SELECT * FROM users WHERE id = ${userId}`;

// Transactions work the same as 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}`;
});

// Bulk inserts
const newUsers = [
  { name: "Alice", email: "alice@example.com" },
  { name: "Bob", email: "bob@example.com" },
];
await mysql`INSERT INTO users ${mysql(newUsers)}`;

MySQL 连接字符串格式

MySQL 特定功能

SQLite

Bun.SQL 内置了 SQLite 支持,提供相同的带标签的模板字面量接口

import { SQL } from "bun";

// In-memory database
const memory = new SQL(":memory:");
const memory2 = new SQL("sqlite://:memory:");

// File-based database
const db = new SQL("sqlite://myapp.db");

// Using options object
const db2 = new SQL({
  adapter: "sqlite",
  filename: "./data/app.db",
});

// For simple filenames, specify adapter explicitly
const db3 = new SQL("myapp.db", { adapter: "sqlite" });

SQLite 连接字符串格式

SQLite 特定选项

插入数据

您可以直接将 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.array 助手

sql.array 助手从 JavaScript 数组创建 PostgreSQL 数组字面量

// Create array literals for PostgreSQL
await sql`INSERT INTO tags (items) VALUES (${sql.array(["red", "blue", "green"])})`;
// Generates: INSERT INTO tags (items) VALUES (ARRAY['red', 'blue', 'green'])

// Works with numeric arrays too
await sql`SELECT * FROM products WHERE ids = ANY(${sql.array([1, 2, 3])})`;
// Generates: SELECT * FROM products WHERE ids = ANY(ARRAY[1, 2, 3])

注意sql.array 仅限于 PostgreSQL。多维数组和 NULL 元素可能尚未支持。

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 连接参数可以使用环境变量进行配置。客户端按照特定的优先级顺序检查这些变量,并根据连接字符串格式自动检测数据库类型。

自动数据库检测

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

MySQL 自动检测

当连接字符串符合这些模式时,会自动选择 MySQL:

  • mysql://... - MySQL 协议 URL
  • mysql2://... - MySQL2 协议 URL(兼容性别名)
// These all use MySQL automatically (no adapter needed)
const sql1 = new SQL("mysql://user:pass@localhost/mydb");
const sql2 = new SQL("mysql2://user:pass@localhost:3306/mydb");

// Works with DATABASE_URL environment variable
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:... - 不带斜杠的文件协议
// These all use SQLite automatically (no adapter needed)
const sql1 = new SQL(":memory:");
const sql2 = new SQL("sqlite://app.db");
const sql3 = new SQL("file://./database.db");

// Works with DATABASE_URL environment variable
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 is detected for these patterns
DATABASE_URL="postgres://user:pass@localhost:5432/mydb" bun run app.js
DATABASE_URL="postgresql://user:pass@localhost:5432/mydb" bun run app.js

# Or any URL that doesn't match MySQL or SQLite patterns
DATABASE_URL="localhost:5432/mydb" bun run app.js

MySQL 环境变量

MySQL 连接可以通过环境变量配置

# Primary connection URL (checked first)
MYSQL_URL="mysql://user:pass@localhost:3306/mydb"

# Alternative: DATABASE_URL with MySQL protocol
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(空)数据库密码
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

如果未提供连接 URL,系统将检查以下单个参数:

环境变量回退变量默认值描述
PGHOST-localhost数据库主机
PGPORT-5432数据库端口
PGUSERNAMEPGUSER, USER, USERNAMEpostgres数据库用户
PGPASSWORD-(空)数据库密码
PGDATABASE-用户名数据库名

SQLite 环境变量

DATABASE_URL 包含 SQLite 兼容的 URL 时,可以通过它配置 SQLite 连接

# These are all recognized as SQLite
DATABASE_URL=":memory:"
DATABASE_URL="sqlite://./app.db"
DATABASE_URL="file:///absolute/path/to/db.sqlite"

注意:使用 SQLite 时,PostgreSQL 特定的环境变量(POSTGRES_URLPGHOST 等)将被忽略。

运行时预连接

Bun 可以在启动时预连接到 PostgreSQL,通过在应用程序代码运行之前建立数据库连接来提高性能。这有助于减少首次数据库查询的连接延迟。

# Enable PostgreSQL preconnection
bun --sql-preconnect index.js

# Works with DATABASE_URL environment variable
DATABASE_URL=postgres://user:pass@localhost:5432/db bun --sql-preconnect index.js

# Can be combined with other runtime flags
bun --sql-preconnect --hot index.js

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

连接选项

您可以通过将选项传递给 SQL 构造函数来手动配置数据库连接。选项因数据库适配器而异

MySQL 选项

import { SQL } from "bun";

const db = new SQL({
  // Required for MySQL when using options object
  adapter: "mysql",

  // Connection details
  hostname: "localhost",
  port: 3306,
  database: "myapp",
  username: "dbuser",
  password: "secretpass",

  // Unix socket connection (alternative to hostname/port)
  // socket: "/var/run/mysqld/mysqld.sock",

  // Connection pool settings
  max: 20, // Maximum connections in pool (default: 10)
  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: {
    rejectUnauthorized: true,
    ca: "path/to/ca.pem",
    key: "path/to/key.pem",
    cert: "path/to/cert.pem",
  },

  // Callbacks
  onconnect: client => {
    console.log("Connected to MySQL");
  },
  onclose: (client, err) => {
    if (err) {
      console.error("MySQL connection error:", err);
    } else {
      console.log("MySQL connection closed");
    }
  },
});

PostgreSQL 选项

import { SQL } from "bun";

const db = new SQL({
  // Connection details (adapter is auto-detected as PostgreSQL)
  url: "postgres://user:pass@localhost:5432/dbname",

  // Alternative connection parameters
  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 PostgreSQL");
  },
  onclose: client => {
    console.log("PostgreSQL connection closed");
  },
});

SQLite 选项

import { SQL } from "bun";

const db = new SQL({
  // Required for SQLite
  adapter: "sqlite",
  filename: "./data/app.db", // or ":memory:" for in-memory database

  // SQLite-specific access modes
  readonly: false, // Open in read-only mode
  create: true, // Create database if it doesn't exist
  readwrite: true, // Allow read and write operations

  // SQLite data handling
  strict: true, // Enable strict mode for better type safety
  safeIntegers: false, // Use BigInt for integers exceeding JS number range

  // Callbacks
  onconnect: client => {
    console.log("SQLite database opened");
  },
  onclose: client => {
    console.log("SQLite database closed");
  },
});

SQLite 连接注意事项

动态密码

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

import { SQL } from "bun";

const sql = new SQL(url, {
  // Other connection config
  ...
  // Password function for the database user
  password: async () => await signer.getAuthToken(),
});

SQLite 特定功能

查询执行

SQLite 同步执行查询,与使用异步 I/O 的 PostgreSQL 不同。但是,API 仍然使用 Promise 保持一致

const sqlite = new SQL("sqlite://app.db");

// Works the same as PostgreSQL, but executes synchronously under the hood
const users = await sqlite`SELECT * FROM users`;

// Parameters work identically
const user = await sqlite`SELECT * FROM users WHERE id = ${userId}`;

SQLite Pragmas

您可以使用 PRAGMA 语句配置 SQLite 行为

const sqlite = new SQL("sqlite://app.db");

// Enable foreign keys
await sqlite`PRAGMA foreign_keys = ON`;

// Set journal mode to WAL for better concurrency
await sqlite`PRAGMA journal_mode = WAL`;

// Check integrity
const integrity = await sqlite`PRAGMA integrity_check`;

数据类型差异

SQLite 的类型系统比 PostgreSQL 更灵活

// SQLite stores data in 5 storage classes: NULL, INTEGER, REAL, TEXT, BLOB
const sqlite = new SQL("sqlite://app.db");

// SQLite is more lenient with types
await sqlite`
  CREATE TABLE flexible (
    id INTEGER PRIMARY KEY,
    data TEXT,        -- Can store numbers as strings
    value NUMERIC,    -- Can store integers, reals, or text
    blob BLOB         -- Binary data
  )
`;

// JavaScript values are automatically converted
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 => {
  // 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) 是一种分布式事务协议,其中第一阶段协调器通过确保数据已写入并准备好提交来准备节点,而第二阶段则根据协调器的决定,节点要么提交要么回滚。此过程确保数据持久性和适当的锁管理。

在 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 模式描述
禁用不使用 SSL/TLS。如果服务器需要 SSL,则连接失败。
首选首先尝试 SSL,如果 SSL 失败则回退到非 SSL。如果未指定,则为默认模式。
必需需要 SSL,但不进行证书验证。如果无法建立 SSL,则失败。
验证 CA验证服务器证书是否由受信任的 CA 签名。如果验证失败,则失败。
完全验证最安全的模式。验证证书和主机名是否匹配。防止不受信任的证书和 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()

请注意,禁用预处理语句可能会影响频繁执行且参数不同的查询的性能,因为服务器需要从头开始解析和规划每个查询。

错误处理

客户端为不同的故障场景提供了类型化错误。错误是特定于数据库的,并继承自基本错误类

错误类

import { SQL } from "bun";

try {
  await sql`SELECT * FROM users`;
} catch (error) {
  if (error instanceof SQL.PostgresError) {
    // PostgreSQL-specific error
    console.log(error.code); // PostgreSQL error code
    console.log(error.detail); // Detailed error message
    console.log(error.hint); // Helpful hint from PostgreSQL
  } else if (error instanceof SQL.SQLiteError) {
    // SQLite-specific error
    console.log(error.code); // SQLite error code (e.g., "SQLITE_CONSTRAINT")
    console.log(error.errno); // SQLite error number
    console.log(error.byteOffset); // Byte offset in SQL statement (if available)
  } else if (error instanceof SQL.SQLError) {
    // Generic SQL error (base class)
    console.log(error.message);
  }
}

PostgreSQL 特定错误代码

SQLite 特定错误

SQLite 错误提供与 SQLite 标准错误代码相对应的错误代码和数字

常见 SQLite 错误代码

数字和 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)。这主要受到使用 WebKit 的 WTF::String 在 C++ 中实现 Unicode 感知的大小写转换的限制。
  • 列类型转换

数据库特定功能

认证方法

MySQL 支持多种身份验证插件,可自动协商。

  • mysql_native_password - 传统的 MySQL 身份验证,兼容性广。
  • caching_sha2_password - MySQL 8.0+ 中的默认值,通过 RSA 密钥交换提供更强的安全性。
  • sha256_password - 基于 SHA-256 的身份验证。

当服务器请求时,客户端会自动处理身份验证插件切换,包括在非 SSL 连接上安全地交换密码。

预处理语句和性能

MySQL 对所有参数化查询都使用服务器端预处理语句。

// This automatically creates a prepared statement on the server
const user = await mysql`SELECT * FROM users WHERE id = ${userId}`;

// Prepared statements are cached and reused for identical queries
for (const id of userIds) {
  // Same prepared statement is reused
  await mysql`SELECT * FROM users WHERE id = ${id}`;
}

// Query pipelining - multiple statements sent without waiting
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");

// Multi-statement queries with simple() method
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 发送客户端信息,以实现更好的监控。

// These attributes are sent automatically:
// _client_name: "Bun"
// _client_version: <bun version>
// You can see these in MySQL's performance_schema.session_connect_attrs

类型处理

MySQL 类型自动转换为 JavaScript 类型。

MySQL 类型JavaScript 类型备注
INT, TINYINT, MEDIUMINTnumber在安全整数范围内
BIGINTstring, number 或 BigInt如果值符合 i32/u32 大小,将是 number;否则根据 bigint 选项为 string 或 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 结果集

// Getting insert ID after INSERT
const result = await mysql`INSERT INTO users (name) VALUES (${"Alice"})`;
console.log(result.lastInsertRowid); // MySQL's LAST_INSERT_ID()

// Handling affected rows
const updated =
  await mysql`UPDATE users SET active = ${false} WHERE age < ${18}`;
console.log(updated.affectedRows); // Number of rows updated

// Using MySQL-specific functions
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("Duplicate entry detected");
  } else if (error.code === "ER_ACCESS_DENIED_ERROR") {
    console.log("Access denied");
  } else if (error.code === "ER_BAD_DB_ERROR") {
    console.log("Database does not exist");
  }
  // MySQL error codes are compatible with mysql/mysql2 packages
}

MySQL 性能提示

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

常见问题

为什么是 Bun.sql 而不是 Bun.postgres

计划是在未来添加更多数据库驱动。现在增加了 MySQL 支持,这个统一的 API 支持 PostgreSQL、MySQL 和 SQLite。

我如何知道正在使用哪个数据库适配器?

适配器从连接字符串中自动检测。

  • mysql://mysql2:// 开头的 URL 使用 MySQL。
  • 匹配 SQLite 模式(:memory:, sqlite://, file://)的 URL 使用 SQLite。
  • 其余的默认为 PostgreSQL。

支持 MySQL 存储过程吗?

是的,完全支持存储过程,包括 OUT 参数和多个结果集。

// Call stored procedure
const results = await mysql`CALL GetUserStats(${userId}, @total_orders)`;

// Get OUT parameter
const outParam = await mysql`SELECT @total_orders as total`;

我可以使用 MySQL 特定的 SQL 语法吗?

是的,您可以使用任何 MySQL 特定的语法。

// MySQL-specific syntax works fine
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 引擎内部机制,使得创建在库中难以实现的 JavaScript 对象变得更快。

致谢

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