Bun implementa nativamente un controlador SQLite3 de alto rendimiento. Para usarlo, importa desde el módulo integrado 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" }La API es simple, síncrona y rápida. El crédito es para better-sqlite3 y sus contribuyentes por inspirar la API de bun:sqlite.
Las características incluyen:
- Transacciones
- Parámetros (con nombre y posicionales)
- Sentencias preparadas
- Conversiones de tipos de datos (
BLOBse convierte enUint8Array) - Mapea resultados de consultas a clases sin un ORM -
query.as(MyClass) - El rendimiento más rápido de cualquier controlador SQLite para JavaScript
- Soporte
bigint - Sentencias de múltiples consultas (ej.
SELECT 1; SELECT 2;) en una sola llamada a database.run(query)
El módulo bun:sqlite es aproximadamente 3-6 veces más rápido que better-sqlite3 y 8-9 veces más rápido que deno.land/x/sqlite para consultas de lectura. Cada controlador fue evaluado con el conjunto de datos Northwind Traders. Ver y ejecutar el código fuente del benchmark.
Base de datos
Para abrir o crear una base de datos SQLite3:
import { Database } from "bun:sqlite";
const db = new Database("mydb.sqlite");Para abrir una base de datos en memoria:
import { Database } from "bun:sqlite";
// todos estos hacen lo mismo
const db = new Database(":memory:");
const db = new Database();
const db = new Database("");Para abrir en modo readonly:
import { Database } from "bun:sqlite";
const db = new Database("mydb.sqlite", { readonly: true });Para crear la base de datos si el archivo no existe:
import { Database } from "bun:sqlite";
const db = new Database("mydb.sqlite", { create: true });Modo estricto
Por defecto, bun:sqlite requiere que los parámetros de enlace incluyan el prefijo $, :, o @, y no lanza un error si falta un parámetro.
Para lanzar un error cuando falta un parámetro y permitir enlace sin prefijo, establece strict: true en el constructor Database:
import { Database } from "bun:sqlite";
const strict = new Database(":memory:", { strict: true });
// lanza error debido al error tipográfico:
const query = strict.query("SELECT $message;").all({ messag: "Hello world" });
const notStrict = new Database(":memory:");
// no lanza error:
notStrict.query("SELECT $message;").all({ messag: "Hello world" });Cargar mediante importación de módulo ES
También puedes usar un atributo de importación para cargar una base de datos.
import db from "./mydb.sqlite" with { type: "sqlite" };
console.log(db.query("select * from users LIMIT 1").get());Esto es equivalente a lo siguiente:
import { Database } from "bun:sqlite";
const db = new Database("./mydb.sqlite");.close(throwOnError: boolean = false)
Para cerrar una conexión de base de datos, pero permitir que las consultas existentes finalicen, llama a .close(false):
const db = new Database();
// ... hacer cosas
db.close(false);Para cerrar la base de datos y lanzar un error si hay consultas pendientes, llama a .close(true):
const db = new Database();
// ... hacer cosas
db.close(true);NOTE
`close(false)` se llama automáticamente cuando la base de datos es recolectada por el garbage collector. Es seguro llamarlo múltiples veces pero no tiene efecto después del primero.Sentencia using
Puedes usar la sentencia using para asegurar que una conexión de base de datos se cierre cuando se sale del bloque 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 soporta el mecanismo integrado de SQLite para serializar y deserializar bases de datos hacia y desde memoria.
const olddb = new Database("mydb.sqlite");
const contents = olddb.serialize(); // => Uint8Array
const newdb = Database.deserialize(contents);Internamente, .serialize() llama a sqlite3_serialize.
.query()
Usa el método db.query() en tu instancia Database para preparar una consulta SQL. El resultado es una instancia Statement que se almacenará en caché en la instancia Database. La consulta no se ejecutará.
const query = db.query(`select "Hello world" as message`);NOTE
**¿Qué significa "almacenado en caché"?**El almacenamiento en caché se refiere a la sentencia preparada compilada (el bytecode SQL), no a los resultados de la consulta. Cuando llamas a db.query() con la misma cadena SQL múltiples veces, Bun devuelve el mismo objeto Statement almacenado en caché en lugar de recompilar el SQL.
Es completamente seguro reutilizar una sentencia almacenada en caché con diferentes valores de parámetro:
const query = db.query("SELECT * FROM users WHERE id = ?");
query.get(1); // ✓ Funciona
query.get(2); // ✓ También funciona - los parámetros se vinculan nuevamente cada vez
query.get(3); // ✓ Todavía funcionaUsa .prepare() en lugar de .query() cuando quieras una instancia Statement nueva que no esté almacenada en caché, por ejemplo, si estás generando SQL dinámicamente y no quieres llenar la caché con consultas únicas.
// compilar la sentencia preparada sin almacenar en caché
const query = db.prepare("SELECT * FROM foo WHERE bar = ?");Modo WAL
SQLite soporta el modo de registro por adelantado (WAL) que mejora dramáticamente el rendimiento, especialmente en situaciones con muchos lectores concurrentes y un solo escritor. Es ampliamente recomendado habilitar el modo WAL para la mayoría de las aplicaciones típicas.
Para habilitar el modo WAL, ejecuta esta consulta pragma al inicio de tu aplicación:
db.run("PRAGMA journal_mode = WAL;");¿Qué es el modo WAL?
En modo WAL, las escrituras a la base de datos se escriben directamente en un archivo separado llamado "archivo WAL" (registro por adelantado). Este archivo se integrará más tarde en el archivo de base de datos principal. Piensa en ello como un búfer para escrituras pendientes. Consulta la documentación de SQLite para una visión general más detallada.
En macOS, los archivos WAL pueden ser persistentes por defecto. Esto no es un error, es cómo macOS configuró la versión del sistema de SQLite.
Sentencias
Una Statement es una consulta preparada, lo que significa que ha sido analizada y compilada en una forma binaria eficiente. Puede ejecutarse múltiples veces de manera eficiente.
Crea una sentencia con el método .query en tu instancia Database.
const query = db.query(`select "Hello world" as message`);Las consultas pueden contener parámetros. Estos pueden ser numéricos (?1) o con nombre ($param o :param o @param).
const query = db.query(`SELECT ?1, ?2;`);
const query = db.query(`SELECT $param1, $param2;`);Los valores se vinculan a estos parámetros cuando se ejecuta la consulta. Una Statement puede ejecutarse con varios métodos diferentes, cada uno devolviendo los resultados en una forma diferente.
Vincular valores
Para vincular valores a una sentencia, pasa un objeto al método .all(), .get(), .run(), o .values().
const query = db.query(`select $message;`);
query.all({ $message: "Hello world" });También puedes vincular usando parámetros posicionales:
const query = db.query(`select ?1;`);
query.all("Hello world");strict: true te permite vincular valores sin prefijos
Por defecto, los prefijos $, :, y @ se incluyen al vincular valores a parámetros con nombre. Para vincular sin estos prefijos, usa la opción strict en el constructor Database.
import { Database } from "bun:sqlite";
const db = new Database(":memory:", {
// vincular valores sin prefijos
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() para ejecutar una consulta y obtener los resultados como un array de objetos.
const query = db.query(`select $message;`);
query.all({ $message: "Hello world" });[{ message: "Hello world" }]Internamente, esto llama a sqlite3_reset y repetidamente llama a sqlite3_step hasta que devuelve SQLITE_DONE.
.get()
Usa .get() para ejecutar una consulta y obtener el primer resultado como un objeto.
const query = db.query(`select $message;`);
query.get({ $message: "Hello world" });{ $message: "Hello world" }Internamente, esto llama a sqlite3_reset seguido de sqlite3_step hasta que ya no devuelve SQLITE_ROW. Si la consulta no devuelve filas, se devuelve undefined.
.run()
Usa .run() para ejecutar una consulta y obtener undefined. Esto es útil para consultas que modifican el esquema (ej. CREATE TABLE) u operaciones de escritura masiva.
const query = db.query(`create table foo;`);
query.run();{
lastInsertRowid: 0,
changes: 0,
}Internamente, esto llama a sqlite3_reset y llama a sqlite3_step una vez. No es necesario recorrer todas las filas cuando no te importan los resultados.
La propiedad lastInsertRowid devuelve el ID de la última fila insertada en la base de datos. La propiedad changes es el número de filas afectadas por la consulta.
.as(Class) - Mapear resultados de consulta a una clase
Usa .as(Class) para ejecutar una consulta y obtener los resultados como instancias de una clase. Esto te permite adjuntar métodos y getters/setters a los resultados.
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
trueComo optimización de rendimiento, el constructor de la clase no se llama, los inicializadores predeterminados no se ejecutan y los campos privados no son accesibles. Esto es más como usar Object.create que new. El prototipo de la clase se asigna al objeto, los métodos se adjuntan y los getters/setters se configuran, pero el constructor no se llama.
Las columnas de la base de datos se establecen como propiedades en la instancia de la clase.
.iterate() (@@iterator)
Usa .iterate() para ejecutar una consulta y devolver resultados incrementalmente. Esto es útil para conjuntos de resultados grandes que quieres procesar una fila a la vez sin cargar todos los resultados en memoria.
const query = db.query("SELECT * FROM foo");
for (const row of query.iterate()) {
console.log(row);
}También puedes usar el protocolo @@iterator:
const query = db.query("SELECT * FROM foo");
for (const row of query) {
console.log(row);
}.values()
Usa values() para ejecutar una consulta y obtener todos los resultados como un array de arrays.
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, esto llama a sqlite3_reset y repetidamente llama a sqlite3_step hasta que devuelve SQLITE_DONE.
.finalize()
Usa .finalize() para destruir una Statement y liberar cualquier recurso asociado con ella. Una vez finalizada, una Statement no puede ejecutarse nuevamente. Típicamente, el recolector de basura hará esto por ti, pero la finalización explícita puede ser útil en aplicaciones sensibles al rendimiento.
const query = db.query("SELECT title, year FROM movies");
const movies = query.all();
query.finalize();.toString()
Llamar a toString() en una instancia Statement imprime la consulta SQL expandida. Esto es útil para depuración.
import { Database } from "bun:sqlite";
// configuración
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, esto llama a sqlite3_expanded_sql. Los parámetros se expanden usando los valores vinculados más recientemente.
Parámetros
Las consultas pueden contener parámetros. Estos pueden ser numéricos (?1) o con nombre ($param o :param o @param). Vincula valores a estos parámetros al ejecutar la consulta:
const query = db.query("SELECT * FROM foo WHERE bar = $bar");
const results = query.all({
$bar: "bar",
});[{ "$bar": "bar" }]Los parámetros numerados (posicionales) también funcionan:
const query = db.query("SELECT ?1, ?2");
const results = query.all("hello", "goodbye");[
{
"?1": "hello",
"?2": "goodbye",
},
];Enteros
SQLite soporta enteros con signo de 64 bits, pero JavaScript solo soporta enteros con signo de 52 bits o enteros de precisión arbitraria con bigint.
La entrada bigint es soportada en todas partes, pero por defecto bun:sqlite devuelve enteros como tipos number. Si necesitas manejar enteros más grandes que 2^53, establece la opción safeIntegers en true al crear una instancia Database. Esto también valida que los bigint pasados a bun:sqlite no excedan 64 bits.
Por defecto, bun:sqlite devuelve enteros como tipos number. Si necesitas manejar enteros más grandes que 2^53, puedes usar el tipo bigint.
safeIntegers: true
Cuando safeIntegers es true, bun:sqlite devolverá enteros como tipos 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);9007199254741093nCuando safeIntegers es true, bun:sqlite lanzará un error si un valor bigint en un parámetro vinculado excede 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);
}El valor BigInt '81129638414606663681390495662081' está fuera de rangosafeIntegers: false (predeterminado)
Cuando safeIntegers es false, bun:sqlite devolverá enteros como tipos number y truncará cualquier bit más allá 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);9007199254741092Transacciones
Las transacciones son un mecanismo para ejecutar múltiples consultas de manera atómica; es decir, o todas las consultas tienen éxito o ninguna lo hace. Crea una transacción con el método 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);
});En esta etapa, ¡no hemos insertado ningún gato! La llamada a db.transaction() devuelve una nueva función (insertCats) que envuelve la función que ejecuta las consultas.
Para ejecutar la transacción, llama a esta función. Todos los argumentos se pasarán a la función envuelta; el valor de retorno de la función envuelta será devuelto por la función de transacción. La función envuelta también tiene acceso al contexto this como se define donde se ejecuta la transacción.
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(`Insertados ${count} gatos`);El controlador automáticamente begin una transacción cuando se llama a insertCats y la commit cuando la función envuelta devuelve. Si se lanza una excepción, la transacción se revertirá. La excepción se propagará como es habitual; no se captura.
NOTE
**Transacciones anidadas** — Las funciones de transacción pueden llamarse desde dentro de otras funciones de transacción. Al hacerlo, la transacción interna se convierte en un [savepoint](https://www.sqlite.org/lang_savepoint.html).Ver ejemplo de transacción anidada
// configuración
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("gastos de adopción", 20);
insertCats(cats); // transacción anidada
});
adopt([
{ $name: "Joey", $age: 2 },
{ $name: "Sally", $age: 4 },
{ $name: "Junior", $age: 1 },
]);Las transacciones también vienen con versiones deferred, immediate, y exclusive.
insertCats(cats); // usa "BEGIN"
insertCats.deferred(cats); // usa "BEGIN DEFERRED"
insertCats.immediate(cats); // usa "BEGIN IMMEDIATE"
insertCats.exclusive(cats); // usa "BEGIN EXCLUSIVE".loadExtension()
Para cargar una extensión de SQLite, llama a .loadExtension(name) en tu instancia Database
import { Database } from "bun:sqlite";
const db = new Database();
db.loadExtension("myext");NOTE
**Usuarios de MacOS** Por defecto, macOS viene con la compilación propietaria de SQLite de Apple, que no soporta extensiones. Para usar extensiones, necesitarás instalar una compilación vanilla de SQLite.brew install sqlite
which sqlite # obtener ruta al binarioPara apuntar bun:sqlite a la nueva compilación, llama a Database.setCustomSQLite(path) antes de crear cualquier instancia Database. (En otros sistemas operativos, esto es una operación vacía.) Pasa una ruta al archivo .dylib de SQLite, no al ejecutable. Con versiones recientes de Homebrew esto es algo así como /opt/homebrew/Cellar/sqlite/<version>/libsqlite3.dylib.
import { Database } from "bun:sqlite";
Database.setCustomSQLite("/ruta/a/libsqlite.dylib");
const db = new Database();
db.loadExtension("myext");.fileControl(cmd: number, value: any)
Para usar la API avanzada sqlite3_file_control, llama a .fileControl(cmd, value) en tu instancia Database.
import { Database, constants } from "bun:sqlite";
const db = new Database();
// Asegurar que el modo WAL NO sea persistente
// esto previene que los archivos wal permanezcan después de cerrar la base de datos
db.fileControl(constants.SQLITE_FCNTL_PERSIST_WAL, 0);value puede ser:
numberTypedArrayundefinedonull
Referencia
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; // destruir sentencia y limpiar recursos
toString(): string; // serializar a SQL
columnNames: string[]; // los nombres de columnas del conjunto de resultados
columnTypes: string[]; // tipos basados en valores reales en la primera fila (llama a .get()/.all() primero)
declaredTypes: (string | null)[]; // tipos del esquema CREATE TABLE (llama a .get()/.all() primero)
paramsCount: number; // el número de parámetros esperados por la sentencia
native: any; // el objeto nativo que representa la sentencia
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>;Tipos de datos
| Tipo JavaScript | Tipo SQLite |
|---|---|
string | TEXT |
number | INTEGER o DECIMAL |
boolean | INTEGER (1 o 0) |
Uint8Array | BLOB |
Buffer | BLOB |
bigint | INTEGER |
null | NULL |