Skip to content

Bun нативно реализует высокопроизводительный драйвер SQLite3. Для его использования импортируйте из встроенного модуля bun:sqlite.

ts
import { Database } from "bun:sqlite";

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

API простой, синхронный и быстрый. Благодарность better-sqlite3 и его участникам за вдохновение API bun:sqlite.

Возможности включают:

  • Транзакции
  • Параметры (именованные и позиционные)
  • Подготовленные выражения
  • Преобразования типов данных (BLOB становится Uint8Array)
  • Сопоставление результатов запроса с классами без ORM - query.as(MyClass)
  • Самая высокая производительность среди всех драйверов SQLite для JavaScript
  • Поддержка bigint
  • Множественные выражения в одном вызове (например, SELECT 1; SELECT 2;) в одном вызове database.run(query)

Модуль bun:sqlite примерно в 3-6 раз быстрее, чем better-sqlite3, и в 8-9 раз быстрее, чем deno.land/x/sqlite для запросов чтения. Каждый драйвер был протестирован на наборе данных Northwind Traders. Просмотрите и запустите исходный код бенчмарка.


Database

Для открытия или создания базы данных SQLite3:

ts
import { Database } from "bun:sqlite";

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

Для открытия базы данных в памяти:

ts
import { Database } from "bun:sqlite";

// все они делают одно и то же
const db = new Database(":memory:");
const db = new Database();
const db = new Database("");

Для открытия в режиме readonly:

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

Для создания базы данных, если файл не существует:

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

Строгий режим

По умолчанию bun:sqlite требует, чтобы параметры привязки включали префикс $, : или @, и не выбрасывает ошибку, если параметр отсутствует.

Чтобы выбрасывать ошибку при отсутствии параметра и разрешить привязку без префикса, установите strict: true в конструкторе Database:

ts
import { Database } from "bun:sqlite";

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

// выбрасывает ошибку из-за опечатки:
const query = strict.query("SELECT $message;").all({ messag: "Hello world" });

const notStrict = new Database(":memory:");
// не выбрасывает ошибку:
notStrict.query("SELECT $message;").all({ messag: "Hello world" });

Загрузка через импорт ES-модуля

Вы также можете использовать атрибут импорта для загрузки базы данных.

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

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

Это эквивалентно следующему:

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

.close(throwOnError: boolean = false)

Чтобы закрыть подключение к базе данных, но позволить существующим запросам завершиться, вызовите .close(false):

ts
const db = new Database();
// ... делаем дела
db.close(false);

Чтобы закрыть базу данных и выбросить ошибку, если есть какие-либо ожидающие запросы, вызовите .close(true):

ts
const db = new Database();
// ... делаем дела
db.close(true);

NOTE

`close(false)` вызывается автоматически, когда база данных собирается сборщиком мусора. Безопасно вызывать несколько раз, но не имеет эффекта после первого вызова.

Оператор using

Вы можете использовать оператор using, чтобы убедиться, что подключение к базе данных закрыто при выходе из блока using.

ts
import { Database } from "bun:sqlite";

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

.serialize()

bun:sqlite поддерживает встроенный механизм SQLite для сериализации и десериализации баз данных в память и из памяти.

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

Внутри .serialize() вызывает sqlite3_serialize.

.query()

Используйте метод db.query() на вашем экземпляре Database для подготовки SQL-запроса. Результатом является экземпляр Statement, который будет кэширован в экземпляре Database. Запрос не будет выполнен.

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

NOTE

**Что означает "кэшировано"?**

Кэширование относится к скомпилированному подготовленному выражению (SQL-байт-код), а не к результатам запроса. Когда вы вызываете db.query() с одной и той же SQL-строкой несколько раз, Bun возвращает один и тот же кэшированный объект Statement вместо перекомпиляции SQL.

Совершенно безопасно повторно использовать кэшированное выражение с разными значениями параметров:

ts
const query = db.query("SELECT * FROM users WHERE id = ?");
query.get(1); // ✓ Работает
query.get(2); // ✓ Также работает - параметры привязываются заново каждый раз
query.get(3); // ✓ Все еще работает

Используйте .prepare() вместо .query(), когда вам нужен свежий экземпляр Statement, который не кэшируется, например, если вы динамически генерируете SQL и не хотите заполнять кэш одноразовыми запросами.

ts
// компилируем подготовленное выражение без кэширования
const query = db.prepare("SELECT * FROM foo WHERE bar = ?");

Режим WAL

SQLite поддерживает режим журнала упреждающей записи (WAL), который значительно улучшает производительность, особенно в ситуациях со многими одновременными читателями и одним писателем. В целом рекомендуется включать режим WAL для большинства типичных приложений.

Чтобы включить режим WAL, выполните этот pragma-запрос в начале вашего приложения:

ts
db.run("PRAGMA journal_mode = WAL;");

Что такое режим WAL?">

В режиме WAL запись в базу данных записывается напрямую в отдельный файл, называемый "WAL-файл" (журнал упреждающей записи). Этот файл будет позже интегрирован в основной файл базы данных. Думайте об этом как о буфере для ожидающих записей. Обратитесь к документации SQLite для более подробного обзора.

На macOS WAL-файлы могут быть постоянными по умолчанию. Это не ошибка, так macOS настроила системную версию SQLite.


Statements

Statement — это подготовленный запрос, что означает, что он был разобран и скомпилирован в эффективную бинарную форму. Он может быть выполнен несколько раз производительным способом.

Создайте выражение с помощью метода .query на вашем экземпляре Database.

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

Запросы могут содержать параметры. Они могут быть числовыми (?1) или именованными ($param или :param или @param).

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

Значения привязываются к этим параметрам при выполнении запроса. Statement может быть выполнен с помощью нескольких различных методов, каждый из которых возвращает результаты в разной форме.

Привязка значений

Чтобы привязать значения к выражению, передайте объект в метод .all(), .get(), .run() или .values().

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

Вы также можете привязывать с помощью позиционных параметров:

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

strict: true позволяет привязывать значения без префиксов

По умолчанию префиксы $, : и @ включаются при привязке значений к именованным параметрам. Чтобы привязывать без этих префиксов, используйте опцию strict в конструкторе Database.

ts
import { Database } from "bun:sqlite";

const db = new Database(":memory:", {
  // привязка значений без префиксов
  strict: true, 
});

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

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

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

.all()

Используйте .all() для выполнения запроса и получения результатов в виде массива объектов.

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

Внутри это вызывает sqlite3_reset и многократно вызывает sqlite3_step, пока не вернется SQLITE_DONE.

.get()

Используйте .get() для выполнения запроса и получения первого результата в виде объекта.

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

Внутри это вызывает sqlite3_reset, за которым следует sqlite3_step, пока не перестанет возвращаться SQLITE_ROW. Если запрос не возвращает строк, возвращается undefined.

.run()

Используйте .run() для выполнения запроса и получения undefined. Это полезно для запросов, изменяющих схему (например, CREATE TABLE) или массовых операций записи.

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

Внутри это вызывает sqlite3_reset и вызывает sqlite3_step один раз. Проход по всем строкам не нужен, когда вас не волнуют результаты.

Свойство lastInsertRowid возвращает ID последней строки, вставленной в базу данных. Свойство changes — это количество строк, затронутых запросом.

.as(Class) - Сопоставление результатов запроса с классом

Используйте .as(Class) для выполнения запроса и получения результатов в виде экземпляров класса. Это позволяет добавлять методы и геттеры/сеттеры к результатам.

ts
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);
console.log(first.isMarvel);
txt
true
true

В качестве оптимизации производительности конструктор класса не вызывается, инициализаторы по умолчанию не выполняются, и приватные поля недоступны. Это больше похоже на использование Object.create, чем new. Прототип класса присваивается объекту, методы прикрепляются, геттеры/сеттеры настраиваются, но конструктор не вызывается.

Столбцы базы данных устанавливаются как свойства экземпляра класса.

.iterate() (@@iterator)

Используйте .iterate() для выполнения запроса и постепенного возврата результатов. Это полезно для больших наборов результатов, которые вы хотите обрабатывать по одной строке за раз, не загружая все результаты в память.

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

Вы также можете использовать протокол @@iterator:

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

.values()

Используйте values() для выполнения запроса и получения всех результатов в виде массива массивов.

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

query.values({ $message: "Hello world" });
query.values(2);
txt
[
  [ "Iron Man", 2008 ],
  [ "The Avengers", 2012 ],
  [ "Ant-Man: Quantumania", 2023 ],
]

Внутри это вызывает sqlite3_reset и многократно вызывает sqlite3_step, пока не вернется SQLITE_DONE.

.finalize()

Используйте .finalize() для уничтожения Statement и освобождения всех ресурсов, связанных с ним. После финализации Statement не может быть выполнен снова. Обычно сборщик мусора сделает это за вас, но явная финализация может быть полезна в приложениях, чувствительных к производительности.

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

.toString()

Вызов toString() на экземпляре Statement выводит развернутое SQL-выражение. Это полезно для отладки.

ts
import { Database } from "bun:sqlite";

// настройка
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). Привяжите значения к этим параметрам при выполнении запроса:

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

Нумерованные (позиционные) параметры также работают:

ts
const query = db.query("SELECT ?1, ?2");
const results = query.all("hello", "goodbye");
txt
[
	{
		"?1": "hello",
		"?2": "goodbye",
	},
];

Целые числа

SQLite поддерживает знаковые 64-битные целые числа, но JavaScript поддерживает только знаковые 52-битные целые числа или целые числа произвольной точности с bigint.

Ввод bigint поддерживается везде, но по умолчанию bun:sqlite возвращает целые числа как типы number. Если вам нужно обрабатывать целые числа больше, чем 2^53, установите опцию safeIntegers в true при создании экземпляра Database. Это также проверяет, что bigint, переданные в bun:sqlite, не превышают 64 бита.

По умолчанию bun:sqlite возвращает целые числа как типы number. Если вам нужно обрабатывать целые числа больше, чем 2^53, вы можете использовать тип bigint.

safeIntegers: true

Когда safeIntegers равно true, bun:sqlite будет возвращать целые числа как типы bigint:

ts
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);
txt
9007199254741093n

Когда safeIntegers равно true, bun:sqlite выбросит ошибку, если значение bigint в привязанном параметре превышает 64 бита:

ts
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);
}
txt
Значение BigInt '81129638414606663681390495662081' вне диапазона

safeIntegers: false (по умолчанию)

Когда safeIntegers равно false, bun:sqlite будет возвращать целые числа как типы number и усекать все биты за пределами 53:

ts
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);
txt
9007199254741092

Транзакции

Транзакции — это механизм для выполнения нескольких запросов атомарным способом; то есть либо все запросы выполняются успешно, либо ни один из них не выполняется. Создайте транзакцию с помощью метода db.transaction():

ts
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, как определено там, где выполняется транзакция.

ts
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(`Вставлено ${count} кошек`);

Драйвер автоматически begin транзакцию при вызове insertCats и commit её, когда обернутая функция возвращает. Если выбрасывается исключение, транзакция будет откатана. Исключение будет распространяться как обычно; оно не перехватывается.

NOTE

**Вложенные транзакции** — Функции транзакций могут вызываться изнутри других функций транзакций. При этом внутренняя транзакция становится [точкой сохранения](https://www.sqlite.org/lang_savepoint.html).

Пример вложенной транзакции">

ts
// настройка
import { Database } from "bun:sqlite";
const db = Database.open(":memory:");
db.run("CREATE TABLE expenses (id INTEGER PRIMARY KEY AUTOINCREMENT, note TEXT, dollars INTEGER);");
db.run("CREATE TABLE cats (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE, age INTEGER)");
const insertExpense = db.prepare("INSERT INTO expenses (note, dollars) VALUES (?, ?)");
const insert = db.prepare("INSERT INTO cats (name, age) VALUES ($name, $age)");
const insertCats = db.transaction(cats => {
  for (const cat of cats) insert.run(cat);
});

const adopt = db.transaction(cats => {
  insertExpense.run("adoption fees", 20);
  insertCats(cats); // вложенная транзакция
});

adopt([
  { $name: "Joey", $age: 2 },
  { $name: "Sally", $age: 4 },
  { $name: "Junior", $age: 1 },
]);

Транзакции также поставляются с версиями deferred, immediate и exclusive.

ts
insertCats(cats); // использует "BEGIN"
insertCats.deferred(cats); // использует "BEGIN DEFERRED"
insertCats.immediate(cats); // использует "BEGIN IMMEDIATE"
insertCats.exclusive(cats); // использует "BEGIN EXCLUSIVE"

.loadExtension()

Для загрузки расширения SQLite вызовите .loadExtension(name) на вашем экземпляре Database

ts
import { Database } from "bun:sqlite";

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

NOTE

**Пользователи MacOS** По умолчанию macOS поставляется с проприетарной сборкой SQLite от Apple, которая не поддерживает расширения. Для использования расширений вам нужно будет установить стандартную сборку SQLite.
bash
brew install sqlite
which sqlite # получить путь к бинарнику

Чтобы указать bun:sqlite на новую сборку, вызовите Database.setCustomSQLite(path) перед созданием любых экземпляров Database. (В других операционных системах это no-op.) Передайте путь к .dylib файлу SQLite, не исполняемому файлу. С последними версиями Homebrew это что-то вроде /opt/homebrew/Cellar/sqlite/<version>/libsqlite3.dylib.

ts
import { Database } from "bun:sqlite";

Database.setCustomSQLite("/path/to/libsqlite.dylib");

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

.fileControl(cmd: number, value: any)

Для использования расширенного API sqlite3_file_control вызовите .fileControl(cmd, value) на вашем экземпляре Database.

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

const db = new Database();
// Убедитесь, что WAL не является постоянным
// это предотвращает задержку wal-файлов после закрытия базы данных
db.fileControl(constants.SQLITE_FCNTL_PERSIST_WAL, 0);

value может быть:

  • number
  • TypedArray
  • undefined или null

Справочник

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

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

  transaction(insideTransaction: (...args: any) => void): CallableFunction & {
    deferred: (...args: any) => void;
    immediate: (...args: any) => void;
    exclusive: (...args: any) => void;
  };

  close(throwOnError?: boolean): void;
}

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

  finalize(): void; // уничтожить выражение и очистить ресурсы
  toString(): string; // сериализовать в SQL

  columnNames: string[]; // имена столбцов набора результатов
  columnTypes: string[]; // типы на основе фактических значений в первой строке (вызовите .get()/.all() сначала)
  declaredTypes: (string | null)[]; // типы из схемы CREATE TABLE (вызовите .get()/.all() сначала)
  paramsCount: number; // количество параметров, ожидаемых выражением
  native: any; // нативный объект, представляющий выражение

  as<T>(Class: new (...args: any[]) => T): Statement<T, ParamsType>;
}

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

Типы данных

Тип JavaScriptТип SQLite
stringTEXT
numberINTEGER или DECIMAL
booleanINTEGER (1 или 0)
Uint8ArrayBLOB
BufferBLOB
bigintINTEGER
nullNULL

Bun от www.bunjs.com.cn