npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

kysely-bun-sqlite-dialect

v1.0.0

Published

Kysely dialect for Bun's native bun:sqlite driver

Readme

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.