O Bun implementa nativamente um driver SQLite3 de alta performance. Para usá-lo, importe do módulo 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" }A API é simples, síncrona e rápida. Créditos ao better-sqlite3 e seus contribuidores por inspirar a API do bun:sqlite.
Recursos incluem:
- Transações
- Parâmetros (nomeados e posicionais)
- Prepared statements
- Conversões de tipo de dado (
BLOBviraUint8Array) - Mapeie resultados de query para classes sem ORM -
query.as(MyClass) - A performance mais rápida de qualquer driver SQLite para JavaScript
- Suporte a
bigint - Statements multi-query (e.g.
SELECT 1; SELECT 2;) em uma única chamada para database.run(query)
O módulo bun:sqlite é aproximadamente 3-6x mais rápido que better-sqlite3 e 8-9x mais rápido que deno.land/x/sqlite para queries de leitura. Cada driver foi benchmarked contra o dataset Northwind Traders. Veja e execute o código fonte do benchmark.
Database
Para abrir ou criar um banco de dados SQLite3:
import { Database } from "bun:sqlite";
const db = new Database("mydb.sqlite");Para abrir um banco de dados em memória:
import { Database } from "bun:sqlite";
// todos estes fazem a mesma coisa
const db = new Database(":memory:");
const db = new Database();
const db = new Database("");Para abrir em modo readonly:
import { Database } from "bun:sqlite";
const db = new Database("mydb.sqlite", { readonly: true });Para criar o banco de dados se o arquivo não existir:
import { Database } from "bun:sqlite";
const db = new Database("mydb.sqlite", { create: true });Modo estrito
Por padrão, bun:sqlite requer que parâmetros de binding incluam o prefixo $, :, ou @, e não lança erro se um parâmetro estiver faltando.
Para lançar erro quando um parâmetro estiver faltando e permitir binding sem prefixo, defina strict: true no construtor Database:
import { Database } from "bun:sqlite";
const strict = new Database(":memory:", { strict: true });
// lança erro por causa do erro de digitação:
const query = strict.query("SELECT $message;").all({ messag: "Hello world" });
const notStrict = new Database(":memory:");
// não lança erro:
notStrict.query("SELECT $message;").all({ messag: "Hello world" });Carregar via import de módulo ES
Você também pode usar um atributo de import para carregar um banco de dados.
import db from "./mydb.sqlite" with { type: "sqlite" };
console.log(db.query("select * from users LIMIT 1").get());Isto é equivalente ao seguinte:
import { Database } from "bun:sqlite";
const db = new Database("./mydb.sqlite");.close(throwOnError: boolean = false)
Para fechar uma conexão de banco de dados, mas permitir que queries existentes terminem, chame .close(false):
const db = new Database();
// ... faça coisas
db.close(false);Para fechar o banco de dados e lançar erro se houver queries pendentes, chame .close(true):
const db = new Database();
// ... faça coisas
db.close(true);NOTE
`close(false)` é chamado automaticamente quando o banco de dados é garbage collected. É seguro chamar múltiplas vezes mas não tem efeito após a primeira.Statement using
Você pode usar o statement using para garantir que uma conexão de banco de dados seja fechada quando o bloco using for encerrado.
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 suporta o mecanismo built-in do SQLite para serializar e deserializar bancos de dados para e da memória.
const olddb = new Database("mydb.sqlite");
const contents = olddb.serialize(); // => Uint8Array
const newdb = Database.deserialize(contents);Internamente, .serialize() chama sqlite3_serialize.
.query()
Use o método db.query() na sua instância Database para preparar uma query SQL. O resultado é uma instância Statement que será cacheada na instância Database. A query não será executada.
const query = db.query(`select "Hello world" as message`);NOTE
**O que significa "cached"?**O caching se refere ao prepared statement compilado (o bytecode SQL), não aos resultados da query. Quando você chama db.query() com a mesma string SQL múltiplas vezes, o Bun retorna o mesmo objeto Statement cacheado em vez de recompilar o SQL.
É completamente seguro reutilizar um statement cacheado com diferentes valores de parâmetro:
const query = db.query("SELECT * FROM users WHERE id = ?");
query.get(1); // ✓ Funciona
query.get(2); // ✓ Também funciona - parâmetros são vinculados fresh cada vez
query.get(3); // ✓ Ainda funcionaUse .prepare() em vez de .query() quando quiser uma instância Statement fresh que não é cacheada, por exemplo se você está gerando SQL dinamicamente e não quer preencher o cache com queries one-off.
// compila o prepared statement sem cache
const query = db.prepare("SELECT * FROM foo WHERE bar = ?");Modo WAL
SQLite suporta modo write-ahead log (WAL) que dramaticamente melhora performance, especialmente em situações com muitos leitores concorrentes e um único escritor. É amplamente recomendado habilitar modo WAL para a maioria das aplicações típicas.
Para habilitar modo WAL, execute esta query pragma no início da sua aplicação:
db.run("PRAGMA journal_mode = WAL;");O que é modo WAL?">
No modo WAL, escritas no banco de dados são escritas diretamente em um arquivo separado chamado "arquivo WAL" (write-ahead log). Este arquivo será integrado posteriormente no arquivo principal do banco de dados. Pense nisso como um buffer para escritas pendentes. Consulte a documentação do SQLite para uma visão geral mais detalhada.
No macOS, arquivos WAL podem ser persistentes por padrão. Isto não é um bug, é como o macOS configurou a versão do sistema do SQLite.
Statements
Um Statement é uma query preparada, o que significa que foi parseada e compilada em uma forma binária eficiente. Pode ser executada múltiplas vezes de forma performática.
Crie um statement com o método .query na sua instância Database.
const query = db.query(`select "Hello world" as message`);Queries podem conter parâmetros. Estes podem ser numéricos (?1) ou nomeados ($param ou :param ou @param).
const query = db.query(`SELECT ?1, ?2;`);
const query = db.query(`SELECT $param1, $param2;`);Valores são vinculados a estes parâmetros quando a query é executada. Um Statement pode ser executado com vários métodos diferentes, cada um retornando os resultados em uma forma diferente.
Vinculando valores
Para vincular valores a um statement, passe um objeto para o método .all(), .get(), .run(), ou .values().
const query = db.query(`select $message;`);
query.all({ $message: "Hello world" });Você pode vincular usando parâmetros posicionais também:
const query = db.query(`select ?1;`);
query.all("Hello world");strict: true permite vincular valores sem prefixos
Por padrão, os prefixos $, :, e @ são incluídos ao vincular valores a parâmetros nomeados. Para vincular sem estes prefixos, use a opção strict no construtor Database.
import { Database } from "bun:sqlite";
const db = new Database(":memory:", {
// vincula valores sem prefixos
strict: true,
});
const query = db.query(`select $message;`);
// strict: true
query.all({ message: "Hello world" });
// strict: false
// query.all({ $message: "Hello world" });.all()
Use .all() para executar uma query e obter os resultados como um array de objetos.
const query = db.query(`select $message;`);
query.all({ $message: "Hello world" });[{ message: "Hello world" }]Internamente, isto chama sqlite3_reset e repetidamente chama sqlite3_step até retornar SQLITE_DONE.
.get()
Use .get() para executar uma query e obter o primeiro resultado como um objeto.
const query = db.query(`select $message;`);
query.get({ $message: "Hello world" });{ $message: "Hello world" }Internamente, isto chama sqlite3_reset seguido por sqlite3_step até não retornar mais SQLITE_ROW. Se a query não retornar linhas, undefined é retornado.
.run()
Use .run() para executar uma query e obter undefined. Isto é útil para queries que modificam schema (e.g. CREATE TABLE) ou operações de escrita em massa.
const query = db.query(`create table foo;`);
query.run();{
lastInsertRowid: 0,
changes: 0,
}Internamente, isto chama sqlite3_reset e chama sqlite3_step uma vez. Percorrer todas as linhas não é necessário quando você não se importa com os resultados.
A propriedade lastInsertRowid retorna o ID da última linha inserida no banco de dados. A propriedade changes é o número de linhas afetadas pela query.
.as(Class) - Mapeie resultados de query para uma classe
Use .as(Class) para executar uma query e obter os resultados como instâncias de uma classe. Isto permite anexar métodos & getters/setters aos 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 uma otimização de performance, o construtor da classe não é chamado, inicializadores padrão não são executados, e campos privados não são acessíveis. Isto é mais como usar Object.create que new. O prototype da classe é atribuído ao objeto, métodos são anexados, e getters/setters são configurados, mas o construtor não é chamado.
As colunas do banco de dados são definidas como propriedades na instância da classe.
.iterate() (@@iterator)
Use .iterate() para executar uma query e incrementalmente retornar resultados. Isto é útil para grandes conjuntos de resultados que você quer processar uma linha por vez sem carregar todos os resultados na memória.
const query = db.query("SELECT * FROM foo");
for (const row of query.iterate()) {
console.log(row);
}Você também pode usar o protocolo @@iterator:
const query = db.query("SELECT * FROM foo");
for (const row of query) {
console.log(row);
}.values()
Use values() para executar uma query e obter todos os resultados como um 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, isto chama sqlite3_reset e repetidamente chama sqlite3_step até retornar SQLITE_DONE.
.finalize()
Use .finalize() para destruir um Statement e liberar quaisquer recursos associados a ele. Uma vez finalizado, um Statement não pode ser executado novamente. Tipicamente, o garbage collector fará isto para você, mas finalização explícita pode ser útil em aplicações sensíveis a performance.
const query = db.query("SELECT title, year FROM movies");
const movies = query.all();
query.finalize();.toString()
Chamar toString() em uma instância Statement imprime a query SQL expandida. Isto é útil para 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, isto chama sqlite3_expanded_sql. Os parâmetros são expandidos usando os valores vinculados mais recentemente.
Parâmetros
Queries podem conter parâmetros. Estes podem ser numéricos (?1) ou nomeados ($param ou :param ou @param). Vincule valores a estes parâmetros ao executar a query:
const query = db.query("SELECT * FROM foo WHERE bar = $bar");
const results = query.all({
$bar: "bar",
});[{ "$bar": "bar" }]Parâmetros numerados (posicionais) também funcionam:
const query = db.query("SELECT ?1, ?2");
const results = query.all("hello", "goodbye");[
{
"?1": "hello",
"?2": "goodbye",
},
];Inteiros
SQLite suporta inteiros de 64 bits com sinal, mas JavaScript só suporta inteiros de 52 bits com sinal ou inteiros de precisão arbitrária com bigint.
Input bigint é suportado em todos os lugares, mas por padrão bun:sqlite retorna inteiros como tipos number. Se você precisa lidar com inteiros maiores que 2^53, defina a opção safeIntegers como true ao criar uma instância Database. Isto também valida que bigint passados para bun:sqlite não excedem 64 bits.
Por padrão, bun:sqlite retorna inteiros como tipos number. Se você precisa lidar com inteiros maiores que 2^53, você pode usar o tipo bigint.
safeIntegers: true
Quando safeIntegers é true, bun:sqlite retornará inteiros 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);9007199254741093nQuando safeIntegers é true, bun:sqlite lançará erro se um valor bigint em um parâmetro vinculado exceder 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);
}Valor BigInt '81129638414606663681390495662081' está fora do rangesafeIntegers: false (padrão)
Quando safeIntegers é false, bun:sqlite retornará inteiros como tipos number e truncará quaisquer bits além 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);9007199254741092Transações
Transações são um mecanismo para executar múltiplas queries de forma atômica; isto é, ou todas as queries têm sucesso ou nenhuma tem. Crie uma transação com o 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);
});Neste ponto, não inserimos nenhum gato! A chamada para db.transaction() retorna uma nova função (insertCats) que envolve a função que executa as queries.
Para executar a transação, chame esta função. Todos os argumentos serão passados para a função envolvida; o valor de retorno da função envolvida será retornado pela função de transação. A função envolvida também tem acesso ao contexto this conforme definido onde a transação é executada.
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(`Inserido ${count} gatos`);O driver automaticamente begin uma transação quando insertCats é chamado e faz commit quando a função envolvida retorna. Se uma exceção for lançada, a transação será rolled back. A exceção propagará como normal; não é capturada.
NOTE
**Transações aninhadas** — Funções de transação podem ser chamadas de dentro de outras funções de transação. Ao fazer isso, a transação interna se torna um [savepoint](https://www.sqlite.org/lang_savepoint.html).Ver exemplo de transação aninhada">
// setup
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); // transação aninhada
});
adopt([
{ $name: "Joey", $age: 2 },
{ $name: "Sally", $age: 4 },
{ $name: "Junior", $age: 1 },
]);Transações também vêm com versões 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()
Para carregar uma extensão SQLite, chame .loadExtension(name) na sua instância Database
import { Database } from "bun:sqlite";
const db = new Database();
db.loadExtension("myext");NOTE
**Usuários MacOS** Por padrão, macOS vem com a build proprietária da Apple do SQLite, que não suporta extensões. Para usar extensões, você precisará instalar uma build vanilla do SQLite.brew install sqlite
which sqlite # obtém caminho do binárioPara apontar bun:sqlite para a nova build, chame Database.setCustomSQLite(path) antes de criar quaisquer instâncias Database. (Em outros sistemas operacionais, isto é no-op.) Passe um caminho para o arquivo .dylib do SQLite, não o executável. Com versões recentes do Homebrew isto é algo como /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)
Para usar a API avançada sqlite3_file_control, chame .fileControl(cmd, value) na sua instância Database.
import { Database, constants } from "bun:sqlite";
const db = new Database();
// Garante que modo WAL NÃO é persistente
// isto previne que arquivos wal fiquem lingering após o banco de dados ser fechado
db.fileControl(constants.SQLITE_FCNTL_PERSIST_WAL, 0);value pode ser:
numberTypedArrayundefinedounull
Referência
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; // destroy statement and clean up resources
toString(): string; // serialize to SQL
columnNames: string[]; // os nomes das colunas do result set
columnTypes: string[]; // tipos baseados em valores reais na primeira linha (chame .get()/.all() primeiro)
declaredTypes: (string | null)[]; // tipos do schema CREATE TABLE (chame .get()/.all() primeiro)
paramsCount: number; // o número de parâmetros esperados pelo statement
native: any; // o objeto nativo representando o 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>;Tipos de Dados
| Tipo JavaScript | Tipo SQLite |
|---|---|
string | TEXT |
number | INTEGER ou DECIMAL |
boolean | INTEGER (1 ou 0) |
Uint8Array | BLOB |
Buffer | BLOB |
bigint | INTEGER |
null | NULL |