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 и его участникам за вдохновение 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:
import { Database } from "bun:sqlite";
const db = new Database("mydb.sqlite");Для открытия базы данных в памяти:
import { Database } from "bun:sqlite";
// все они делают одно и то же
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:sqlite требует, чтобы параметры привязки включали префикс $, : или @, и не выбрасывает ошибку, если параметр отсутствует.
Чтобы выбрасывать ошибку при отсутствии параметра и разрешить привязку без префикса, установите strict: true в конструкторе Database:
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-модуля
Вы также можете использовать атрибут импорта для загрузки базы данных.
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();
// ... делаем дела
db.close(false);Чтобы закрыть базу данных и выбросить ошибку, если есть какие-либо ожидающие запросы, вызовите .close(true):
const db = new Database();
// ... делаем дела
db.close(true);NOTE
`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()
Используйте метод db.query() на вашем экземпляре Database для подготовки SQL-запроса. Результатом является экземпляр Statement, который будет кэширован в экземпляре Database. Запрос не будет выполнен.
const query = db.query(`select "Hello world" as message`);NOTE
**Что означает "кэшировано"?**Кэширование относится к скомпилированному подготовленному выражению (SQL-байт-код), а не к результатам запроса. Когда вы вызываете db.query() с одной и той же SQL-строкой несколько раз, Bun возвращает один и тот же кэшированный объект Statement вместо перекомпиляции SQL.
Совершенно безопасно повторно использовать кэшированное выражение с разными значениями параметров:
const query = db.query("SELECT * FROM users WHERE id = ?");
query.get(1); // ✓ Работает
query.get(2); // ✓ Также работает - параметры привязываются заново каждый раз
query.get(3); // ✓ Все еще работаетИспользуйте .prepare() вместо .query(), когда вам нужен свежий экземпляр Statement, который не кэшируется, например, если вы динамически генерируете SQL и не хотите заполнять кэш одноразовыми запросами.
// компилируем подготовленное выражение без кэширования
const query = db.prepare("SELECT * FROM foo WHERE bar = ?");Режим WAL
SQLite поддерживает режим журнала упреждающей записи (WAL), который значительно улучшает производительность, особенно в ситуациях со многими одновременными читателями и одним писателем. В целом рекомендуется включать режим WAL для большинства типичных приложений.
Чтобы включить режим WAL, выполните этот pragma-запрос в начале вашего приложения:
db.run("PRAGMA journal_mode = WAL;");Что такое режим WAL?">
В режиме WAL запись в базу данных записывается напрямую в отдельный файл, называемый "WAL-файл" (журнал упреждающей записи). Этот файл будет позже интегрирован в основной файл базы данных. Думайте об этом как о буфере для ожидающих записей. Обратитесь к документации SQLite для более подробного обзора.
На macOS WAL-файлы могут быть постоянными по умолчанию. Это не ошибка, так macOS настроила системную версию SQLite.
Statements
Statement — это подготовленный запрос, что означает, что он был разобран и скомпилирован в эффективную бинарную форму. Он может быть выполнен несколько раз производительным способом.
Создайте выражение с помощью метода .query на вашем экземпляре Database.
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 позволяет привязывать значения без префиксов
По умолчанию префиксы $, : и @ включаются при привязке значений к именованным параметрам. Чтобы привязывать без этих префиксов, используйте опцию strict в конструкторе Database.
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() для выполнения запроса и получения результатов в виде массива объектов.
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 один раз. Проход по всем строкам не нужен, когда вас не волнуют результаты.
Свойство lastInsertRowid возвращает ID последней строки, вставленной в базу данных. Свойство changes — это количество строк, затронутых запросом.
.as(Class) - Сопоставление результатов запроса с классом
Используйте .as(Class) для выполнения запроса и получения результатов в виде экземпляров класса. Это позволяет добавлять методы и геттеры/сеттеры к результатам.
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);true
trueВ качестве оптимизации производительности конструктор класса не вызывается, инициализаторы по умолчанию не выполняются, и приватные поля недоступны. Это больше похоже на использование Object.create, чем new. Прототип класса присваивается объекту, методы прикрепляются, геттеры/сеттеры настраиваются, но конструктор не вызывается.
Столбцы базы данных устанавливаются как свойства экземпляра класса.
.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);
}.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()
Вызов toString() на экземпляре Statement выводит развернутое SQL-выражение. Это полезно для отладки.
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). Привяжите значения к этим параметрам при выполнении запроса:
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, установите опцию safeIntegers в true при создании экземпляра Database. Это также проверяет, что bigint, переданные в bun:sqlite, не превышают 64 бита.
По умолчанию bun:sqlite возвращает целые числа как типы number. Если вам нужно обрабатывать целые числа больше, чем 2^53, вы можете использовать тип bigint.
safeIntegers: true
Когда safeIntegers равно true, 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Когда safeIntegers равно true, bun:sqlite выбросит ошибку, если значение bigint в привязанном параметре превышает 64 бита:
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 '81129638414606663681390495662081' вне диапазонаsafeIntegers: false (по умолчанию)
Когда safeIntegers равно false, 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(`Вставлено ${count} кошек`);Драйвер автоматически begin транзакцию при вызове insertCats и commit её, когда обернутая функция возвращает. Если выбрасывается исключение, транзакция будет откатана. Исключение будет распространяться как обычно; оно не перехватывается.
NOTE
**Вложенные транзакции** — Функции транзакций могут вызываться изнутри других функций транзакций. При этом внутренняя транзакция становится [точкой сохранения](https://www.sqlite.org/lang_savepoint.html).Пример вложенной транзакции">
// настройка
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.
insertCats(cats); // использует "BEGIN"
insertCats.deferred(cats); // использует "BEGIN DEFERRED"
insertCats.immediate(cats); // использует "BEGIN IMMEDIATE"
insertCats.exclusive(cats); // использует "BEGIN EXCLUSIVE".loadExtension()
Для загрузки расширения SQLite вызовите .loadExtension(name) на вашем экземпляре Database
import { Database } from "bun:sqlite";
const db = new Database();
db.loadExtension("myext");NOTE
**Пользователи MacOS** По умолчанию macOS поставляется с проприетарной сборкой SQLite от Apple, которая не поддерживает расширения. Для использования расширений вам нужно будет установить стандартную сборку SQLite.brew install sqlite
which sqlite # получить путь к бинарникуЧтобы указать bun:sqlite на новую сборку, вызовите Database.setCustomSQLite(path) перед созданием любых экземпляров Database. (В других операционных системах это no-op.) Передайте путь к .dylib файлу SQLite, не исполняемому файлу. С последними версиями Homebrew это что-то вроде /opt/homebrew/Cellar/sqlite/<version>/libsqlite3.dylib.
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.
import { Database, constants } from "bun:sqlite";
const db = new Database();
// Убедитесь, что WAL не является постоянным
// это предотвращает задержку wal-файлов после закрытия базы данных
db.fileControl(constants.SQLITE_FCNTL_PERSIST_WAL, 0);value может быть:
numberTypedArrayundefinedилиnull
Справочник
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 |
|---|---|
string | TEXT |
number | INTEGER или DECIMAL |
boolean | INTEGER (1 или 0) |
Uint8Array | BLOB |
Buffer | BLOB |
bigint | INTEGER |
null | NULL |