kysely-bun-sqlite-dialect
v1.0.0
Published
Kysely dialect for Bun's native bun:sqlite driver
Maintainers
Readme
kysely-bun-sqlite-dialect
A Kysely dialect for Bun's native
bun:sqlite driver.
Install
bun add kysely-bun-sqlite-dialect kyselyUsage
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 yourDatabase.await db.destroy()callsdatabase.close(), the same as Kysely's built-in dialect. That close is lazy where streams are concerned:bun:sqlitereleases 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, orbreaking out of it, finalizes immediately.Dateparameters are rejected.bun:sqlitebinds only strings,TypedArrays, booleans, numbers, bigints and null, so aDateraisesBinding expected string, TypedArray, boolean, number, bigint or null. Store timestamps as ISO strings or epoch numbers. (better-sqlite3rejects them too.)insert … returninggives you rows, not aninsertId. As withbetter-sqlite3, a statement with areturningclause 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 personreturns"nope"as a value. better-sqlite3 compiles withSQLITE_DQS=0and raisesno such columninstead, 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 wheretransactionBehavior: "immediate"matters most, and where write contention surfaces as aSQLITE_BUSYerror rather than a hang. safeIntegersworks. Set it on theDatabaseand integer columns come back asbigint.- Errors are not wrapped. Bun's error reaches you as-is, with its
code(SQLITE_CONSTRAINT_UNIQUEand friends) anderrnointact.
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 testbunfig.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.
