Bun SQLite printed Ada and Grace from a file next to the code

1 month ago

Two names printed. Ada, then Grace. That is the bun sqlite module on Bun 1.4.0. SQLite is compiled into the Bun binary. You import bun sqlite. The directory listing is index.ts and mydb.sqlite. A table that used to wait on Docker Postgres is a file next to the server.

Ask

Ask about this presentation

Answers are generated from this presentation.

Chapters

  1. 0:00Ada and Grace printed from a file next to the code
  2. 0:19This binary prints SQLite 3.51.0
  3. 0:44The opening snippet opens an in-memory database
  4. 0:59hello.ts returns Hello world after query, then get
  5. 1:15db.query compiles and caches the statement
  6. 2:05get, all, and run step the compiled statement
  7. 2:30A missing bind key becomes null by default
  8. 3:11new Database with a filename creates the sqlite file
  9. 3:53One prepared insert writes Ada, then Grace
  10. 4:24dot all printed Ada then Grace
  11. 4:41ls shows mydb.sqlite sitting next to index.ts
  12. 4:56strict true throws on a dollar-prefixed bind key
  13. 5:23After close, dash-wal and dash-shm are still on disk
  14. 6:06This binary answers sqlite_version 3.51.0
  15. 6:36The whole file is nine lines
  16. 6:51The three issues were checked today
Show transcript

Ada and Grace printed from a file next to the code

Bun SQLite printed Ada and Grace from a file next to the code
1.4.0 · 34cbb9a40
index.ts
import { Database } from "bun:sqlite";

const db = new Database("mydb.sqlite", { create: true });
db.run(
  "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)"
);
const insert = db.query(
  "INSERT INTO users (name) VALUES ($name)"
);
insert.run({ $name: "Ada" });
insert.run({ $name: "Grace" });
const rows = db.query(
  "SELECT id, name FROM users ORDER BY id"
).all();
console.log(rows);
$ bun index.ts
[
  {
    id: 1,
    name: "Ada",
  }, {
    id: 2,
    name: "Grace",
  }
]
$ ls -la
-rw-r--r--     1 siderakis  wheel    384 Sep  4 20:29 index.ts
-rw-r--r--@    1 siderakis  wheel   8192 Sep  4 20:29 mydb.sqlite
the directory listing is index.ts and mydb.sqlite
Source: assets/captures/05-two-row-file.txt, run 2026-09-04 on Bun 1.4.0+34cbb9a40

Two names printed. Ada, then Grace. That is the bun sqlite module on Bun 1.4.0. SQLite is compiled into the Bun binary. You import bun sqlite. The directory listing is index.ts and mydb.sqlite. A table that used to wait on Docker Postgres is a file next to the server.

This binary prints SQLite 3.51.0

01
Install
This Bun 1.4.0 binary prints SQLite version 3.51.0
$ bun --version
1.4.0
$ bun --revision
1.4.0+34cbb9a40
$ bun sqlite-version.ts
{
  v: "3.51.0",
}
version · 1.4.0
revision · 34cbb9a40
builtin module
import { Database } from "bun:sqlite"
Source: version pair, assets/captures/01-version-revision-1.4.0.txt · 3.51.0 print, assets/captures/02-sqlite-version-1.4.0.txt

Install is already done. Episode one of this series put Bun on the machine. Bun dash dash version prints 1.4.0. Bun dash dash revision prints 1.4.0 plus 34cbb9a40, the commit this binary was built from. Then one query: select sqlite version. This binary prints 3.51.0. The import line is from bun sqlite. bun sqlite is a builtin module.

The opening snippet opens an in-memory database

02
First run
The opening snippet imports Database and opens an in-memory database
Source: bun.com/docs/runtime/sqlite, captured 2026-09-04 · assets/docs/doc-opening-hello.png

The top of the runtime sqlite page opens with the bun sqlite import. Then new Database colon memory. Then db dot query with select Hello world as message. Then dot get. Query is one call. Dot get is the other.

hello.ts returns Hello world after query, then get

The hello program returns Hello world after query, then get
hello.ts
import { Database } from "bun:sqlite";
const db = new Database(":memory:");
const query = db.query("select 'Hello world' as message;");
console.log(query.get());
$ bun hello.ts
{
  message: "Hello world",
}
Source: assets/captures/03-hello.txt, run 2026-09-04 on Bun 1.4.0+34cbb9a40 · assets/lab/hello.ts

Run hello.ts on this 1.4.0 binary. The captured print is message Hello world, with the trailing comma Bun’s inspect adds. The lab is hello.ts. We called query, then we called get. Query and get are two different calls.

db.query compiles and caches the statement

03
The shape
db.query compiles the SQL and caches the compiled statement
MAX_QUERY_CACHE_SIZE · 20
the query is not executed
cache keeps the twenty most recently used SQL strings
Source: bun.com/docs/runtime/sqlite#query · cache size from assets/captures/10-cache-size.txt

On the query section of bun.com/docs/runtime/sqlite, the page says the query is not executed. db dot query takes a SQL string and prepares a Statement. db dot query compiles the SQL into a reusable object and caches that object on the Database. A prepared statement is compiled SQL you can run more than once. The cache holds that compiled object. The same SQL string returns the same Statement. The cache keeps the twenty most recently used SQL strings. That twenty is Database dot MAX QUERY CACHE SIZE, readable from JavaScript on this binary. When a statement is evicted the statement still works. A later query of the same string compiles a new Statement. db dot prepare compiles a fresh Statement for generated SQL. The page credits better-sqlite3 for the synchronous API shape.

get, all, and run step the compiled statement

get, all, and run are the calls that step the compiled statement
.get()
first row or null
.all()
array of objects
.run()
{ lastInsertRowid, changes }
db.run(sql) · one-shot for schema, alias exec
compile → step
Source: bun.com/docs/runtime/sqlite Statements · demonstrated in assets/captures/03-hello.txt and 05-two-row-file.txt

The step is a later call. Dot get returns the first row, or null if there is no row. Dot all returns an array of objects. Dot run steps once and returns last insert row id and changes. db dot run, alias exec, is the one-shot for schema. The hello program was query compiling, then dot get doing the step. Compile, then step. Two calls exist because compile and step are separate.

A missing bind key becomes null by default

A missing bind key becomes null on the default constructor
the SQL says $message; the object you pass uses the same prefix
constructortypo {messag:"typo"}
default[{"message":null}]
strict: trueMissing parameter "message"
Source: bun.com/docs/runtime/sqlite#strict-mode · measured 2026-09-04 in assets/captures/06-bind-strict.txt

Binding is a second compile-time contract. The SQL can name a parameter with dollar, colon, or at, or with a numbered question mark. By default, the object you pass must use the same prefix. A missing key becomes null. Measured on this 1.4.0 binary: select dollar message, with messag spelled wrong, returns message null. Strict true is an option on the constructor. Missing keys throw. Keys may omit the dollar, the colon, or the at. The same typo under strict true throws Missing parameter message. Under strict true, the bind object uses the name without the prefix. A dollar message key is the trap in issue 13409.

new Database with a filename creates the sqlite file

new Database with a filename creates the sqlite file
new Database("mydb.sqlite") · creates or opens that file
{ create: true } · documented create if missing
:memory: · "" · no-arg · the same in-memory database
default open of a missing file also creates it
-wal · pending writes -shm · shared-memory index
WAL is a pragma that writes those two files
Source: bun.com/docs/runtime/sqlite Database, #wal-mode · default-create: assets/captures/04-create-vs-default.txt

Open is a constructor. new Database mydb.sqlite creates or opens that file. Colon memory, an empty string, and no argument are the same in-memory database. The documented create-if-missing form is create true. Default open of a missing file also creates the file on this binary. The visible first result uses a file, so you can ls the file. Write-ahead logging is a pragma you run. Constructor flags are create, readonly, and strict. You run pragma journal mode equals WAL. Writes go to a dash wal file. Dash shm is the shared-memory index. Those are two extra files SQLite writes next to the database. Those sidecar files can remain after close.

One prepared insert writes Ada, then Grace

04
The real thing
One prepared insert writes Ada, then Grace, into a users table
index.ts
import { Database } from "bun:sqlite";

const db = new Database("mydb.sqlite", { create: true });
db.run(
  "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)"
);
const insert = db.query(
  "INSERT INTO users (name) VALUES ($name)"
);
insert.run({ $name: "Ada" });
insert.run({ $name: "Grace" });
const rows = db.query(
  "SELECT id, name FROM users ORDER BY id"
).all();
console.log(rows);
Source: assets/lab/index.ts, run 2026-09-04 on Bun 1.4.0+34cbb9a40

Now build the table the title promised. new Database mydb.sqlite, create true. db dot run creates a users table with an integer id and a text name. Then one prepared insert: insert into users name values dollar name. That insert statement is compiled once. The same Statement runs twice. First bind, dollar name Ada. Second bind, dollar name Grace. The bind object uses dollar name, the same prefix as the SQL. Then a select of id and name, ordered by id. Dot all is the step that prints the rows.

dot all printed Ada then Grace

dot all printed Ada then Grace, and toString showed Ada written into the SQL
$ bun index.ts
[
  {
    id: 1,
    name: "Ada",
  }, {
    id: 2,
    name: "Grace",
  }
]
tostring.ts · SELECT $name as name
$ bun tostring.ts
before bind: "SELECT NULL as name;"
after Ada: "SELECT 'Ada' as name;"
after Grace: "SELECT 'Grace' as name;"
Source: assets/captures/05-two-row-file.txt · toString: assets/captures/08-tostring.txt (SELECT $name as name, after the Ada bind)

The captured inspect dump is Ada, then Grace, with trailing commas. After binding Ada into a select dollar name as name, query toString prints select Ada as name. That print is the compiled statement with Ada written into the SQL you can read.

ls shows mydb.sqlite sitting next to index.ts

ls shows mydb.sqlite sitting next to index.ts
$ ls -la
total 24
drwx------     4 siderakis  wheel    128 Sep  4 20:29 .
drwxrwxrwt  1050 root       wheel  33600 Sep  4 20:29 ..
-rw-r--r--     1 siderakis  wheel    384 Sep  4 20:29 index.ts
-rw-r--r--@    1 siderakis  wheel   8192 Sep  4 20:29 mydb.sqlite

$ ls mydb.sqlite
mydb.sqlite
index.ts · 384 bytes
mydb.sqlite · 8192 bytes
the directory listing is those two files
Source: assets/captures/05-two-row-file.txt, run 2026-09-04 on Bun 1.4.0+34cbb9a40

ls mydb.sqlite. The file is there, eight thousand one hundred and ninety-two bytes. Next to mydb.sqlite, index.ts. The directory listing is those two files. A table that used to wait on Docker Postgres is sitting next to the server.

strict true throws on a dollar-prefixed bind key

05
Gotchas
strict true throws on a dollar-prefixed bind key, and issue 13409 is still open
oven-sh/bun issue 13409, Open
#13409 · Open
strict: true
{ message: "Hello" } → [{"message":"Hello"}] ✓
strict: true
{ $message: "Hello" } → Missing parameter "message" ✗
Source: github.com/oven-sh/bun/issues/13409, still Open 2026-09-04 · assets/captures/06-bind-strict.txt

This is the part people get wrong. Default bind turns a typo into null. Strict true throws Missing parameter message, and strict true lets you omit the prefix. With strict true, passing dollar message still throws Missing parameter message, even though the SQL is dollar message. The docs say allow binding without a prefix. The constructor still rejects the dollar key. Issue 13409 is open as of this recording.

After close, dash-wal and dash-shm are still on disk

After close, the dash-wal and dash-shm files are still on disk
#27481 · Closed · PR #28212
oven-sh/bun issue 27481, Closed, PR 28212
after close()
waltest.sqlite
waltest.sqlite-shm
waltest.sqlite-wal
1 · fileControl persist WAL 0
2 · PRAGMA wal_checkpoint(TRUNCATE)
3 · close()
SQLITE_FCNTL_PERSIST_WAL
after recipe
walclean.sqlite
Source: github.com/oven-sh/bun/issues/27481, Closed 2026-03-18 via PR #28212 · leftovers: assets/captures/07-wal-close-1.4.0.txt · recipe: 07c-wal-cleanup-recipe-1.4.0.txt · doc: bun.com/docs/runtime/sqlite#wal-sidecar-file-cleanup

This is the part people get wrong. After write-ahead logging and close, on this Mac the dash wal and dash shm files still exist. Those leftover files exist on 1.4.0 and on 1.4.1. Issue 27481 is closed. Issue 27481 closed on March the eighteenth, 2026, through pull request 28212. Pull request 28212 added the cleanup recipe to the page. The recipe is: turn off persistent write-ahead logging with file control, run pragma wal checkpoint truncate, then close. After the recipe, only the database file remains.

This binary answers sqlite_version 3.51.0

Print sqlite_version. This binary answers 3.51.0
$ bun sqlite-version.ts
{
  v: "3.51.0",
}
#16717 · Open · describes 3.43.2 #31247 · Closed as duplicate /usr/bin/sqlite3 · 3.51.0
Source: assets/captures/02-sqlite-version-1.4.0.txt · issue 16717 still Open 2026-09-04 describing 3.43.2, citation only · Apple CLI: assets/captures/14-apple-sqlite-cli.txt · sqlite.org 3.51.0 released 2025-11-04

This is the part people get wrong. Issue 16717 is still open. It describes sqlite version 3.43.2. Print the function. This 1.4.0 binary, revision 34cbb9a40, prints 3.51.0. 1.4.1 prints the same 3.51.0. sqlite.org released 3.51.0 on November the fourth, 2025. This Mac’s system sqlite3 also prints 3.51.0.

The whole file is nine lines

The whole file is nine lines, and close true then throws Database has closed
index.ts
import { Database } from "bun:sqlite";

const db = new Database("mydb.sqlite", { create: true });
db.run(
  "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)"
);
const insert = db.query(
  "INSERT INTO users (name) VALUES ($name)"
);
insert.run({ $name: "Ada" });
insert.run({ $name: "Grace" });
const rows = db.query(
  "SELECT id, name FROM users ORDER BY id"
).all();
console.log(rows);
close(true) then query.get()
THROW: Database has closed
Source: assets/lab/index.ts · close: assets/captures/09-close-using.txt

Here is the whole file. Import Database from bun sqlite. Open a file. One prepared insert used twice. One select. Beside the script, mydb.sqlite. Close true, then a later get throws Database has closed.

The three issues were checked today

The three issues in this episode are public, and every state was checked today
Bun 1.4.0 · 34cbb9a40
#13409 open
#27481 closed
#16717 open
Source: bun.com/docs/runtime/sqlite · github.com/oven-sh/bun, all states checked 2026-09-04

So take one question back to your own project. How many of the databases in your stack would fit in a file next to the server?