Development
bun install
bun run typecheck
bun run lint
bun run format:check # `bun run format` to apply
bun test
Layout
| Path | Contents |
|---|---|
src/ | The adapter. |
test/unit/ | Pure-function and adapter-level tests. |
test/differential/ | Parity comparison against the official adapter. |
test/e2e/ | A generated PrismaClient against a real schema. |
test/cli/ | prisma migrate / db push / db pull under Bun. |
docs/ | Guides and the generated API reference. |
site/ | This documentation site. |
Within src/, conversion.ts and errors.ts hold most of the correctness risk
and depend on nothing but their inputs — no Database, no filesystem — so the bulk
of the test coverage lives against them directly.
The four test layers
Each catches things the others structurally cannot, which is the reason for having all four rather than the largest one.
Unit
bun test test/unit
Argument mapping across every scalar type, row mapping, column-type inference, error translation, statement-cache eviction, and mutex ordering. Also covers the adapter surface directly: transactions, savepoints, connection ownership, PRAGMAs.
Differential
bun test test/differential
bun run fixtures # regenerate the reference fixture (requires Node)
Runs a shared corpus through this adapter and through
@prisma/adapter-better-sqlite3, comparing full result sets.
The two cannot run in one process, because Bun refuses to load better-sqlite3.
So test/differential/corpus.ts is consumed twice: by a Node script that writes
fixtures/reference.json, and by the Bun test that compares against it. Both sides
share the same serializer, which encodes bigint and byte values as tagged forms —
JSON.stringify throws on the former and Node’s Buffer serialises differently
from Bun’s Uint8Array, so the comparison has to be byte-level rather than
representational.
The fixture is committed, and CI regenerates it and fails on any diff. Without that check it would quietly become a snapshot of this adapter’s own behaviour the moment the reference changes.
To record an intentional divergence, add divergence: { reason, expect } to the
corpus case. The suite then asserts the stated result and that the reference
still disagrees, so the exemption cannot outlive its justification.
End-to-end
cd test/e2e && bunx prisma generate --config prisma.config.ts
bun test test/e2e
A real schema and generated client: CRUD, defaults, filtering, aggregates, nested writes, cascades, batch and interactive transactions, and raw queries. The unit and differential layers both sit below Prisma’s query compiler, so this is the only one that proves the adapter works through it.
It earns its place. It caught a bug neither other layer could: with
usePhantomQuery: false, Prisma issues the closing COMMIT itself through
executeRaw and only then calls commit(). An implementation that also issued one
failed with cannot commit - no transaction is active and desynchronised Prisma’s
transaction state machine.
CLI
bun test test/cli
Runs migrate dev, db push, and db pull in temporary workspaces and checks the
resulting database is readable and writable through the adapter. Since the CLI does
not route through the adapter (see Migrations), these assert what
actually matters to a user rather than a call chain that does not exist.
Documentation
bun run docs:api # regenerate docs/api from the source JSDoc
cd site && bun run dev
cd site && bun run build && bun run check:links # verify every internal link resolves
The site emits `sitemap-index.xml`, `robots.txt`, and `llms.txt`, and every page
carries JSON-LD (`SoftwareSourceCode` on the home page, `TechArticle` for guides,
`APIReference` for the reference, plus a `BreadcrumbList`). `llms.txt` is the
answer-engine summary — keep it accurate when the guides change, since it is what
an LLM reads instead of crawling the site.
Note that `robots.txt` is inert on GitHub Pages project sites: crawlers read it
from the domain root, which this repository does not own. It is committed so the
rules travel if the docs ever move to their own domain.
The API reference under docs/api/ is generated from the doc comments in src/,
so it is not edited by hand. Guides under docs/guides/ are written directly. The
site reads both.
Linting
ESLint with typescript-eslint (type-checked) and eslint-plugin-unicorn.
Violations are fixed rather than suppressed. Two exemptions remain, both justified
where they appear:
unicorn/prefer-number-coercionon twoparseFloatcalls.Number()disagrees withparseFloaton trailing garbage and whitespace-only input, and the official adapter usesparseFloat, so switching would be a silent behavioural divergence.unicorn/no-null, project-wide.bun:sqlitedeclaresStatement.declaredTypesasArray<string | null>and Prisma’sResultValueincludesnull, soundefinedwould not typecheck.
Every class member carries an explicit public/private/protected modifier,
enforced by @typescript-eslint/explicit-member-accessibility.
Formatting is Prettier’s, checked in CI by format:check. eslint-config-prettier
is applied last in the ESLint config so the two cannot disagree. Generated
artifacts and Markdown prose are excluded — see .prettierignore for why.
Both staleness gates (the differential fixture and the API reference) call
scripts/assert-generated-current.sh, so the reasoning behind using
git status --porcelain rather than git diff lives in one place.
site/ is excluded from this config. It is a separate package with its own
dependencies and Astro-generated types, so linting it from the root would mean
installing them and running astro sync first. It is typechecked by
astro check, which the Docs workflow runs on every deploy.