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

@8x/kysely-d1

v1.1.0

Published

A Kysely dialect for Cloudflare D1, with insertId support and typed atomic batches.

Readme

@8x/kysely-d1

Install

bun add @8x/kysely-d1

Usage

Create a Kysely instance (recommended)

initD1Kysely takes a D1Database and returns a Kysely instance configured with the D1 dialect.

import { initD1Kysely } from "@8x/kysely-d1";

// Example database type
type Database = {
	Comment: {
		id: number;
		content: string;
		isHidden: 0 | 1;
	};
};

export default {
	async fetch(_req: Request, env: { DB: D1Database }) {
		const db = initD1Kysely<Database>(env.DB);

		const comments = await db.selectFrom("Comment").selectAll().execute();
		const rows = await db
			.selectFrom("Comment")
			.select(["Comment.content", "id", "isHidden as is"])
			.execute();

		return Response.json({ comments, rows });
	},
};

Create manually with D1SQLiteDialect

If you prefer to construct Kysely yourself, you can use D1SQLiteDialect directly.

import { Kysely } from "kysely";
import { D1SQLiteDialect } from "@8x/kysely-d1";

const db = new Kysely<Database>({
	dialect: new D1SQLiteDialect(env.DB),
});

Run batch (atomic on D1)

runBatch(db, queries) executes an array of Kysely queries (Compilable) using D1's db.batch(). Per D1 behavior, a batch is executed atomically; if any statement fails, the batch is rolled back.

import { runBatch } from "@8x/kysely-d1";

const result = await runBatch(env.DB, [
	db.insertInto("Person").values({ first_name: "John" }),
	db.insertInto("Pet").values({ name: "Fido", owner_id: 1 }),
	db.selectFrom("Person").selectAll(),
]);

console.log(result[0].meta.last_row_id); // insert id of the first query
console.log(result[1].meta.last_row_id); // insert id of the second query
console.log(result[2].results); // rows returned by the third (select) query

For insert / update / delete queries without returning(), D1 returns an empty results array (typed as never[]); the affected-row info lives in meta (last_row_id, changes). Add returning() / returningAll() to get typed rows back.

Chaining a generated id into a later statement in the same batch

All queries passed to runBatch are compiled up front, before any of them run, so a later statement can't reference the JS value of an id generated earlier in the same call (it doesn't exist yet at compile time — unlike calling runBatch / .execute() separately per statement and threading the returned id through your own code).

SQLite's last_insert_rowid(), via Kysely's sql tag, is evaluated while the batch runs instead:

import { sql } from "kysely";
import { runBatch } from "@8x/kysely-d1";

const result = await runBatch(env.DB, [
	db.insertInto("Person").values({ first_name: "John" }),
	// No need to know Person's new id ahead of time — `last_insert_rowid()` resolves
	// to it once the batch actually runs.
	db.insertInto("Pet").values({
		name: "Fido",
		owner_id: sql<number>`last_insert_rowid()`,
	}),
]);

console.log(result[0].meta.last_row_id); // Person's generated id — but see the caveats

Read the caveats before using this. last_insert_rowid() does not mean "the id from the previous statement". It returns the rowid of the most recent successful INSERT into a rowid table on the database connection, so it is only safe for the narrow shape above: a parent INSERT that actually inserts exactly one row into a table whose primary key is INTEGER PRIMARY KEY, followed by a single child statement inserting a single row, with no other insert in between. Each of the following yields a wrong id — usually silently, because a wrong rowid is often still a valid parent id, though it surfaces as a foreign key error when it happens not to be:

  • More than one child statement. The second child reads back the first child's rowid, not the parent's, because that insert became the most recent one. If that rowid happens to be a valid parent id, the foreign key still holds and the row commits against the wrong parent.
  • A multi-row parent insert. values([a, b, c]) — or INSERT ... SELECT — leaves last_insert_rowid() pointing at c, not at a.
  • A multi-row child insert. The value is re-evaluated per row, so rows after the first read back the child's own rowids.
  • A parent that inserts nothing (on conflict do nothing, or an ignored insert or ignore). The value stays at whatever it was before.
  • A parent on conflict do update that takes the UPDATE path. meta.changes is 1, but no row was inserted, so the value is stale. Checking meta.changes >= 1 does not make this safe — the same known limitation that insertId carries for upserts (see docs/ROADMAP.md).
  • A parent whose id column is not INTEGER PRIMARY KEY. For a TEXT PRIMARY KEY (uuid) or a plain INT PRIMARY KEY, you get the table's hidden rowid, which is unrelated to the key you wanted; for a WITHOUT ROWID table the value is not updated at all.
  • Referencing it before any insert in the batch. The value comes from the connection's history, not from the batch, and is 0 only on a fresh connection. An insert counts towards it even if its batch later rolled back.

Non-INSERT statements in between (SELECT / UPDATE / DELETE) do not disturb the value, but they do not refresh it either.

If your case doesn't fit the narrow safe shape, use returning() and split the work into separate round trips, or make the relationship expressible without the generated id (e.g. a natural key).

One more caveat: this relies on all statements in a batch running on the same connection. D1 documents that a batch's statements execute sequentially, non-concurrently, as a single transaction, but it does not explicitly document connection identity — and the test for this is run against local (Miniflare) D1.

Notes / Limitations

  • Kysely transaction() is not supported because D1 does not support SQL transaction statements like BEGIN, COMMIT, and ROLLBACK.
  • streamQuery is not supported.
  • The package is published as compiled ESM: dist/*.js plus dist/*.d.ts, with . as the only entry point. src/ ships alongside it so that source maps and "go to definition" land on the original TypeScript, but it is not importable — resolve through the package name.
  • ESM only. There is no CommonJS build — Workers, wrangler, Vite and Bun are all ESM. Node versions that support require(esm) (20.19+ / 22.12+) can still require() it; older ones cannot.
  • D1Database / D1Result are referenced as ambient globals, not imported. You get them from wrangler types (worker-configuration.d.ts), which is the usual Workers setup, or from @cloudflare/workers-types — but that package is not under @types/, so installing it is not enough: add "types": ["@cloudflare/workers-types"] to your tsconfig.json. It is declared as an optional peer dependency to record that requirement; you do not need to install it if wrangler types already gives you the globals. (Do not use both — their declarations collide.)
  • D1 reports failures by rejecting, so a failed statement rejects out of execute() rather than coming back as an empty result set. A rejection is passed through untouched. If a D1-compatible stand-in resolves with a failure instead, that is converted to a rejection shaped like the binding's own — D1_ERROR: <message>, with the bare message as cause.

Installing from a git URL

Prefer the registry. A git dependency ships no prebuilt dist/, so it relies on the prepare lifecycle script to build one — and bun blocks lifecycle scripts by default, which leaves the package installed but without its entry point. The install reports success; the failure surfaces later as Cannot find package '@8x/kysely-d1'.

bun pm untrusted            # shows the blocked script
bun pm trust @8x/kysely-d1  # then reinstall

npm, pnpm and yarn install devDependencies before running prepare, so they build it without extra steps.

Development

bun install
bun run typecheck   # wrangler types && tsc
bun run build       # tsc -p tsconfig.build.json -> dist/
bun run test        # vitest run