Differences from the official adapter
The bar for this package is parity with @prisma/adapter-better-sqlite3: same
behaviour, same result shapes. That is measured rather than asserted — a shared
corpus runs through both adapters and the full result sets are compared.
Every difference below is declared, with a reason. The ones a query can observe are declared in the test corpus, and the suite asserts both that this adapter produces the stated result and that the reference still disagrees, so an exemption cannot outlive the behaviour that justified it. The remaining deviations cannot be reached by any query Prisma generates; each is described below and marked unreachable rather than declared in the corpus.
Bound integers infer a different column type
The drivers disagree about how to bind a JavaScript number. Verified with
SELECT typeof(?) on both:
| Bound value | better-sqlite3 | bun:sqlite |
|---|---|---|
1 | real | integer |
1.5 | real | real |
2 ** 40 | real | integer |
better-sqlite3 binds every JS number as SQLite REAL; bun:sqlite inspects
integrality and binds whole numbers as INTEGER. So a bound integer read straight
back out — SELECT ? AS x — is a float there and an integer here, and the inferred
Prisma column type differs accordingly.
What this affects in practice: very little. Row values are identical. Any
column with a declared type is unaffected, because SQLite applies column affinity
on write, so an integer bound as REAL still lands in an INTEGER column as an
integer. The difference is observable only for expression columns that echo a bound
parameter, which Prisma’s own queries do not generate.
This is a driver-level difference, not a choice this adapter makes.
BLOBs from undeclared columns work here
The official adapter throws UnexpectedTypeError when a BLOB reaches value
inference — that is, when it comes from a column with no declared type:
await adapter.queryRaw({ sql: "SELECT X'DEADBEEF' AS b", args: [], argTypes: [] })
// UnexpectedTypeError: unexpected value of type object
Its type inference accepts only ArrayBuffer, while better-sqlite3 returns a
Buffer. randomblob() and zeroblob() fail the same way; a column declared
BLOB is fine, because the declared type is handled before inference runs.
This adapter checks for Uint8Array, which covers both Buffer and Bun’s return
type, and maps it to Bytes as intended.
Reported upstream as prisma/prisma#29868.
A failing BEGIN does not deadlock
SQLite permits a single writer, so both adapters serialise transactions behind a
mutex. The official adapter acquires that mutex and then runs BEGIN, but only
hands the release callback to the transaction object on the success path. If
BEGIN throws — for example because another transaction is already open on that
connection — the lock is never released and every subsequent transaction on that
adapter blocks forever.
This adapter releases the mutex before rethrowing. There is a regression test that
provokes a failing BEGIN and then asserts the next transaction still starts.
Deliberate non-differences
Two places where matching the reference meant not doing the more obvious thing:
Column type inference for all-NULL columns. Bun exposes
Statement.columnTypes, the actual storage class of each column, which
better-sqlite3 has no equivalent for. It would be a better fallback than the
reference’s behaviour of guessing Int32 when a column is NULL in every row. It is
deliberately unused: adopting it would change columnTypes output for exactly the
cases the differential suite exists to check.
lastInsertId. The reference never populates it, because Prisma’s SQLite
queries recover generated ids through RETURNING. Setting it would break
result-set parity for no gain, so this adapter leaves it unset too.
Mismatched argument metadata is rejected
Prisma supplies one argTypes entry per argument. If a caller supplies fewer, this
adapter rejects the query with InvalidInputValue; the reference succeeds for
numbers, booleans, null, and Dates (it dereferences argType.scalarType only
for string arguments, where it throws a raw TypeError).
Rejecting is deliberate. bun:sqlite binds an object it does not recognise as SQL
NULL without raising, so passing an unconverted Date through would write
NULL and lose the value silently. Failing loudly is worth the divergence, and no
Prisma-generated query reaches it.
Constraint field lists
When a constraint message carries no usable column names, this adapter omits the
constraint rather than handing Prisma { fields: [''] } as the reference does. Only
malformed messages reach this path.
Larger query batches
This adapter implements getConnectionInfo; the reference does not. Without it
Prisma assumes SQLite accepts 999 bind values — the limit SQLite raised in 3.32.
Bun’s SQLite 3.53 build accepts 250,000 (measured), so this adapter reports 32,766,
the modern SQLite default, and the query compiler batches accordingly instead of
splitting statements it did not need to.
A unit test binds exactly the reported number of parameters, so a future Bun that lowered the limit would fail the suite rather than produce queries SQLite rejects.
A footnote: ragged rows
One further deviation exists, smaller than those above. When
getColumnTypes scans rows to infer the type of an undeclared column, it skips
values that are null or undefined; the reference skips only null. A row
holding fewer values than there are declared columns therefore makes the reference
throw UnexpectedTypeError on the undefined, while this adapter falls back to
Int32 as it does for an all-NULL column.
It is not a headline divergence because it is not reachable: every row a driver returns has one value per declared column, and Prisma-generated SQL produces nothing else. No query can be written that distinguishes the two adapters here, which is also why the differential corpus does not declare it as an exemption. It is defensive only — a guard against a malformed result set turning a type-inference pass into a crash.
Performance
Prepared statements are cached per connection, keyed by SQL text. The official
adapter calls prepare() on every queryRaw and executeRaw, re-parsing
identical SQL every time. Since Prisma’s query compiler emits a small set of
recurring SQL shapes, the cache hits often.
Caching is also where safeIntegers is applied. Unlike better-sqlite3,
bun:sqlite has no connection-level defaultSafeIntegers — only a per-statement
setter — so caching means it is paid once per distinct SQL string rather than once
per query.