Skip to content

About

A Kysely dialect for Bun's native bun:sqlite driver.

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

kysely-bun-sqlite-dialect

NPM version NPM Downloads TypeScript

A Kysely dialect for Bun's native bun:sqlite driver.

Install

bun add kysely-bun-sqlite-dialect kysely

Usage

import { Database } from "bun:sqlite";
import { Kysely, type Generated } from "kysely";
import { BunSqliteDialect } from "kysely-bun-sqlite-dialect";

interface DB {
  person: {
    id: Generated<number>;
    name: string;
    age: number;
  };
}

const db = new Kysely<DB>({
  dialect: new BunSqliteDialect({
    database: new Database("app.db"),
  }),
});

const people = await db.selectFrom("person").selectAll().execute();

You construct the Database, so every bun:sqlite option is yours to set — strict, safeIntegers, readonly, loadExtension, and any pragmas.

Opening the database lazily

Pass a function to defer opening until the first query. This is also the place to apply pragmas:

new BunSqliteDialect({
  database: async () => {
    const database = new Database("app.db");
    database.run("pragma journal_mode = WAL");
    database.run("pragma foreign_keys = ON");
    return database;
  },
});

The function is called once, on the first query.

Configuration

Option Type Default Description
database Database | (options?) => Promise<Database> — A bun:sqlite database, or a function returning one
transactionBehavior "deferred" | "immediate" | "exclusive" "deferred" How begin opens a transaction
onCreateConnection (connection, options?) => Promise<void> — Called once, after the connection is created

Transactions

db.transaction(), db.startTransaction() and savepoints all work as usual. By default a transaction opens with a bare begin, matching Kysely's built-in SQLite dialect.

In WAL mode with concurrent writers, prefer immediate: a deferred transaction that reads before it writes can fail with SQLITE_BUSY when it tries to upgrade its read lock to a write lock, and begin immediate takes the write lock up front instead.

new BunSqliteDialect({ database, transactionBehavior: "immediate" });

Streaming

.stream() is supported for select queries and yields one row at a time:

for await (const person of db.selectFrom("person").selectAll().stream()) {
  console.log(person.name);
}

SQLite has a single connection, and Kysely holds it for the life of a stream. Do not run another query inside a for await over a stream — it will wait for a connection that the stream is still holding. Collect what you need first, then query. This is Kysely's behavior for every single-connection dialect, not something specific to this one.

Things worth knowing

  • destroy() closes your Database. await db.destroy() calls database.close(), the same as Kysely's built-in dialect. That close is lazy where streams are concerned: bun:sqlite releases the connection only after the last prepared statement is finalized, so a stream abandoned without being run to completion or closed can defer the real close until GC. Consuming a stream fully, or breaking out of it, finalizes immediately.
  • Date parameters are rejected. bun:sqlite binds only strings, TypedArrays, booleans, numbers, bigints and null, so a Date raises Binding expected string, TypedArray, boolean, number, bigint or null. Store timestamps as ISO strings or epoch numbers. (better-sqlite3 rejects them too.)
  • insert … returning gives you rows, not an insertId. As with better-sqlite3, a statement with a returning clause takes the row-reading path; read the values out of the returned rows.
  • Statements are cached by Bun. Queries go through db.query(), so Bun's compiled-statement LRU owns compilation and finalization. Streams are the one exception: they get a private statement, because a cached statement holds a single cursor and would be reset by any re-execution of the same SQL.
  • A mistyped column name yields its own name, not an error. SQLite's double-quoted-string misfeature is enabled in Bun's build, so an unresolvable double-quoted identifier is accepted as a string literal: select "nope" from person returns "nope" as a value. better-sqlite3 compiles with SQLITE_DQS=0 and raises no such column instead, so this is a real difference to watch for when migrating. Kysely quotes all identifiers, so any typo Kysely cannot catch at the type level reaches SQLite this way. It is not an injection risk — the whole token is one literal — but it does mean a bad column reference fails silently.
  • Two handles on one file are fine. A second connection — an auth library with its own Database, say — sees this one's committed writes and vice versa, provided the file is in WAL mode. That is also the setup where transactionBehavior: "immediate" matters most, and where write contention surfaces as a SQLITE_BUSY error rather than a hang.
  • safeIntegers works. Set it on the Database and integer columns come back as bigint.
  • Errors are not wrapped. Bun's error reaches you as-is, with its code (SQLITE_CONSTRAINT_UNIQUE and friends) and errno intact.

Kysely compatibility

Requires Kysely >=0.29.0. CI runs the full suite and type checks against both the declared floor and the pinned development version, so the floor cannot drift from what is actually supported.

Kysely 0.28 is not supported, and the reason is worth stating: in 0.28 the connection mutex that serializes access to a single-connection database lived inside SqliteDriver itself, and 0.29 moved it up into Kysely's RuntimeDriver, driven by the adapter's supportsMultipleConnections. A dialect written against 0.29 therefore has no lock of its own, and on 0.28 parallel transactions interleave on the one connection — a rolled-back transaction's writes can be committed by another. Measured, not theorized.

Contributing

See CONTRIBUTING.md. Development notes below.

Toolchain

Concern Tool
Package manager bun
Bundler tsdown (Rolldown)
Type checking TypeScript 7 (native compiler)
Lint / format biome
Tests bun test (built in, Jest-compatible)
Task runner turbo
Versioning changesets
Publishing npm OIDC trusted publishing (no long-lived token)
bun install
bunx lefthook install
bun test

bunfig.toml sets [run] bun = true, so every script and node_modules/.bin entry executes under Bun's runtime rather than deferring to its #!/usr/bin/env node shebang.

engines.bun is the whole runtime contract; there is deliberately no engines.node. The published JavaScript never imports bun:sqlite itself — it only accepts a Database you hand it — so a Node floor would make npm and pnpm warn EBADENGINE, or fail outright under engineStrict, over what is purely a build-time concern here. scripts/verify-engines.ts fails the build if one reappears.

Node survives in exactly one place: the two release workflows pin it directly, because changesets shells out to the npm CLI and npm is what implements OIDC trusted publishing. The build's syntax target is a named constant in tsdown.config.ts, not derived from any engines field.

publint prints one suggestion on every build — that the package specify engines.node. That is expected and deliberately not acted on. It is a suggestion, not an error, so it does not fail the build; publint offers only a blanket level filter and no per-rule ignore, and silencing every suggestion to hide one is not worth it. Adding the field to quiet it will fail bun run lint:packages.

Note that tsdown.config.ts keeps nodeProtocol: true but exempts bun:sqlite via deps.neverBundle. The option prefixes builtin specifiers with node: and counts bun:sqlite as one, which emits an unresolvable "node:bun:sqlite" into the declaration file — and neither publint nor attw flags it. Exempting the one specifier keeps the prefixing working for real node builtins; disabling the option outright would silently stop normalizing those too.

Development workflow

  • Create a branch and make changes.
  • Create a changeset entry: bun run changeset
  • Commit and open a pull request.
  • When the release PR that changesets opens is merged, the version is bumped, the changelog updated, and the package published.

Lefthook runs lint, formatting, package checks and type checking on pre-commit, lint on pre-push, and commitlint on the commit message.

Scripts

Script Description
bun run build Bundle to dist/, then validate with publint and attw
bun run test Run the test suite once
bun run test:watch Run the test suite in watch mode
bun run verify-types Type check without emitting
bun run lint Lint and format src/
bun run lint:packages Check dependency ranges (syncpack) and the Bun version couplings
bun run syncpack:update Update all dependencies to their latest versions and reinstall
bun run clean Remove .turbo, node_modules and dist

Built and maintained by Disaresta. MIT licensed.

About

A Kysely dialect for Bun's native bun:sqlite driver.

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages