Bun implementa nativamente un driver SQLite3 ad alte prestazioni. Per usarlo importa dal modulo built-in 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 è semplice, sincrona e veloce. Credito a better-sqlite3 e ai suoi collaboratori per aver ispirato l'API di bun:sqlite.
Le funzionalità includono:
- Transazioni
- Parametri (nominati e posizionali)
- Prepared statement
- Conversioni di tipi di dati (
BLOBdiventaUint8Array) - Mappa i risultati delle query a classi senza un ORM -
query.as(MyClass) - Le prestazioni più veloci di qualsiasi driver SQLite per JavaScript
- Supporto per
bigint - Istruzioni multi-query (es.
SELECT 1; SELECT 2;) in una singola chiamata a database.run(query)
Il modulo bun:sqlite è circa 3-6 volte più veloce di better-sqlite3 e 8-9 volte più veloce di deno.land/x/sqlite per le query di lettura. Ogni driver è stato testato con il dataset Northwind Traders. Visualizza ed esegui il codice del benchmark.
Database
Per aprire o creare un database SQLite3:
import { Database } from "bun:sqlite";
const db = new Database("mydb.sqlite");Per aprire un database in memoria:
import { Database } from "bun:sqlite";
// tutti questi fanno la stessa cosa
const db = new Database(":memory:");
const db = new Database();
const db = new Database("");Per aprire in modalità readonly:
import { Database } from "bun:sqlite";
const db = new Database("mydb.sqlite", { readonly: true });Per creare il database se il file non esiste:
import { Database } from "bun:sqlite";
const db = new Database("mydb.sqlite", { create: true });Modalità strict
Per impostazione predefinita, bun:sqlite richiede che i parametri di binding includano il prefisso $, :, o @, e non lancia un errore se un parametro è mancante.
Per lanciare invece un errore quando un parametro è mancante e permettere il binding senza prefisso, imposta strict: true nel costruttore del Database:
import { Database } from "bun:sqlite";
const strict = new Database(":memory:", { strict: true });
// lancia errore a causa del typo:
const query = strict.query("SELECT $message;").all({ messag: "Hello world" });
const notStrict = new Database(":memory:");
// non lancia errore:
notStrict.query("SELECT $message;").all({ messag: "Hello world" });Carica tramite import di ES module
Puoi anche usare un import attribute per caricare un database.
import db from "./mydb.sqlite" with { type: "sqlite" };
console.log(db.query("select * from users LIMIT 1").get());Questo è equivalente a:
import { Database } from "bun:sqlite";
const db = new Database("./mydb.sqlite");.close(throwOnError: boolean = false)
Per chiudere una connessione al database, ma permettere alle query esistenti di finire, chiama .close(false):
const db = new Database();
// ... fai delle cose
db.close(false);Per chiudere il database e lanciare un errore se ci sono query in sospeso, chiama .close(true):
const db = new Database();
// ... fai delle cose
db.close(true);NOTE
`close(false)` viene chiamato automaticamente quando il database viene garbage collected. È sicuro chiamarlo più volte ma non ha effetto dopo la prima chiamata.Istruzione using
Puoi usare l'istruzione using per assicurarti che una connessione al database venga chiusa quando si esce dal blocco 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 supporta il meccanismo built-in di SQLite per serializzare e deserializzare i database da e verso la memoria.
const olddb = new Database("mydb.sqlite");
const contents = olddb.serialize(); // => Uint8Array
const newdb = Database.deserialize(contents);Internamente, .serialize() chiama sqlite3_serialize.
.query()
Usa il metodo db.query() sulla tua istanza di Database per preparare una query SQL. Il risultato è un'istanza di Statement che verrà memorizzata nella cache sulla istanza di Database. La query non verrà eseguita.
const query = db.query(`select "Hello world" as message`);NOTE
**Cosa significa "memorizzato nella cache"?**La memorizzazione nella cache si riferisce allo statement preparato compilato (il bytecode SQL), non ai risultati della query. Quando chiami db.query() con la stessa stringa SQL più volte, Bun restituisce lo stesso oggetto Statement memorizzato nella cache invece di ricompilare l'SQL.
È completamente sicuro riutilizzare uno statement memorizzato nella cache con valori di parametri diversi:
const query = db.query("SELECT * FROM users WHERE id = ?");
query.get(1); // ✓ Funziona
query.get(2); // ✓ Funziona anche - i parametri vengono freshly bound ogni volta
query.get(3); // ✓ Funziona ancoraUsa .prepare() invece di .query() quando vuoi una nuova istanza di Statement che non viene memorizzata nella cache, per esempio se stai generando SQL dinamicamente e non vuoi riempire la cache con query one-off.
// compila lo statement preparato senza caching
const query = db.prepare("SELECT * FROM foo WHERE bar = ?");Modalità WAL
SQLite supporta la modalità write-ahead log (WAL) che migliora drasticamente le prestazioni, specialmente in situazioni con molti lettori concurrenti e un singolo writer. È generalmente raccomandato abilitare la modalità WAL per la maggior parte delle applicazioni tipiche.
Per abilitare la modalità WAL, esegui questa query pragma all'inizio della tua applicazione:
db.run("PRAGMA journal_mode = WAL;");Cos'è la modalità WAL?">
In modalità WAL, le scritture al database vengono scritte direttamente in un file separato chiamato "WAL file" (write-ahead log). Questo file sarà successivamente integrato nel file del database principale. Pensalo come un buffer per le scritture in sospeso. Consulta la documentazione di SQLite per una panoramica più dettagliata.
Su macOS, i file WAL possono persistere per impostazione predefinita. Non è un bug, è come macOS ha configurato la versione di sistema di SQLite.
Statement
Un Statement è una query preparata, il che significa che è stata analizzata e compilata in una forma binaria efficiente. Può essere eseguita più volte in modo performante.
Crea uno statement con il metodo .query sulla tua istanza di Database.
const query = db.query(`select "Hello world" as message`);Le query possono contenere parametri. Questi possono essere numerici (?1) o nominati ($param o :param o @param).
const query = db.query(`SELECT ?1, ?2;`);
const query = db.query(`SELECT $param1, $param2;`);I valori vengono bound a questi parametri quando la query viene eseguita. Un Statement può essere eseguito con diversi metodi, ognuno dei quali restituisce i risultati in una forma diversa.
Binding dei valori
Per boundare valori a uno statement, passa un oggetto ai metodi .all(), .get(), .run(), o .values().
const query = db.query(`select $message;`);
query.all({ $message: "Hello world" });Puoi anche usare parametri posizionali:
const query = db.query(`select ?1;`);
query.all("Hello world");strict: true ti permette di bindare valori senza prefissi
Per impostazione predefinita, i prefissi $, :, e @ sono inclusi quando si boundano valori a parametri nominati. Per bindare senza questi prefissi, usa l'opzione strict nel costruttore del Database.
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()
Usa .all() per eseguire una query e ottenere i risultati come un array di oggetti.
const query = db.query(`select $message;`);
query.all({ $message: "Hello world" });[{ message: "Hello world" }]Internamente, questo chiama sqlite3_reset e ripetutamente chiama sqlite3_step fino a quando restituisce SQLITE_DONE.
.get()
Usa .get() per eseguire una query e ottenere il primo risultato come oggetto.
const query = db.query(`select $message;`);
query.get({ $message: "Hello world" });{ $message: "Hello world" }Internamente, questo chiama sqlite3_reset seguito da sqlite3_step fino a quando non restituisce più SQLITE_ROW. Se la query non restituisce righe, viene restituito undefined.
.run()
Usa .run() per eseguire una query e ottenere undefined. Questo è utile per query che modificano lo schema (es. CREATE TABLE) o operazioni di scrittura bulk.
const query = db.query(`create table foo;`);
query.run();{
lastInsertRowid: 0,
changes: 0,
}Internamente, questo chiama sqlite3_reset e chiama sqlite3_step una volta. Non è necessario iterare su tutte le righe quando non ti interessano i risultati.
La proprietà lastInsertRowid restituisce l'ID dell'ultima riga inserita nel database. La proprietà changes è il numero di righe interessate dalla query.
.as(Class) - Mappa i risultati della query a una classe
Usa .as(Class) per eseguire una query e ottenere i risultati come istanze di una classe. Questo ti permette di allegare metodi e getter/setter ai risultati.
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
trueCome ottimizzazione delle prestazioni, il costruttore della classe non viene chiamato, gli inizializzatori predefiniti non vengono eseguiti e i campi privati non sono accessibili. È più simile a usare Object.create che new. Il prototipo della classe viene assegnato all'oggetto, i metodi vengono allegati e i getter/setter vengono impostati, ma il costruttore non viene chiamato.
Le colonne del database vengono impostate come proprietà sull'istanza della classe.
.iterate() (@@iterator)
Usa .iterate() per eseguire una query e restituire i risultati incrementalmente. Questo è utile per set di risultati grandi che vuoi elaborare una riga alla volta senza caricare tutti i risultati in memoria.
const query = db.query("SELECT * FROM foo");
for (const row of query.iterate()) {
console.log(row);
}Puoi anche usare il protocollo @@iterator:
const query = db.query("SELECT * FROM foo");
for (const row of query) {
console.log(row);
}.values()
Usa values() per eseguire una query e ottenere tutti i risultati come array di array.
const query = db.query(`select $message;`);
query.values({ $message: "Hello world" });
query.values(2);[
[ "Iron Man", 2008 ],
[ "The Avengers", 2012 ],
[ "Ant-Man: Quantumania", 2023 ],
]Internamente, questo chiama sqlite3_reset e ripetutamente chiama sqlite3_step fino a quando restituisce SQLITE_DONE.
.finalize()
Usa .finalize() per distruggere un Statement e liberare tutte le risorse associate ad esso. Una volta finalizzato, un Statement non può essere eseguito again. Tipicamente, il garbage collector farà questo per te, ma la finalizzazione esplicita può essere utile in applicazioni sensibili alle prestazioni.
const query = db.query("SELECT title, year FROM movies");
const movies = query.all();
query.finalize();.toString()
Chiamare toString() su un'istanza di Statement stampa la query SQL espansa. Questo è utile per il debug.
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"Internamente, questo chiama sqlite3_expanded_sql. I parametri vengono espansi usando i valori bound più recentemente.
Parametri
Le query possono contenere parametri. Questi possono essere numerici (?1) o nominati ($param o :param o @param). Binda i valori a questi parametri quando esegui la query:
const query = db.query("SELECT * FROM foo WHERE bar = $bar");
const results = query.all({
$bar: "bar",
});[{ "$bar": "bar" }]I parametri numerati (posizionali) funzionano anche:
const query = db.query("SELECT ?1, ?2");
const results = query.all("hello", "goodbye");[
{
"?1": "hello",
"?2": "goodbye",
},
];Interi
SQLite supporta interi con segno a 64 bit, ma JavaScript supporta solo interi con segno a 52 bit o interi a precisione arbitraria con bigint.
L'input bigint è supportato ovunque, ma per impostazione predefinita bun:sqlite restituisce gli interi come tipi number. Se hai bisogno di gestire interi più grandi di 2^53, imposta l'opzione safeIntegers a true quando crei un'istanza di Database. Questo valida anche che i bigint passati a bun:sqlite non superino i 64 bit.
Per impostazione predefinita, bun:sqlite restituisce gli interi come tipi number. Se hai bisogno di gestire interi più grandi di 2^53, puoi usare il tipo bigint.
safeIntegers: true
Quando safeIntegers è true, bun:sqlite restituirà gli interi come tipi 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);9007199254741093nQuando safeIntegers è true, bun:sqlite lancerà un errore se un valore bigint in un parametro bound supera i 64 bit:
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 rangesafeIntegers: false (default)
Quando safeIntegers è false, bun:sqlite restituirà gli interi come tipi number e troncerà qualsiasi bit oltre 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);9007199254741092Transazioni
Le transazioni sono un meccanismo per eseguire più query in modo atomico; cioè, o tutte le query hanno successo o nessuna ha successo. Crea una transazione con il metodo 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);
});A questo stadio, non abbiamo inserito nessun gatto! La chiamata a db.transaction() restituisce una nuova funzione (insertCats) che avvolge la funzione che esegue le query.
Per eseguire la transazione, chiama questa funzione. Tutti gli argomenti verranno passati alla funzione avvolta; il valore di ritorno della funzione avvolta verrà restituito dalla funzione di transazione. La funzione avvolta ha anche accesso al contesto this come definito dove la transazione viene eseguita.
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`);Il driver inizierà automaticamente una transazione quando insertCats viene chiamata e farà commit quando la funzione avvolta ritorna. Se viene lanciata un'eccezione, la transazione verrà ripristinata. L'eccezione si propagherà come al solito; non viene catturata.
NOTE
**Transazioni nidificate** — Le funzioni di transazione possono essere chiamate dall'interno di altre funzioni di transazione. Quando si fa così, la transazione interna diventa un [savepoint](https://www.sqlite.org/lang_savepoint.html).Visualizza esempio di transazione nidificata">
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); // transazione nidificata
});
adopt([
{ $name: "Joey", $age: 2 },
{ $name: "Sally", $age: 4 },
{ $name: "Junior", $age: 1 },
]);Le transazioni sono disponibili anche con versioni deferred, immediate e exclusive.
insertCats(cats); // usa "BEGIN"
insertCats.deferred(cats); // usa "BEGIN DEFERRED"
insertCats.immediate(cats); // usa "BEGIN IMMEDIATE"
insertCats.exclusive(cats); // usa "BEGIN EXCLUSIVE".loadExtension()
Per caricare un'estensione SQLite, chiama .loadExtension(name) sulla tua istanza di Database.
import { Database } from "bun:sqlite";
const db = new Database();
db.loadExtension("myext");NOTE
**Utenti MacOS** Per impostazione predefinita, macOS viene fornito con la build proprietaria di SQLite di Apple, che non supporta le estensioni. Per usare le estenzioni, dovrai installare una build vanilla di SQLite.brew install sqlite
which sqlite # percorso del binarioPer puntare bun:sqlite alla nuova build, chiama Database.setCustomSQLite(path) prima di creare istanze di Database. (Su altri sistemi operativi, questo è un no-op.) Passa un percorso al file .dylib di SQLite, non l'eseguibile. Con le versioni recenti di Homebrew questo è qualcosa come /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)
Per usare l'API avanzata sqlite3_file_control, chiama .fileControl(cmd, value) sulla tua istanza di Database.
import { Database, constants } from "bun:sqlite";
const db = new Database();
// Assicurati che la modalità WAL NON sia persistente
// questo previene che i file wal rimangano dopo la chiusura del database
db.fileControl(constants.SQLITE_FCNTL_PERSIST_WAL, 0);value può essere:
numberTypedArrayundefinedonull
Riferimento
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; // distruggi statement e pulisci risorse
toString(): string; // serializza a SQL
columnNames: string[]; // i nomi delle colonne del result set
columnTypes: string[]; // tipi basati sui valori effettivi nella prima riga (chiama .get()/.all() prima)
declaredTypes: (string | null)[]; // tipi dallo schema CREATE TABLE (chiama .get()/.all() prima)
paramsCount: number; // il numero di parametri attesi dallo statement
native: any; // l'oggetto nativo che rappresenta lo statement
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>;Tipi di dati
| Tipo JavaScript | Tipo SQLite |
|---|---|
string | TEXT |
number | INTEGER or DECIMAL |
boolean | INTEGER (1 or 0) |
Uint8Array | BLOB |
Buffer | BLOB |
bigint | INTEGER |
null | NULL |