Skip to content
bun:sqlite/ prisma adapter

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 valuebetter-sqlite3bun:sqlite
1realinteger
1.5realreal
2 ** 40realinteger

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.