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 支持
  • 单次调用 database.run(query) 中的多查询语句(例如 SELECT 1; SELECT 2;

对于读取查询,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
在 M1 MacBook Pro (64GB) 运行 macOS 12.3.1 上进行基准测试

数据库

打开或创建 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("");

readonly 模式打开

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 attribute 来加载数据库。

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 时自动 begin 事务,并在包装函数返回时 commit 它。如果抛出异常,事务将被回滚。异常将按原样传播;它不会被捕获。

嵌套事务 — 事务函数可以从其他事务函数内部调用。进行此操作时,内部事务将成为一个 savepoint

查看嵌套事务示例

事务还附带 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

Reference

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
  columnTypes: string[]; // types based on actual values in first row (call .get()/.all() first)
  declaredTypes: (string | null)[]; // types from CREATE TABLE schema (call .get()/.all() first)
  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