Bun implémente nativement un pilote SQLite3 haute performance. Pour l'utiliser, importez depuis le module intégré 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" }L'API est simple, synchrone et rapide. Crédit à better-sqlite3 et ses contributeurs pour avoir inspiré l'API de bun:sqlite.
Les fonctionnalités incluent :
- Transactions
- Paramètres (nommés et positionnels)
- Instructions préparées
- Conversions de types de données (
BLOBdevientUint8Array) - Mapper les résultats de requête vers des classes sans ORM -
query.as(MyClass) - Les performances les plus rapides de tous les pilotes SQLite pour JavaScript
- Prise en charge de
bigint - Instructions multi-requêtes (par exemple
SELECT 1; SELECT 2;) en un seul appel à database.run(query)
Le module bun:sqlite est environ 3 à 6 fois plus rapide que better-sqlite3 et 8 à 9 fois plus rapide que deno.land/x/sqlite pour les requêtes de lecture. Chaque pilote a été benchmarké avec le jeu de données Northwind Traders. Consultez et exécutez la source du benchmark.
Database
Pour ouvrir ou créer une base de données SQLite3 :
import { Database } from "bun:sqlite";
const db = new Database("mydb.sqlite");Pour ouvrir une base de données en mémoire :
import { Database } from "bun:sqlite";
// tous font la même chose
const db = new Database(":memory:");
const db = new Database();
const db = new Database("");Pour ouvrir en mode readonly :
import { Database } from "bun:sqlite";
const db = new Database("mydb.sqlite", { readonly: true });Pour créer la base de données si le fichier n'existe pas :
import { Database } from "bun:sqlite";
const db = new Database("mydb.sqlite", { create: true });Mode strict
Par défaut, bun:sqlite nécessite que les paramètres de liaison incluent le préfixe $, : ou @, et ne génère pas d'erreur si un paramètre est manquant.
Pour plutôt générer une erreur lorsqu'un paramètre est manquant et permettre la liaison sans préfixe, définissez strict: true sur le constructeur Database :
import { Database } from "bun:sqlite";
const strict = new Database(":memory:", { strict: true });
// génère une erreur à cause de la faute de frappe :
const query = strict.query("SELECT $message;").all({ messag: "Hello world" });
const notStrict = new Database(":memory:");
// ne génère pas d'erreur :
notStrict.query("SELECT $message;").all({ messag: "Hello world" });Chargement via import de module ES
Vous pouvez également utiliser un attribut d'import pour charger une base de données.
import db from "./mydb.sqlite" with { type: "sqlite" };
console.log(db.query("select * from users LIMIT 1").get());Ceci est équivalent à ce qui suit :
import { Database } from "bun:sqlite";
const db = new Database("./mydb.sqlite");.close(throwOnError: boolean = false)
Pour fermer une connexion de base de données, mais permettre aux requêtes existantes de se terminer, appelez .close(false) :
const db = new Database();
// ... faire des choses
db.close(false);Pour fermer la base de données et générer une erreur s'il y a des requêtes en attente, appelez .close(true) :
const db = new Database();
// ... faire des choses
db.close(true);NOTE
`close(false)` est appelé automatiquement lorsque la base de données est garbage collectée. Il est sûr de l'appeler plusieurs fois mais n'a aucun effet après le premier appel.Instruction using
Vous pouvez utiliser l'instruction using pour vous assurer qu'une connexion de base de données est fermée lorsque le bloc using est quitté.
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 prend en charge le mécanisme intégré de SQLite pour sérialiser et désérialiser des bases de données vers et depuis la mémoire.
const olddb = new Database("mydb.sqlite");
const contents = olddb.serialize(); // => Uint8Array
const newdb = Database.deserialize(contents);En interne, .serialize() appelle sqlite3_serialize.
.query()
Utilisez la méthode db.query() sur votre instance Database pour préparer une requête SQL. Le résultat est une instance Statement qui sera mise en cache sur l'instance Database. La requête ne sera pas exécutée.
const query = db.query(`select "Hello world" as message`);NOTE
**Que signifie "mis en cache" ?**La mise en cache fait référence à l'instruction préparée compilée (le bytecode SQL), pas aux résultats de la requête. Lorsque vous appelez db.query() avec la même chaîne SQL plusieurs fois, Bun renvoie le même objet Statement mis en cache au lieu de recompiler le SQL.
Il est tout à fait sûr de réutiliser une instruction mise en cache avec différentes valeurs de paramètres :
const query = db.query("SELECT * FROM users WHERE id = ?");
query.get(1); // ✓ Fonctionne
query.get(2); // ✓ Fonctionne aussi - les paramètres sont liés à nouveau à chaque fois
query.get(3); // ✓ Fonctionne toujoursUtilisez .prepare() au lieu de .query() lorsque vous voulez une instance Statement fraîche qui n'est pas mise en cache, par exemple si vous générez dynamiquement du SQL et ne voulez pas remplir le cache avec des requêtes uniques.
// compiler l'instruction préparée sans mise en cache
const query = db.prepare("SELECT * FROM foo WHERE bar = ?");Mode WAL
SQLite prend en charge le mode write-ahead log (WAL) qui améliore considérablement les performances, en particulier dans les situations avec de nombreux lecteurs concurrents et un seul rédacteur. Il est largement recommandé d'activer le mode WAL pour la plupart des applications typiques.
Pour activer le mode WAL, exécutez cette requête pragma au début de votre application :
db.run("PRAGMA journal_mode = WAL;");Qu'est-ce que le mode WAL ?">
En mode WAL, les écritures dans la base de données sont écrites directement dans un fichier séparé appelé "fichier WAL" (write-ahead log). Ce fichier sera ensuite intégré dans le fichier de base de données principal. Considérez-le comme un tampon pour les écritures en attente. Consultez la documentation SQLite pour un aperçu plus détaillé.
Sur macOS, les fichiers WAL peuvent être persistants par défaut. Ce n'est pas un bug, c'est ainsi que macOS a configuré la version système de SQLite.
Statements
Une Statement est une requête préparée, ce qui signifie qu'elle a été analysée et compilée dans une forme binaire efficace. Elle peut être exécutée plusieurs fois de manière performante.
Créez une instruction avec la méthode .query sur votre instance Database.
const query = db.query(`select "Hello world" as message`);Les requêtes peuvent contenir des paramètres. Ceux-ci peuvent être numériques (?1) ou nommés ($param ou :param ou @param).
const query = db.query(`SELECT ?1, ?2;`);
const query = db.query(`SELECT $param1, $param2;`);Les valeurs sont liées à ces paramètres lorsque la requête est exécutée. Une Statement peut être exécutée avec plusieurs méthodes différentes, chacune renvoyant les résultats sous une forme différente.
Liaison de valeurs
Pour lier des valeurs à une instruction, passez un objet à la méthode .all(), .get(), .run() ou .values().
const query = db.query(`select $message;`);
query.all({ $message: "Hello world" });Vous pouvez également lier en utilisant des paramètres positionnels :
const query = db.query(`select ?1;`);
query.all("Hello world");strict: true vous permet de lier des valeurs sans préfixes
Par défaut, les préfixes $, : et @ sont inclus lors de la liaison de valeurs aux paramètres nommés. Pour lier sans ces préfixes, utilisez l'option strict dans le constructeur Database.
import { Database } from "bun:sqlite";
const db = new Database(":memory:", {
// lier des valeurs sans préfixes
strict: true,
});
const query = db.query(`select $message;`);
// strict: true
query.all({ message: "Hello world" });
// strict: false
// query.all({ $message: "Hello world" });.all()
Utilisez .all() pour exécuter une requête et obtenir les résultats sous forme de tableau d'objets.
const query = db.query(`select $message;`);
query.all({ $message: "Hello world" });[{ message: "Hello world" }]En interne, cela appelle sqlite3_reset et appelle répétitivement sqlite3_step jusqu'à ce qu'il renvoie SQLITE_DONE.
.get()
Utilisez .get() pour exécuter une requête et obtenir le premier résultat sous forme d'objet.
const query = db.query(`select $message;`);
query.get({ $message: "Hello world" });{ $message: "Hello world" }En interne, cela appelle sqlite3_reset suivi de sqlite3_step jusqu'à ce qu'il ne renvoie plus SQLITE_ROW. Si la requête ne renvoie aucune ligne, undefined est renvoyé.
.run()
Utilisez .run() pour exécuter une requête et obtenir undefined. Cela est utile pour les requêtes de modification de schéma (par exemple CREATE TABLE) ou les opérations d'écriture en vrac.
const query = db.query(`create table foo;`);
query.run();{
lastInsertRowid: 0,
changes: 0,
}En interne, cela appelle sqlite3_reset et appelle sqlite3_step une fois. Parcourir toutes les lignes n'est pas nécessaire lorsque vous ne vous souciez pas des résultats.
La propriété lastInsertRowid renvoie l'ID de la dernière ligne insérée dans la base de données. La propriété changes est le nombre de lignes affectées par la requête.
.as(Class) - Mapper les résultats de requête vers une classe
Utilisez .as(Class) pour exécuter une requête et obtenir les résultats sous forme d'instances d'une classe. Cela vous permet d'attacher des méthodes et getters/setters aux résultats.
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
trueComme optimisation de performance, le constructeur de classe n'est pas appelé, les initialiseurs par défaut ne sont pas exécutés et les champs privés ne sont pas accessibles. C'est plus comme utiliser Object.create que new. Le prototype de la classe est assigné à l'objet, les méthodes sont attachées et les getters/setters sont configurés, mais le constructeur n'est pas appelé.
Les colonnes de la base de données sont définies comme propriétés sur l'instance de classe.
.iterate() (@@iterator)
Utilisez .iterate() pour exécuter une requête et renvoyer incrémentalement les résultats. Cela est utile pour les grands ensembles de résultats que vous souhaitez traiter une ligne à la fois sans charger tous les résultats en mémoire.
const query = db.query("SELECT * FROM foo");
for (const row of query.iterate()) {
console.log(row);
}Vous pouvez également utiliser le protocole @@iterator :
const query = db.query("SELECT * FROM foo");
for (const row of query) {
console.log(row);
}.values()
Utilisez values() pour exécuter une requête et obtenir tous les résultats sous forme de tableau de tableaux.
const query = db.query(`select $message;`);
query.values({ $message: "Hello world" });
query.values(2);[
[ "Iron Man", 2008 ],
[ "The Avengers", 2012 ],
[ "Ant-Man: Quantumania", 2023 ],
]En interne, cela appelle sqlite3_reset et appelle répétitivement sqlite3_step jusqu'à ce qu'il renvoie SQLITE_DONE.
.finalize()
Utilisez .finalize() pour détruire une Statement et libérer toutes les ressources qui y sont associées. Une fois finalisée, une Statement ne peut plus être exécutée. Typiquement, le garbage collector le fera pour vous, mais la finalisation explicite peut être utile dans les applications sensibles aux performances.
const query = db.query("SELECT title, year FROM movies");
const movies = query.all();
query.finalize();.toString()
Appeler toString() sur une instance Statement affiche la requête SQL étendue. Cela est utile pour le débogage.
import { Database } from "bun:sqlite";
// configuration
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"En interne, cela appelle sqlite3_expanded_sql. Les paramètres sont étendus en utilisant les valeurs liées les plus récemment.
Paramètres
Les requêtes peuvent contenir des paramètres. Ceux-ci peuvent être numériques (?1) ou nommés ($param ou :param ou @param). Liez des valeurs à ces paramètres lors de l'exécution de la requête :
const query = db.query("SELECT * FROM foo WHERE bar = $bar");
const results = query.all({
$bar: "bar",
});[{ "$bar": "bar" }]Les paramètres numérotés (positionnels) fonctionnent également :
const query = db.query("SELECT ?1, ?2");
const results = query.all("hello", "goodbye");[
{
"?1": "hello",
"?2": "goodbye",
},
];Integers
SQLite prend en charge les entiers signés 64 bits, mais JavaScript ne prend en charge que les entiers signés 52 bits ou les entiers à précision arbitraire avec bigint.
L'entrée bigint est prise en charge partout, mais par défaut bun:sqlite renvoie les entiers comme types number. Si vous devez gérer des entiers plus grands que 2^53, définissez l'option safeIntegers sur true lors de la création d'une instance Database. Cela valide également que les bigint passés à bun:sqlite ne dépassent pas 64 bits.
Par défaut, bun:sqlite renvoie les entiers comme types number. Si vous devez gérer des entiers plus grands que 2^53, vous pouvez utiliser le type bigint.
safeIntegers: true
Lorsque safeIntegers est true, bun:sqlite renverra les entiers comme types 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);9007199254741093nLorsque safeIntegers est true, bun:sqlite générera une erreur si une valeur bigint dans un paramètre lié dépasse 64 bits :
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);
}La valeur BigInt '81129638414606663681390495662081' est hors de portéesafeIntegers: false (par défaut)
Lorsque safeIntegers est false, bun:sqlite renverra les entiers comme types number et tronquera tous les bits au-delà de 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);9007199254741092Transactions
Les transactions sont un mécanisme pour exécuter plusieurs requêtes de manière atomique ; c'est-à-dire que soit toutes les requêtes réussissent, soit aucune ne réussit. Créez une transaction avec la méthode 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);
});À ce stade, nous n'avons inséré aucun chat ! L'appel à db.transaction() renvoie une nouvelle fonction (insertCats) qui enveloppe la fonction qui exécute les requêtes.
Pour exécuter la transaction, appelez cette fonction. Tous les arguments seront transmis à la fonction enveloppée ; la valeur de retour de la fonction enveloppée sera renvoyée par la fonction de transaction. La fonction enveloppée a également accès au contexte this tel que défini où la transaction est exécutée.
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`);Le pilote démarrera automatiquement begin une transaction lorsque insertCats est appelé et la commit lorsque la fonction enveloppée retourne. Si une exception est levée, la transaction sera annulée. L'exception se propagera comme d'habitude ; elle n'est pas attrapée.
NOTE
**Transactions imbriquées** — Les fonctions de transaction peuvent être appelées depuis l'intérieur d'autres fonctions de transaction. Ce faisant, la transaction interne devient un [savepoint](https://www.sqlite.org/lang_savepoint.html).Voir l'exemple de transaction imbriquée">
// configuration
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); // transaction imbriquée
});
adopt([
{ $name: "Joey", $age: 2 },
{ $name: "Sally", $age: 4 },
{ $name: "Junior", $age: 1 },
]);Les transactions sont également disponibles avec les versions deferred, immediate et exclusive.
insertCats(cats); // utilise "BEGIN"
insertCats.deferred(cats); // utilise "BEGIN DEFERRED"
insertCats.immediate(cats); // utilise "BEGIN IMMEDIATE"
insertCats.exclusive(cats); // utilise "BEGIN EXCLUSIVE".loadExtension()
Pour charger une extension SQLite, appelez .loadExtension(name) sur votre instance Database
import { Database } from "bun:sqlite";
const db = new Database();
db.loadExtension("myext");NOTE
**Utilisateurs MacOS** Par défaut, macOS est livré avec la build propriétaire d'Apple de SQLite, qui ne prend pas en charge les extensions. Pour utiliser des extensions, vous devrez installer une build vanilla de SQLite.brew install sqlite
which sqlite # obtenir le chemin du binairePour pointer bun:sqlite vers la nouvelle build, appelez Database.setCustomSQLite(path) avant de créer des instances Database. (Sur d'autres systèmes d'exploitation, c'est un no-op.) Passez un chemin vers le fichier .dylib SQLite, pas l'exécutable. Avec les versions récentes de Homebrew, c'est quelque chose comme /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)
Pour utiliser l'API avancée sqlite3_file_control, appelez .fileControl(cmd, value) sur votre instance Database.
import { Database, constants } from "bun:sqlite";
const db = new Database();
// S'assurer que le mode WAL n'est PAS persistant
// cela empêche les fichiers wal de persister après la fermeture de la base de données
db.fileControl(constants.SQLITE_FCNTL_PERSIST_WAL, 0);value peut être :
numberTypedArrayundefinedounull
Reference
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; // détruire l'instruction et nettoyer les ressources
toString(): string; // sérialiser en SQL
columnNames: string[]; // les noms de colonnes du jeu de résultats
columnTypes: string[]; // types basés sur les valeurs réelles dans la première ligne (appelez .get()/.all() d'abord)
declaredTypes: (string | null)[]; // types du schéma CREATE TABLE (appelez .get()/.all() d'abord)
paramsCount: number; // le nombre de paramètres attendus par l'instruction
native: any; // l'objet natif représentant l'instruction
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>;Datatypes
| Type JavaScript | Type SQLite |
|---|---|
string | TEXT |
number | INTEGER ou DECIMAL |
boolean | INTEGER (1 ou 0) |
Uint8Array | BLOB |
Buffer | BLOB |
bigint | INTEGER |
null | NULL |