Bun

SQLite

Bun 原生实现了一个高性能的 SQLite3 驱动程序。要使用它,请从内置的 bun:sqlite 模块导入。

import { Database } from "bun:sqlite";

const db = new Database(":memory:");
const query = db.query("select 'Hello world' as message;");
query.get(); // => { message: "Hello world" }

该 API 简单、同步且快速。感谢 better-sqlite3 及其贡献者为 bun:sqlite 的 API 提供了灵感。

功能包括

  • 事务
  • 参数(命名和位置)
  • 预处理语句
  • 数据类型转换(BLOB 转换为 Uint8Array
  • 将查询结果映射到类,无需 ORM - query.as(MyClass)
  • JavaScript 最快的 SQLite 驱动程序性能
  • bigint 支持
  • 多查询语句(例如 SELECT 1; SELECT 2;)在单个 database.run(query) 调用中

对于读取查询,bun:sqlite 模块大约比 better-sqlite3 快 3-6 倍,比 deno.land/x/sqlite 快 8-9 倍。每个驱动程序都针对 Northwind Traders 数据集进行了基准测试。查看并运行 基准测试源

SQLite benchmarks for Bun, better-sqlite3, and deno.land/x/sqlite
在运行 macOS 12.3.1 的 M1 MacBook Pro (64GB) 上进行基准测试

数据库

打开或创建 SQLite3 数据库

import { Database } from "bun:sqlite";

const db = new Database("mydb.sqlite");

打开内存数据库

import { Database } from "bun:sqlite";

// all of these do the same thing
const db = new Database(":memory:");
const db = new Database();
const db = new Database("");

以只读模式打开

import { Database } from "bun:sqlite";
const db = new Database("mydb.sqlite", { readonly: true });

如果文件不存在,则创建数据库

import { Database } from "bun:sqlite";
const db = new Database("mydb.sqlite", { create: true });

严格模式

在 Bun v1.1.14 中添加

默认情况下,bun:sqlite 要求绑定参数包含 $:@ 前缀,并且如果缺少参数,则不会抛出错误。

为了在缺少参数时抛出错误并允许无前缀绑定,请在 Database 构造函数上设置 strict: true

import { Database } from "bun:sqlite";

const strict = new Database(
  ":memory:",
  { strict: true }
);

// throws error because of the typo:
const query = strict
  .query("SELECT $message;")
  .all({ message: "Hello world" });

const notStrict = new Database(
  ":memory:"
);
// does not throw error:
notStrict
  .query("SELECT $message;")
  .all({ message: "Hello world" });

通过 ES 模块导入加载

您还可以使用 import 属性来加载数据库。

import db from "./mydb.sqlite" with { "type": "sqlite" };

console.log(db.query("select * from users LIMIT 1").get());

这等效于以下内容

import { Database } from "bun:sqlite";
const db = new Database("./mydb.sqlite");

.close(throwOnError: boolean = false)

要关闭数据库连接,但允许现有查询完成,请调用 .close(false)

const db = new Database();
// ... do stuff
db.close(false);

要关闭数据库并在有任何挂起的查询时抛出错误,请调用 .close(true)

const db = new Database();
// ... do stuff
db.close(true);

注意:当数据库被垃圾回收时,会自动调用 close(false)。可以多次安全调用,但在第一次之后没有效果。

using 语句

您可以使用 using 语句来确保在退出 using 代码块时关闭数据库连接。

import { Database } from "bun:sqlite";

{
  using db = new Database("mydb.sqlite");
  using query = db.query("select 'Hello world' as message;");
  console.log(query.get()); // => { message: "Hello world" }
}

.serialize()

bun:sqlite 支持 SQLite 的内置机制,用于将数据库序列化和反序列化到内存和从内存中序列化和反序列化。

const olddb = new Database("mydb.sqlite");
const contents = olddb.serialize(); // => Uint8Array
const newdb = Database.deserialize(contents);

在内部,.serialize() 调用 sqlite3_serialize

.query()

在您的 Database 实例上使用 db.query() 方法来准备 SQL 查询。结果是一个 Statement 实例,它将被缓存在 Database 实例上。查询将不会被执行。

const query = db.query(`select "Hello world" as message`);

注意 — 使用 .prepare() 方法来准备查询,而不在 Database 实例上缓存它。

// compile the prepared statement
const query = db.prepare("SELECT * FROM foo WHERE bar = ?");

WAL 模式

SQLite 支持预写式日志模式 (WAL),它可以显著提高性能,尤其是在有许多并发读取器和单个写入器的情况下。强烈建议为大多数典型应用程序启用 WAL 模式。

要启用 WAL 模式,请在应用程序的开头运行此 pragma 查询

db.exec("PRAGMA journal_mode = WAL;");

什么是 WAL 模式

语句

Statement 是一个预处理查询,这意味着它已被解析并编译成高效的二进制形式。它可以高性能地执行多次。

在您的 Database 实例上使用 .query 方法创建一个语句。

const query = db.query(`select "Hello world" as message`);

查询可以包含参数。这些参数可以是数字的(?1)或命名的($param:param@param)。

const query = db.query(`SELECT ?1, ?2;`);
const query = db.query(`SELECT $param1, $param2;`);

当执行查询时,值会绑定到这些参数。一个 Statement 可以通过几种不同的方法执行,每种方法都以不同的形式返回结果。

绑定值

要将值绑定到语句,请将一个对象传递给 .all().get().run().values() 方法。

const query = db.query(`select $message;`);
query.all({ $message: "Hello world" });

您也可以使用位置参数进行绑定

const query = db.query(`select ?1;`);
query.all("Hello world");

strict: true 允许您在没有前缀的情况下绑定值

在 Bun v1.1.14 中添加

默认情况下,在将值绑定到命名参数时,会包含 $:@ 前缀。要在没有这些前缀的情况下绑定,请在 Database 构造函数中使用 strict 选项。

import { Database } from "bun:sqlite";

const db = new Database(":memory:", {
  // bind values without prefixes
  strict: true,
});

const query = db.query(`select $message;`);

// strict: true
query.all({ message: "Hello world" });

// strict: false
// query.all({ $message: "Hello world" });

.all()

使用 .all() 运行查询并将结果作为对象数组返回。

const query = db.query(`select $message;`);
query.all({ $message: "Hello world" });
// => [{ message: "Hello world" }]

在内部,这会调用 sqlite3_reset 并重复调用 sqlite3_step,直到它返回 SQLITE_DONE

.get()

使用 .get() 运行查询并将第一个结果作为对象返回。

const query = db.query(`select $message;`);
query.get({ $message: "Hello world" });
// => { $message: "Hello world" }

在内部,这会调用 sqlite3_reset,然后调用 sqlite3_step,直到它不再返回 SQLITE_ROW。如果查询不返回任何行,则返回 undefined

.run()

使用 .run() 运行查询并返回 undefined。这对于模式修改查询(例如 CREATE TABLE)或批量写入操作非常有用。

const query = db.query(`create table foo;`);
query.run();
// {
//   lastInsertRowid: 0,
//   changes: 0,
// }

在内部,这会调用 sqlite3_reset 并调用一次 sqlite3_step。当您不关心结果时,无需遍历所有行。

自 Bun v1.1.14 起,.run() 返回一个具有两个属性的对象:lastInsertRowidchanges

lastInsertRowid 属性返回插入到数据库的最后一行记录的 ID。changes 属性是受查询影响的行数。

.as(Class) - 将查询结果映射到类

在 Bun v1.1.14 中添加

使用 .as(Class) 运行查询并将结果作为类的实例返回。这使您可以将方法和 getter/setter 附加到结果。

class Movie {
  title: string;
  year: number;

  get isMarvel() {
    return this.title.includes("Marvel");
  }
}

const query = db.query("SELECT title, year FROM movies").as(Movie);
const movies = query.all();
const first = query.get();
console.log(movies[0].isMarvel); // => true
console.log(first.isMarvel); // => true

作为性能优化,类构造函数不会被调用,默认初始化程序不会运行,并且私有字段不可访问。这更像是使用 Object.create 而不是 new。类的原型被分配给对象,方法被附加,并且 getter/setter 被设置,但构造函数不会被调用。

数据库列设置为类实例上的属性。

.iterate() (@@iterator)

使用 .iterate() 运行查询并增量返回结果。这对于您想要一次处理一行而不将所有结果加载到内存中的大型结果集非常有用。

const query = db.query("SELECT * FROM foo");
for (const row of query.iterate()) {
  console.log(row);
}

您也可以使用 @@iterator 协议

const query = db.query("SELECT * FROM foo");
for (const row of query) {
  console.log(row);
}

此功能在 Bun v1.1.31 中添加。

.values()

使用 values() 运行查询并将所有结果作为数组的数组返回。

const query = db.query(`select $message;`);
query.values({ $message: "Hello world" });

query.values(2);
// [
//   [ "Iron Man", 2008 ],
//   [ "The Avengers", 2012 ],
//   [ "Ant-Man: Quantumania", 2023 ],
// ]

在内部,这会调用 sqlite3_reset 并重复调用 sqlite3_step,直到它返回 SQLITE_DONE

.finalize()

使用 .finalize() 销毁 Statement 并释放与其关联的任何资源。一旦完成,Statement 将无法再次执行。通常,垃圾回收器会为您执行此操作,但显式完成可能在性能敏感型应用程序中很有用。

const query = db.query("SELECT title, year FROM movies");
const movies = query.all();
query.finalize();

.toString()

Statement 实例上调用 toString() 会打印扩展的 SQL 查询。这对于调试很有用。

import { Database } from "bun:sqlite";

// setup
const query = db.query("SELECT $param;");

console.log(query.toString()); // => "SELECT NULL"

query.run(42);
console.log(query.toString()); // => "SELECT 42"

query.run(365);
console.log(query.toString()); // => "SELECT 365"

在内部,这会调用 sqlite3_expanded_sql。参数使用最近绑定的值进行扩展。

参数

查询可以包含参数。这些参数可以是数字的(?1)或命名的($param:param@param)。在执行查询时将值绑定到这些参数

查询
结果
查询
const query = db.query("SELECT * FROM foo WHERE bar = $bar");
const results = query.all({
  $bar: "bar",
});
结果
[
  { "$bar": "bar" }
]

编号(位置)参数也有效

查询
结果
查询
const query = db.query("SELECT ?1, ?2");
const results = query.all("hello", "goodbye");
结果
[
  {
    "?1": "hello",
    "?2": "goodbye"
  }
]

整数

SQLite 支持有符号 64 位整数,但 JavaScript 仅支持有符号 52 位整数或具有 bigint 的任意精度整数。

bigint 输入在任何地方都受支持,但默认情况下 bun:sqlite 将整数作为 number 类型返回。如果您需要处理大于 2^53 的整数,请在创建 Database 实例时将 safeIntegers 选项设置为 true。这还会验证传递给 bun:sqlitebigint 是否未超过 64 位。

默认情况下,bun:sqlite 将整数作为 number 类型返回。如果您需要处理大于 2^53 的整数,则可以使用 bigint 类型。

safeIntegers: true

在 Bun v1.1.14 中添加

safeIntegerstrue 时,bun:sqlite 将返回 bigint 类型的整数

import { Database } from "bun:sqlite";

const db = new Database(":memory:", { safeIntegers: true });
const query = db.query(
  `SELECT ${BigInt(Number.MAX_SAFE_INTEGER) + 102n} as max_int`,
);
const result = query.get();
console.log(result.max_int); // => 9007199254741093n

safeIntegerstrue 时,如果绑定参数中的 bigint 值超过 64 位,bun:sqlite 将抛出错误

import { Database } from "bun:sqlite";

const db = new Database(":memory:", { safeIntegers: true });
db.run("CREATE TABLE test (id INTEGER PRIMARY KEY, value INTEGER)");

const query = db.query("INSERT INTO test (value) VALUES ($value)");

try {
  query.run({ $value: BigInt(Number.MAX_SAFE_INTEGER) ** 2n });
} catch (e) {
  console.log(e.message); // => BigInt value '81129638414606663681390495662081' is out of range
}

safeIntegers: false (默认)

safeIntegersfalse 时,bun:sqlite 将返回 number 类型的整数,并截断超出 53 位的任何位。

import { Database } from "bun:sqlite";

const db = new Database(":memory:", { safeIntegers: false });
const query = db.query(
  `SELECT ${BigInt(Number.MAX_SAFE_INTEGER) + 102n} as max_int`,
);
const result = query.get();
console.log(result.max_int); // => 9007199254741092

事务

事务是以原子方式执行多个查询的机制;也就是说,要么所有查询都成功,要么都不成功。使用 db.transaction() 方法创建一个事务。

const insertCat = db.prepare("INSERT INTO cats (name) VALUES ($name)");
const insertCats = db.transaction(cats => {
  for (const cat of cats) insertCat.run(cat);
});

在此阶段,我们尚未插入任何猫!调用 db.transaction() 返回一个新函数 (insertCats),该函数包装了执行查询的函数。

要执行事务,请调用此函数。所有参数都将传递给包装函数;包装函数的返回值将由事务函数返回。包装函数还可以访问在执行事务的位置定义的 this 上下文。

const insert = db.prepare("INSERT INTO cats (name) VALUES ($name)");
const insertCats = db.transaction(cats => {
  for (const cat of cats) insert.run(cat);
  return cats.length;
});

const count = insertCats([
  { $name: "Keanu" },
  { $name: "Salem" },
  { $name: "Crookshanks" },
]);

console.log(`Inserted ${count} cats`);

当调用 insertCats 时,驱动程序将自动开始一个事务,并在包装函数返回时提交它。如果抛出异常,事务将被回滚。异常将照常传播;它不会被捕获。

嵌套事务 — 可以从其他事务函数内部调用事务函数。这样做时,内部事务将成为保存点

查看嵌套事务示例

事务还具有 deferredimmediateexclusive 版本。

insertCats(cats); // uses "BEGIN"
insertCats.deferred(cats); // uses "BEGIN DEFERRED"
insertCats.immediate(cats); // uses "BEGIN IMMEDIATE"
insertCats.exclusive(cats); // uses "BEGIN EXCLUSIVE"

.loadExtension()

要加载 SQLite 扩展,请在您的 Database 实例上调用 .loadExtension(name)

import { Database } from "bun:sqlite";

const db = new Database();
db.loadExtension("myext");

对于 macOS 用户

.fileControl(cmd: number, value: any)

要使用高级 sqlite3_file_control API,请在您的 Database 实例上调用 .fileControl(cmd, value)

import { Database, constants } from "bun:sqlite";

const db = new Database();
// Ensure WAL mode is NOT persistent
// this prevents wal files from lingering after the database is closed
db.fileControl(constants.SQLITE_FCNTL_PERSIST_WAL, 0);

value 可以是

  • number
  • TypedArray
  • undefinednull

参考

class Database {
  constructor(
    filename: string,
    options?:
      | number
      | {
          readonly?: boolean;
          create?: boolean;
          readwrite?: boolean;
        },
  );

  query<Params, ReturnType>(sql: string): Statement<Params, ReturnType>;
  run(
    sql: string,
    params?: SQLQueryBindings,
  ): { lastInsertRowid: number; changes: number };
  exec = this.run;
}

class Statement<Params, ReturnType> {
  all(params: Params): ReturnType[];
  get(params: Params): ReturnType | undefined;
  run(params: Params): {
    lastInsertRowid: number;
    changes: number;
  };
  values(params: Params): unknown[][];

  finalize(): void; // destroy statement and clean up resources
  toString(): string; // serialize to SQL

  columnNames: string[]; // the column names of the result set
  paramsCount: number; // the number of parameters expected by the statement
  native: any; // the native object representing the statement

  as(Class: new () => ReturnType): this;
}

type SQLQueryBindings =
  | string
  | bigint
  | TypedArray
  | number
  | boolean
  | null
  | Record<string, string | bigint | TypedArray | number | boolean | null>;

数据类型

JavaScript 类型SQLite 类型
stringTEXT
numberINTEGERDECIMAL
booleanINTEGER (1 或 0)
Uint8ArrayBLOB
BufferBLOB
bigintINTEGER
nullNULL