Skip to main content
Bun natively implements a high-performance SQLite3 driver. Import it from the built-in bun:sqlite module — no installation required.
import { Database } from "bun:sqlite";

const db = new Database(":memory:");
const query = db.query("select 'Hello world' as message;");
query.get(); // { message: "Hello world" }
The API is synchronous and modeled after better-sqlite3. bun:sqlite is roughly 3–6x faster than better-sqlite3 for read queries. Features:
  • Prepared statements (cached and uncached)
  • Named and positional parameters
  • Transactions with savepoints
  • BLOBUint8Array type conversion
  • Map rows to class instances with .as(Class)
  • bigint support for 64-bit integers
  • WAL mode for better write concurrency
  • Multi-statement queries via db.run()

Opening a database

import { Database } from "bun:sqlite";

const db = new Database("mydb.sqlite");

Import attribute

Load a database directly via an import statement:
import db from "./mydb.sqlite" with { type: "sqlite" };

const user = db.query("SELECT * FROM users LIMIT 1").get();

Strict mode

By default, named parameters require a $, :, or @ prefix. With strict: true, you can bind values without prefixes, and missing parameters throw an error:
import { Database } from "bun:sqlite";

const db = new Database(":memory:", { strict: true });

const query = db.query("SELECT $name;");
query.all({ name: "Alice" }); // no $ prefix needed

Closing a database

db.close();        // allow pending queries to finish
db.close(true);    // throw if queries are pending
close(false) is called automatically when the database is garbage collected.

Using the using statement

import { Database } from "bun:sqlite";

{
  using db = new Database("mydb.sqlite");
  using query = db.query("SELECT 'hello' as msg;");
  console.log(query.get()); // { msg: "hello" }
} // db and query are automatically closed here

Queries and statements

db.query() — cached prepared statements

db.query() compiles and caches the SQL statement. Calling it again with the same SQL string returns the cached compiled form, not a new compilation.
const query = db.query("SELECT * FROM users WHERE id = $id");

query.get({ $id: 1 }); // first row matching id=1
query.all({ $id: 2 }); // all rows matching id=2
Caching applies to the compiled prepared statement, not the query results. The same cached statement can be reused safely with different parameter values.

db.prepare() — uncached prepared statements

Use db.prepare() when you want a fresh Statement that is not stored in the cache (e.g., for dynamically generated SQL):
const stmt = db.prepare("SELECT * FROM logs WHERE level = ?");
stmt.all("error");

db.run() — execute without reading results

Use db.run() for INSERT, UPDATE, DELETE, or DDL statements where you don’t need rows back:
db.run("CREATE TABLE cats (id INTEGER PRIMARY KEY, name TEXT)");
db.run("INSERT INTO cats (name) VALUES (?)", ["Luna"]);
Returns { lastInsertRowid: number, changes: number }.

Statement methods

A Statement is a compiled query that can be executed multiple times with different parameter values.

Binding parameters

Named parameters use $, :, or @ prefixes (or no prefix when strict: true):
const q = db.query("SELECT * FROM users WHERE id = $id AND role = :role");
q.get({ $id: 1, ":role": "admin" });
Positional parameters use ? or ?N:
const q = db.query("SELECT ?1, ?2");
q.all("hello", "world");
// [{ "?1": "hello", "?2": "world" }]

.get() — fetch first row

const user = db.query("SELECT * FROM users WHERE id = $id").get({ $id: 1 });
// { id: 1, name: "Alice" } or undefined

.all() — fetch all rows

const users = db.query("SELECT * FROM users").all();
// [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }]

.run() — execute and get metadata

const result = db.query("INSERT INTO cats (name) VALUES ($name)").run({ $name: "Salem" });
// { lastInsertRowid: 3, changes: 1 }

.values() — fetch rows as arrays

const rows = db.query("SELECT name, age FROM users").values();
// [["Alice", 30], ["Bob", 25]]

.iterate() — stream rows one at a time

Useful for large result sets that shouldn’t be fully loaded into memory:
const query = db.query("SELECT * FROM large_table");

for (const row of query.iterate()) {
  console.log(row);
}

.as(Class) — map rows to class instances

class Movie {
  title: string;
  year: number;

  get isRecent() {
    return this.year >= 2020;
  }
}

const query = db.query("SELECT title, year FROM movies").as(Movie);
const movies = query.all();

console.log(movies[0].isRecent); // true or false
The class constructor is not called. Columns are assigned directly to the object and the class prototype is attached. Use this for methods and getters, not constructor initialization.

.toString() — inspect expanded SQL

const query = db.query("SELECT $param;");
query.run(42);
console.log(query.toString()); // "SELECT 42"

.finalize() — free resources

query.finalize(); // destroy statement and release memory

Transactions

Wrap multiple queries in an atomic transaction using db.transaction():
import { Database } from "bun:sqlite";

const db = new Database(":memory:");
db.run("CREATE TABLE cats (id INTEGER PRIMARY KEY, name TEXT)");

const insert = db.prepare("INSERT INTO cats (name) VALUES ($name)");

const insertMany = db.transaction((cats: { $name: string }[]) => {
  for (const cat of cats) {
    insert.run(cat);
  }
  return cats.length;
});

const count = insertMany([
  { $name: "Keanu" },
  { $name: "Salem" },
  { $name: "Crookshanks" },
]);

console.log(`Inserted ${count} cats`);
If an exception is thrown inside the transaction function, the transaction is automatically rolled back.

Transaction modes

insertMany(cats);              // BEGIN
insertMany.deferred(cats);     // BEGIN DEFERRED
insertMany.immediate(cats);    // BEGIN IMMEDIATE
insertMany.exclusive(cats);    // BEGIN EXCLUSIVE

Nested transactions (savepoints)

Calling one transaction function from inside another creates a savepoint:
const insertCats = db.transaction((cats) => {
  for (const cat of cats) insert.run(cat);
});

const adopt = db.transaction((cats) => {
  expenses.run("adoption fees", 20);
  insertCats(cats); // nested — becomes a savepoint
});

WAL mode

Write-ahead log mode significantly improves write performance and allows concurrent readers. Enable it at startup:
db.run("PRAGMA journal_mode = WAL;");

WAL sidecar file cleanup

WAL mode creates -wal and -shm sidecar files. On macOS (which uses Apple’s SQLite), these persist after db.close(). To clean them up reliably on all platforms:
import { Database, constants } from "bun:sqlite";

const db = new Database("mydb.sqlite");
db.run("PRAGMA journal_mode = WAL;");

// ... use the database ...

db.fileControl(constants.SQLITE_FCNTL_PERSIST_WAL, 0);
db.run("PRAGMA wal_checkpoint(TRUNCATE);");
db.close();
// only mydb.sqlite remains

Integer handling

SQLite supports 64-bit signed integers, but JavaScript numbers only safely represent up to 53 bits. By default, bun:sqlite returns integers as number. Enable safeIntegers to use bigint for large values:
import { Database } from "bun:sqlite";

const db = new Database(":memory:", { safeIntegers: true });

const query = db.query(`SELECT ${BigInt(Number.MAX_SAFE_INTEGER) + 102n} as big_id`);
const result = query.get();

console.log(result.big_id); // 9007199254741093n (bigint)
With safeIntegers: true, passing a bigint that exceeds 64 bits throws a RangeError.

Serialization

Serialize and deserialize the entire database to/from a Uint8Array:
const original = new Database("mydb.sqlite");
const bytes = original.serialize(); // Uint8Array

const copy = Database.deserialize(bytes); // new in-memory DB with same data

Loading extensions

import { Database } from "bun:sqlite";

const db = new Database();
db.loadExtension("myext");
On macOS, Apple’s system SQLite does not support extensions. Install a vanilla SQLite via Homebrew and point bun:sqlite to it:
brew install sqlite
Database.setCustomSQLite("/opt/homebrew/Cellar/sqlite/<version>/lib/libsqlite3.dylib");

Type mapping

JavaScript typeSQLite type
stringTEXT
numberINTEGER or REAL
booleanINTEGER (1 or 0)
Uint8ArrayBLOB
BufferBLOB
bigintINTEGER
nullNULL

API reference

class Database {
  constructor(
    filename?: string,
    options?: {
      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 };

  transaction(
    fn: (...args: any[]) => void,
  ): CallableFunction & {
    deferred: (...args: any[]) => void;
    immediate: (...args: any[]) => void;
    exclusive: (...args: any[]) => void;
  };

  serialize(): Uint8Array;
  static deserialize(data: Uint8Array): Database;

  fileControl(cmd: number, value: any): void;
  loadExtension(name: string): void;
  close(throwOnError?: boolean): void;
}

class Statement<ReturnType, ParamsType> {
  all(...params: ParamsType[]): ReturnType[];
  get(...params: ParamsType[]): ReturnType | undefined;
  run(...params: ParamsType[]): { lastInsertRowid: number; changes: number };
  values(...params: ParamsType[]): unknown[][];
  iterate(...params: ParamsType[]): IterableIterator<ReturnType>;
  as<T>(Class: new (...args: any[]) => T): Statement<T, ParamsType>;
  finalize(): void;
  toString(): string;

  columnNames: string[];
  paramsCount: number;
}

Build docs developers (and LLMs) love