@vibeorm/adapter-better-sqlite3
v4.3.0
Published
better-sqlite3 adapter for VibeORM v2 — Node.js or Bun (napi)
Maintainers
Readme
@vibeorm/adapter-better-sqlite3
Part of VibeORM — a type-safe TypeScript ORM for Bun and Node. Prisma-schema or TypeScript-DSL input, a canonical schema IR, a generated client, and a dialect-aware SQL layer over PostgreSQL, PGlite, SQLite and MySQL.
SQLite adapter built on better-sqlite3, the Node-compatible sibling of @vibeorm/adapter-sqlite (which is bun:sqlite and therefore Bun-only). Same dialect, same semantics, same error mapping; only the driver and the runtime story differ.
npm i @vibeorm/adapter-better-sqlite3 better-sqlite3The vibeorm CLI (generate, db push, migrate …) runs on Bun, also in a Node project, and opens a SQLite file through bun:sqlite, not through this adapter. So a Node project that runs CLI commands against SQLite installs the CLI and the Bun SQLite adapter as development dependencies; without @vibeorm/adapter-sqlite, vibeorm db push refuses with VIBE_CONFIG ("Could not load @vibeorm/adapter-sqlite"):
bun add -d vibeorm @vibeorm/adapter-sqliteYour application code still runs on Node with this adapter.
Stable.
2.0.0, published under thelatestdist-tag. The v1 line stays installable asvibeorm@1.
Usage
import { VibeClient } from "./generated/vibeorm/index.js";
import { betterSqliteAdapter } from "@vibeorm/adapter-better-sqlite3";
const db = VibeClient({ adapter: betterSqliteAdapter({ filename: "./app.db" }) });
const users = await db.user.findMany({ where: { email: { contains: "@example.com" } }, take: 10 });
await db.$disconnect();Options
betterSqliteAdapter(options?: BetterSqliteAdapterOptions)
| Option | Type | Default | Meaning |
|---|---|---|---|
| filename | string | ":memory:" | Database file to open. Ignored when database is injected. |
| readonly | boolean | false | Open the file read-only. Writes fail with SQLITE_READONLY, mapped to VIBE_ADAPTER. The WAL pragma is skipped. |
| busyTimeoutMs | number | 5000 | PRAGMA busy_timeout in milliseconds. Must be a non-negative integer, validated eagerly (VIBE_VALIDATION). |
| wal | boolean | true | Switch file databases to PRAGMA journal_mode = WAL. Skipped for in-memory, read-only and injected databases. |
| database | BetterSqliteDatabaseLike (an open better-sqlite3 Database satisfies it) | — | Run on an already-open database. It is never closed (disconnect() only ends the adapter; later calls refuse with VIBE_ADAPTER_CLOSED) and its journal mode is left alone; PRAGMA foreign_keys = ON is still applied. |
Every transaction opens with BEGIN IMMEDIATE, so the write lock is taken up front and a write after a read cannot deadlock on a lock upgrade. Nested transactions become savepoints, and a nested call passing isolationLevel or timeout is refused with VIBE_VALIDATION. The driver is synchronous and single-connection, so every operation runs through one serial queue: a whole transaction() holds the queue until commit or rollback.
Integers are read losslessly: defaultSafeIntegers(true) on connections the adapter opens plus statement-level safeIntegers(true) on every prepared statement, then rows normalize bigint → number inside ±(2^53−1); larger values stay bigint. An ORM read (no bigint value to bind) runs without safeIntegers first; when a cell holds a number past ±(2^53−1) it is read again at once with safeIntegers, and that statement stays on the safeIntegers path — the rows are the same either way, with no bigint allocated per cell for ordinary reads.
The raw path ($queryRaw* / $executeRaw* / DDL) serializes values explicitly, because better-sqlite3 rejects raw booleans, Dates and plain objects at bind time: boolean → 0/1, Date → ISO text, object/array → JSON text, undefined → NULL.
Runtime support
The adapter declares Node.js ≥ 18, but the installed better-sqlite3 peer must also support your Node version. The unversioned install above selects the current driver; better-sqlite3 12 dropped Node 18 support. For Node 18, explicitly select a compatible 11.x release, for example bun add @vibeorm/adapter-better-sqlite3 better-sqlite3@11. Injecting a database does not remove the peer dependency or its installation requirements. The repository's native-driver CI uses Node 22; Node 18 with driver 11 is not covered by that job.
Bun cannot dlopen the addon today (oven-sh/bun#4290). Importing this package is always safe; on a runtime that cannot load the addon the failure surfaces at first database use as VibeError code VIBE_ADAPTER with the driver's refusal as cause. Under Bun use @vibeorm/adapter-sqlite.
Capabilities and limits
Identical to @vibeorm/adapter-sqlite — both run the sqlite dialect, whose frozen capability table is exported to docs/dialects.md. A feature the dialect cannot express throws VibeError code VIBE_UNSUPPORTED_CAPABILITY at the earliest knowable moment (generate, then build, then execute) — it never silently misbehaves.
Refused:
scalarArrays— scalar list columns are rejected at generate and migrate time.lateralJoin— the"join"relation strategy is refused. Use the default"query"strategy.
Fallbacks: enums stored as TEXT + CHECK, mode: "insensitive" as LOWER(x) LIKE LOWER(?), distinct resolved client-side, Json stored as TEXT. Native: RETURNING, createManyAndReturn, partial indexes, transactional DDL, null ordering, and the fts5 full-text engine.
Transaction timeout is refused with VIBE_VALIDATION: SQLite's busy_timeout bounds lock acquisition, not statement runtime. Two further SQLite semantics — ASCII-case-insensitive contains and float64-bounded Decimal comparison — are documented in docs/dialect-notes.md.
Caller cancellation of reads
This adapter declares readCancellation: "before-dispatch" on root, transaction and savepoint handles. Read execution options carry signal separately from query arguments. Abort observed before dispatch, including after the operation queue wait, prevents SQL. An already-started read finishes before successful rows are discarded with VIBE_CANCELLED and databaseCancelled: false; unrelated driver failures keep their mapped codes.
cancellation: "running" refuses with VIBE_UNSUPPORTED_CAPABILITY before database acquisition or preparation. The installed better-sqlite3 public API has no interrupt method, and its native statement execution loops synchronously over SQLite calls on the same thread. This adapter has no safe running-interrupt transport. Writes and raw SQL are outside the contract. Injected databases retain their owner and are never closed for a read signal; no native database is constructed merely to reject stronger cancellation.
Error mapping
The same frozen table as @vibeorm/adapter-sqlite, keyed by the SQLite result-code name. Only VibeError ever escapes.
| SQLite result code | VibeError code |
|---|---|
| SQLITE_CONSTRAINT_UNIQUE, SQLITE_CONSTRAINT_PRIMARYKEY, SQLITE_CONSTRAINT_ROWID | VIBE_UNIQUE_VIOLATION |
| SQLITE_CONSTRAINT_FOREIGNKEY | VIBE_FK_VIOLATION |
| SQLITE_CONSTRAINT_NOTNULL, SQLITE_CONSTRAINT_CHECK, SQLITE_CONSTRAINT_DATATYPE | VIBE_VALIDATION |
| SQLITE_BUSY (+ _SNAPSHOT, _RECOVERY, _TIMEOUT), SQLITE_LOCKED (+ _SHAREDCACHE) | VIBE_TRANSACTION, meta.reason: "busy" |
| anything else | VIBE_ADAPTER |
better-sqlite3 errors carry no numeric errno; the SQLITE_* code string is what maps. Multi-statement text with parameters is refused loudly rather than silently truncated.
