first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,110 @@
# @suaveplan/db-sqlite - Universal SQLite adapter for Suaveplan: bun:sqlite on the Bun server, @sqlite.org/sqlite-wasm in the browser, unified behind a Drizzle-ORM surface with LRU prepared-statement cache, forward-only migration runner, OTel instrumentation, and structured SQLITE.* error codes.
## Summary
Universal SQLite adapter for Suaveplan. Runs on `bun:sqlite` on the Bun server and `@sqlite.org/sqlite-wasm` (Web Worker) in the browser, unified behind a single `SqliteAdapter` that implements `DatabaseAdapter` from `@suaveplan/db`. Includes an LRU prepared-statement cache (128 entries by default), a forward-only ledger-tracked `MigrationRunner` with dry-run support, OpenTelemetry span instrumentation, structured `SQLITE_*` error codes, and a `./unsafe` subpath for raw queries.
<!-- begin:auto-docs -->
## Documentation tree
- [adapter](./src/adapter/adapter.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - adapter.wasm — Browser-runtime entry point for the……
- [adapter.bun](./src/adapter/adapter.bun.md) — Bun-runtime entry point for the `./adapter` conditional export in `@suaveplan/db-sqlite`. When a bundler or the Bun run…
- [adapter](./src/adapter/adapter.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - adapter.wasm — Browser-runtime entry point for the……
- [adapter.wasm](./src/adapter/adapter.wasm.md) — Browser-runtime entry point for the `./adapter` conditional export in `@suaveplan/db-sqlite`. When a browser bundler re…
- [cache](./src/cache/cache.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [cache](./src/cache/cache.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [conformance](./src/conformance/conformance-suite.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [conformance-suite](./src/conformance/conformance-suite.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [connection](./src/connection/connection.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [connection](./src/connection/connection.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [bun-driver](./src/drivers/bun-driver.md) — Provides `createBunDriver`, the server-side SQLite driver for `@suaveplan/db-sqlite`. It dynamically imports `bun:sqlit…
- [wasm-driver](./src/drivers/wasm-driver.md) — Provides `createWasmDriver`, the browser-side SQLite driver for `@suaveplan/db-sqlite`. It spawns a Web Worker running…
- [wasm-worker](./src/drivers/wasm-worker.md) — Web Worker implementation for the WASM SQLite driver. This module runs inside a dedicated worker thread created by `cre…
- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [migration](./src/migration/migration.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [migration](./src/migration/migration.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [unsafe](./src/unsafe/unsafe.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [unsafe](./src/unsafe/unsafe.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
<!-- end:auto-docs --> - adapter.wasm — Browser-runtime entry point for the……
- [adapter.bun](./src/adapter/adapter.bun.md) — Bun-runtime entry point for the `./adapter` conditional export in `@suaveplan/db-sqlite`. When a bundler or the Bun run…
- [adapter](./src/adapter/adapter.md) — <!-- begin:auto-docs --> _(no submodules)_- adapter.wasm — Browser-runtime entry point for the……
- [adapter.wasm](./src/adapter/adapter.wasm.md) — Browser-runtime entry point for the `./adapter` conditional export in `@suaveplan/db-sqlite`. When a browser bundler re…
- [cache](./src/cache/cache.md) — <!-- begin:auto-docs --> _(no submodules)_- [cache](./src/cache/cache.md) — <!-- begin:auto-docs --> _(no submodules)_- [conformance](./src/conformance/conformance-suite.md) — <!-- begin:auto-docs --> _(no submodules)_- [conformance-suite](./src/conformance/conformance-suite.md) — <!-- begin:auto-docs --> _(no submodules)_- [connection](./src/connection/connection.md) — <!-- begin:auto-docs --> _(no submodules)_- [connection](./src/connection/connection.md) — <!-- begin:auto-docs --> _(no submodules)_- [bun-driver](./src/drivers/bun-driver.md) — Provides `createBunDriver`, the server-side SQLite driver for `@suaveplan/db-sqlite`. It dynamically imports `bun:sqlit…
- [wasm-driver](./src/drivers/wasm-driver.md) — Provides `createWasmDriver`, the browser-side SQLite driver for `@suaveplan/db-sqlite`. It spawns a Web Worker running…
- [wasm-worker](./src/drivers/wasm-worker.md) — Web Worker implementation for the WASM SQLite driver. This module runs inside a dedicated worker thread created by `cre…
- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_- [migration](./src/migration/migration.md) — <!-- begin:auto-docs --> _(no submodules)_- [migration](./src/migration/migration.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [unsafe](./src/unsafe/unsafe.md) — <!-- begin:auto-docs --> _(no submodules)_- [unsafe](./src/unsafe/unsafe.md) — <!-- begin:auto-docs --> _(no submodules)_- adapter.wasm — Browser-runtime entry point for the……
- [adapter.bun](./src/adapter/adapter.bun.md) — Bun-runtime entry point for the `./adapter` conditional export in `@suaveplan/db-sqlite`. When a bundler or the Bun run…
- [adapter](./src/adapter/adapter.md) — <!-- begin:auto-docs --> _(no submodules)_- adapter.wasm — Browser-runtime entry point for the……
- [adapter.wasm](./src/adapter/adapter.wasm.md) — Browser-runtime entry point for the `./adapter` conditional export in `@suaveplan/db-sqlite`. When a browser bundler re…
- [cache](./src/cache/cache.md) — <!-- begin:auto-docs --> _(no submodules)_- [cache](./src/cache/cache.md) — <!-- begin:auto-docs --> _(no submodules)_- [conformance](./src/conformance/conformance-suite.md) — <!-- begin:auto-docs --> _(no submodules)_- [conformance-suite](./src/conformance/conformance-suite.md) — <!-- begin:auto-docs --> _(no submodules)_- [connection](./src/connection/connection.md) — <!-- begin:auto-docs --> _(no submodules)_- [connection](./src/connection/connection.md) — <!-- begin:auto-docs --> _(no submodules)_- [bun-driver](./src/drivers/bun-driver.md) — Provides `createBunDriver`, the server-side SQLite driver for `@suaveplan/db-sqlite`. It dynamically imports `bun:sqlit…
- [wasm-driver](./src/drivers/wasm-driver.md) — Provides `createWasmDriver`, the browser-side SQLite driver for `@suaveplan/db-sqlite`. It spawns a Web Worker running…
- [wasm-worker](./src/drivers/wasm-worker.md) — Web Worker implementation for the WASM SQLite driver. This module runs inside a dedicated worker thread created by `cre…
- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_- [migration](./src/migration/migration.md) — <!-- begin:auto-docs --> _(no submodules)_- [migration](./src/migration/migration.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [unsafe](./src/unsafe/unsafe.md) — <!-- begin:auto-docs --> _(no submodules)_- [unsafe](./src/unsafe/unsafe.md) — <!-- begin:auto-docs --> _(no submodules)_- adapter.wasm — Browser-runtime entry point for the…
- [adapter.bun](./src/adapter/adapter.bun.md) — Bun-runtime entry point for the `./adapter` conditional export in `@suaveplan/db-sqlite`. When a bundler or the Bun run…
- [adapter](./src/adapter/adapter.md) — <!-- begin:auto-docs --> _(no submodules)_- adapter.wasm — Browser-runtime entry point for the…
- [adapter.wasm](./src/adapter/adapter.wasm.md) — Browser-runtime entry point for the `./adapter` conditional export in `@suaveplan/db-sqlite`. When a browser bundler re…
- [cache](./src/cache/cache.md) — <!-- begin:auto-docs --> _(no submodules)_- [cache](./src/cache/cache.md) — <!-- begin:auto-docs --> _(no submodules)_- [conformance](./src/conformance/conformance-suite.md) — <!-- begin:auto-docs --> _(no submodules)_- [conformance-suite](./src/conformance/conformance-suite.md) — <!-- begin:auto-docs --> _(no submodules)_- [connection](./src/connection/connection.md) — <!-- begin:auto-docs --> _(no submodules)_- [connection](./src/connection/connection.md) — <!-- begin:auto-docs --> _(no submodules)_- [bun-driver](./src/drivers/bun-driver.md) — Provides `createBunDriver`, the server-side SQLite driver for `@suaveplan/db-sqlite`. It dynamically imports `bun:sqlit…
- [wasm-driver](./src/drivers/wasm-driver.md) — Provides `createWasmDriver`, the browser-side SQLite driver for `@suaveplan/db-sqlite`. It spawns a Web Worker running…
- [wasm-worker](./src/drivers/wasm-worker.md) — Web Worker implementation for the WASM SQLite driver. This module runs inside a dedicated worker thread created by `cre…
- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_- [migration](./src/migration/migration.md) — <!-- begin:auto-docs --> _(no submodules)_- [migration](./src/migration/migration.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [unsafe](./src/unsafe/unsafe.md) — <!-- begin:auto-docs --> _(no submodules)_- [unsafe](./src/unsafe/unsafe.md) — <!-- begin:auto-docs --> _(no submodules)_- [adapter.bun](./src/adapter/adapter.bun.md) — Bun-runtime entry point for the `./adapter` conditional export in `@suaveplan/db-sqlite`. When a bundler or the Bun run…
- [adapter](./src/adapter/adapter.md) — <!-- begin:auto-docs --> _(no submodules)_- [adapter.wasm](./src/adapter/adapter.wasm.md) — Browser-runtime entry point for the `./adapter` conditional export in `@suaveplan/db-sqlite`. When a browser bundler re…
- [cache](./src/cache/cache.md) — <!-- begin:auto-docs --> _(no submodules)_ - [cache](./src/cache/cache.md) — <!-- begin:auto-docs --> _(no submodules)_- [conformance](./src/conformance/conformance-suite.md) — <!-- begin:auto-docs --> _(no submodules)_ - [conformance-suite](./src/conformance/conformance-suite.md) — <!-- begin:auto-docs --> _(no submodules)_- [connection](./src/connection/connection.md) — <!-- begin:auto-docs --> _(no submodules)_ - [connection](./src/connection/connection.md) — <!-- begin:auto-docs --> _(no submodules)_- [bun-driver](./src/drivers/bun-driver.md) — Provides `createBunDriver`, the server-side SQLite driver for `@suaveplan/db-sqlite`. It dynamically imports `bun:sqlit…
- [wasm-driver](./src/drivers/wasm-driver.md) — Provides `createWasmDriver`, the browser-side SQLite driver for `@suaveplan/db-sqlite`. It spawns a Web Worker running…
- [wasm-worker](./src/drivers/wasm-worker.md) — Web Worker implementation for the WASM SQLite driver. This module runs inside a dedicated worker thread created by `cre…
- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_ - [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_- [migration](./src/migration/migration.md) — <!-- begin:auto-docs --> _(no submodules)_ - [migration](./src/migration/migration.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_ - [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_ - [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [unsafe](./src/unsafe/unsafe.md) — <!-- begin:auto-docs --> _(no submodules)_ - [unsafe](./src/unsafe/unsafe.md) — <!-- begin:auto-docs --> _(no submodules)_
## Install
```bash
bun add @suaveplan/db-sqlite
```
## Quick start
```ts
import { createSqliteAdapter } from "@suaveplan/db-sqlite/adapter";
const db = await createSqliteAdapter({ url: ":memory:" });
await db.exec(`
CREATE TABLE users (
id TEXT PRIMARY KEY,
email TEXT NOT NULL
);
`);
await db.run("INSERT INTO users (id, email) VALUES (?, ?)", [
"user-123",
"alice@example.com",
]);
const rows = await db.all<{ id: string; email: string }>(
"SELECT id, email FROM users WHERE id = ?",
["user-123"],
);
await db.close();
```
## Modules
- [Adapter (universal)](./src/adapter/adapter.md) — `SqliteAdapter`, `createSqliteAdapter`, runtime selection
- [Adapter (Bun)](./src/adapter/adapter.bun.md) — `bun:sqlite` driver binding
- [Adapter (WASM)](./src/adapter/adapter.wasm.md) — browser/Worker `@sqlite.org/sqlite-wasm` binding
- [Cache](./src/cache/cache.md) — `PreparedStatementCache` (LRU)
- [Connection](./src/connection/connection.md) — connection helpers and singleton management
- [Bun driver](./src/drivers/bun-driver.md) — low-level Bun SQLite driver
- [WASM driver](./src/drivers/wasm-driver.md) — low-level WASM SQLite driver
- [WASM worker](./src/drivers/wasm-worker.md) — Web Worker host for WASM driver
- [Errors](./src/errors/errors.md) — `SqliteError`, `SQLITE_*` codes
- [Migration](./src/migration/migration.md) — `MigrationRunner`, forward-only migration ledger
- [OTel](./src/otel/otel.md) — span and metric instrumentation
- [Schemas](./src/schemas/schemas.md) — Zod-validated config types
- [Conformance](./src/conformance/conformance-suite.md) — shared `DatabaseAdapter` conformance suite
- [Unsafe](./src/unsafe/unsafe.md) — `./unsafe` subpath for raw queries

View File

@@ -0,0 +1,61 @@
# adapter.bun
## Purpose
Bun-runtime entry point for the `./adapter` conditional export in `@suaveplan/db-sqlite`. When a bundler or the Bun runtime resolves `import { SqliteAdapter } from "@suaveplan/db-sqlite/adapter"`, this file is selected via the `"bun"` condition in the package's `exports` field. It re-exports `SqliteAdapter` and the `SqliteAdapterOptions` type so consumers see a single, stable surface regardless of which environment condition matched.
The separation between adapter entry points prevents browser bundles from pulling in `bun:sqlite` references, and prevents Bun server bundles from pulling in WASM worker code. Each entry is a thin barrel that resolves at bundle time.
## Features
- Conditional export target for the `"bun"` condition in `package.json`
- Re-exports `SqliteAdapter` from `./adapter.js`
- Re-exports `SqliteAdapterOptions` type from `../schemas/index.js`
- Zero runtime overhead — pure re-export barrel
## Usage: Basic Example
```ts
// Resolved automatically by Bun via conditional exports
import { SqliteAdapter } from "@suaveplan/db-sqlite/adapter";
const db = new SqliteAdapter();
await db.connect();
const rows = await db.query("SELECT 1 AS n");
await db.disconnect();
```
## Usage: Advanced Example
```ts
import { SqliteAdapter } from "@suaveplan/db-sqlite/adapter";
import type { SqliteAdapterOptions } from "@suaveplan/db-sqlite/adapter";
const opts: SqliteAdapterOptions = {
filename: "/var/data/app.db",
readonly: false,
};
const adapter = new SqliteAdapter(opts);
await adapter.connect();
await adapter.migrate({ migrationsFolder: "./migrations" });
await adapter.transaction(async (tx) => {
await tx.execute("INSERT INTO users (name) VALUES (?)", ["Alice"]);
});
await adapter.disconnect();
```
## API Reference
This file does not export any symbols directly — it is a conditional-export barrel. All types and values are documented in [`adapter.md`](./adapter.md) (`SqliteAdapter`) and [`../schemas/schemas.md`](../schemas/schemas.md) (`SqliteAdapterOptions`).
**Conditional export condition:** `"bun"` — selected when the consumer runs under the Bun runtime or when a bundler sets the `bun` condition (e.g. via `resolve.conditions: ["bun"]` in Vite).
## Implementation Notes
The file intentionally contains no logic. Any change to the Bun-specific adapter behaviour lives in `adapter.ts`, not here. This design keeps the conditional export system orthogonal to implementation: adding a new runtime condition (e.g. `"deno"`) only requires adding a new entry-point barrel and a new `exports` condition, with no changes to the core adapter code.
Vite excludes this file from the default ESM browser bundle because the `"browser"` condition resolves to `adapter.wasm.ts` instead.

View File

@@ -0,0 +1,113 @@
# adapter
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs --> - adapter.wasm — Browser-runtime entry point for the……
- [adapter.wasm](./adapter.wasm.md) — Browser-runtime entry point for the `./adapter` conditional export in `@suaveplan/db-sqlite`. When a browser bundler re…
- adapter.wasm — Browser-runtime entry point for the……
- [adapter.wasm](./adapter.wasm.md) — Browser-runtime entry point for the `./adapter` conditional export in `@suaveplan/db-sqlite`. When a browser bundler re…
- adapter.wasm — Browser-runtime entry point for the……
- [adapter.wasm](./adapter.wasm.md) — Browser-runtime entry point for the `./adapter` conditional export in `@suaveplan/db-sqlite`. When a browser bundler re…
- adapter.wasm — Browser-runtime entry point for the…
- [adapter.wasm](./adapter.wasm.md) — Browser-runtime entry point for the `./adapter` conditional export in `@suaveplan/db-sqlite`. When a browser bundler re…
- [adapter.wasm](./adapter.wasm.md) — Browser-runtime entry point for the `./adapter` conditional export in `@suaveplan/db-sqlite`. When a browser bundler re…
## Purpose
`SqliteAdapter` is the main `DatabaseAdapter` implementation for `@suaveplan/db-sqlite`. It implements the full `DatabaseAdapter` contract from `@suaveplan/db`, backing it with a `bun:sqlite` database instance, an LRU `PreparedStatementCache`, a `MigrationRunner`, and OTel span instrumentation on every public operation.
The adapter is designed for the Bun server runtime only. Attempting to construct it outside Bun throws `SqliteError(SQLITE.ENVIRONMENT_ERROR)` immediately. Browser callers should use `createWasmDriver` directly via the `./adapter` conditional export, which resolves to `adapter.wasm.ts` in browser bundles.
## Features
- Implements `DatabaseAdapter` from `@suaveplan/db` — fully substitutable wherever `DatabaseLike` is expected
- Idempotent `connect()` — concurrent callers share one in-flight connection promise
- `PreparedStatementCache` (LRU, default 128 entries) reused across `query()` calls
- `transaction()` uses `BEGIN IMMEDIATE` to prevent deadlocks on concurrent writes; auto-rollback on failure
- `migrate()` delegates to `MigrationRunner` and invalidates the cache on success
- `AbortSignal` respected on every public method — throws `SqliteError(SQLITE.*)` when aborted
- All operations wrapped in `withQuerySpan` for OTel tracing and latency metrics
- `sideEffects: false` — safe for tree-shaking
## Usage: Basic Example
```ts
import { SqliteAdapter } from "@suaveplan/db-sqlite";
const adapter = new SqliteAdapter({ filename: ":memory:" });
await adapter.connect();
const rows = await adapter.query<{ id: number }>(
"SELECT id FROM users WHERE active = ?",
[1]
);
await adapter.disconnect();
```
## Usage: Advanced Example
```ts
import { SqliteAdapter } from "@suaveplan/db-sqlite";
const adapter = new SqliteAdapter({ filename: "/data/app.db" });
await adapter.connect();
// Transactional insert
await adapter.transaction(async (tx) => {
await tx.execute("INSERT INTO accounts VALUES (?, ?)", [1, "Alice"]);
await tx.execute("INSERT INTO accounts VALUES (?, ?)", [2, "Bob"]);
});
// Run migrations
const result = await adapter.migrate({
migrationsFolder: "./db/migrations",
});
console.log(`Applied ${result.applied} migration(s)`);
await adapter.disconnect();
```
## API Reference
### `new SqliteAdapter(options?, schema?)`
Validates options, throws `SqliteError(SQLITE.ENVIRONMENT_ERROR)` outside Bun.
### `connect(config?, opts?): Promise<void>`
Opens the SQLite file or `:memory:` database. Idempotent under concurrent calls — only one connection is opened.
### `disconnect(opts?): Promise<void>`
Closes the database, invalidates the prepared-statement cache, and resets connection state.
### `query<T>(sql, params?, opts?): Promise<T[]>`
Executes a `SELECT` query. Uses the prepared-statement cache for repeated SQL strings.
### `execute(sql, params?, opts?): Promise<{ rowsAffected: number }>`
Executes a `INSERT`/`UPDATE`/`DELETE` statement. Returns affected row count.
### `transaction<T>(fn, opts?): Promise<T>`
Runs `fn` inside a `BEGIN IMMEDIATE` transaction. Commits on success, rolls back on failure.
### `migrate(config?, opts?): Promise<MigrationResult>`
Delegates to `MigrationRunner` and invalidates the prepared-statement cache on success.
### `isConnected: boolean`
`true` after `connect()` and before `disconnect()`.
## Implementation Notes
The `_connectingPromise` field serialises concurrent `connect()` calls — the second caller awaits the same in-flight promise rather than opening a second connection. This is safe because `bun:sqlite` uses synchronous I/O under the covers and does not support concurrent connections to the same file.
`transaction()` casts `this` to `DatabaseLike` when passing it to `fn`. This is sound because `SqliteAdapter` implements all `DatabaseLike` methods. The cast avoids exposing internal `_sqlite` state to transaction callbacks while keeping the type constraint tight.

View File

@@ -0,0 +1,64 @@
# adapter.wasm
## Purpose
Browser-runtime entry point for the `./adapter` conditional export in `@suaveplan/db-sqlite`. When a browser bundler resolves `import { createWasmDriver } from "@suaveplan/db-sqlite/adapter"`, this file is selected via the `"browser"` condition in the package's `exports` field. It re-exports the WASM-backed driver factory and its related types, giving browser consumers a clean surface that never references `bun:sqlite` or any Node.js-only API.
The separation between Bun and WASM adapter entry points keeps tree-shaking effective: a Bun server bundle never pulls in `@sqlite.org/sqlite-wasm` or the Web Worker, and a browser bundle never pulls in `bun:sqlite`. Each entry point is resolved at build time by the bundler's condition matching.
## Features
- Conditional export target for the `"browser"` condition in `package.json`
- Re-exports `createWasmDriver`, `WasmDriverResult`, and `WasmProxyDb` from the WASM driver
- Re-exports `WasmDriverOptions` type from the shared schemas
- Zero runtime overhead — pure re-export barrel
## Usage: Basic Example
```ts
// Resolved automatically by the browser bundler via conditional exports
import { createWasmDriver } from "@suaveplan/db-sqlite/adapter";
const { db, close } = await createWasmDriver();
const rows = await db.query("SELECT 1 AS n");
await close();
```
## Usage: Advanced Example
```ts
import { createWasmDriver } from "@suaveplan/db-sqlite/adapter";
import type { WasmDriverOptions } from "@suaveplan/db-sqlite/adapter";
const opts: WasmDriverOptions = {
filename: "app.db", // OPFS-backed persistent storage
};
const { db, close } = await createWasmDriver(opts);
await db.execute(
"CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)"
);
await db.execute("INSERT INTO notes (body) VALUES (?)", ["Hello from the browser"]);
const rows = await db.query<{ id: number; body: string }>(
"SELECT * FROM notes ORDER BY id"
);
console.log(rows);
await close();
```
## API Reference
This file does not export any symbols directly — it is a conditional-export barrel. All types and values are documented in [`../drivers/wasm-driver.md`](../drivers/wasm-driver.md).
**Conditional export condition:** `"browser"` — selected when a bundler uses browser-targeting conditions (e.g. Vite's default, Webpack's `target: "web"`).
**Default fallback:** When neither `"bun"` nor `"browser"` conditions are active (e.g. vanilla Node.js `require()`), the `exports.default` condition falls back to the Bun adapter entry point. The WASM path is never the fallback to avoid loading 1.5 MB of WASM in server-side contexts.
## Implementation Notes
`WasmDriverOptions` is imported from `../schemas/index.js` rather than from `wasm-driver.js` because the Zod schemas live in the shared schemas module — keeping the driver files free of Zod as a direct import. This pattern is consistent across all driver options types in this package.
The WASM worker bundle (`wasm-worker.js`) is inlined as an asset URL by Vite at build time using `new URL("./wasm-worker.js", import.meta.url)`. The worker receives messages for `init`, `query`, `execute`, and `close`, and responds synchronously from SQLite's in-memory or OPFS-backed database.

View File

@@ -0,0 +1,88 @@
# cache
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`PreparedStatementCache` is an LRU-bounded cache for `bun:sqlite` prepared statement objects, keyed by SQL text. Reusing prepared statements avoids the overhead of repeated SQL parsing and optimisation — a significant win for hot query paths. The cache is bounded to prevent unbounded memory growth in applications that generate many distinct SQL strings.
The cache integrates with OpenTelemetry to surface hit/miss/eviction counters at `sqlite.cache.hits`, `sqlite.cache.misses`, and `sqlite.cache.evictions`. These are no-ops when no OTel provider is registered, keeping the module safe for use in zero-dependency environments.
## Features
- LRU eviction when `size >= maxSize` (default `128`)
- `get(sql)` — returns cached statement or `undefined`; updates LRU order
- `set(sql, stmt)` — inserts or updates; evicts LRU entry when at capacity, calling `stmt.finalize()` on the evicted statement
- `invalidate()` — clears all entries and calls `finalize()` on each held statement; called by `SqliteAdapter.migrate()` to prevent stale plan reuse after schema changes
- `size` / `maxSize` read-only getters
- OTel counters: `sqlite.cache.hits`, `sqlite.cache.misses`, `sqlite.cache.evictions`
- `static resetInstruments()` — clears the OTel lazy singleton for test isolation
## Usage: Basic Example
```ts
import { PreparedStatementCache } from "@suaveplan/db-sqlite/cache/cache";
const cache = new PreparedStatementCache();
const stmt = sqlite.prepare("SELECT * FROM users WHERE id = ?");
cache.set("SELECT * FROM users WHERE id = ?", stmt);
const hit = cache.get("SELECT * FROM users WHERE id = ?");
// hit === stmt
```
## Usage: Advanced Example
```ts
import { PreparedStatementCache } from "@suaveplan/db-sqlite/cache/cache";
// Small cache for testing eviction
const cache = new PreparedStatementCache(3);
for (let i = 0; i < 4; i++) {
const sql = `SELECT ${i} FROM t`;
cache.set(sql, { finalize: () => undefined }); // mock statement
}
console.log(cache.size); // 3 — first entry evicted
cache.invalidate();
console.log(cache.size); // 0 — all entries cleared
```
## API Reference
### `new PreparedStatementCache(maxSize?: number)`
Constructs a new cache. `maxSize` defaults to `128`.
### `get(sql: string): Statement | undefined`
Returns the cached statement for `sql`, or `undefined` on miss. Increments `sqlite.cache.hits` or `sqlite.cache.misses`. Updates LRU order on hit.
### `set(sql: string, stmt: Statement): void`
Inserts or updates the cache entry for `sql`. On capacity overflow, evicts the least-recently-used entry, calling `stmt.finalize()` on it, and increments `sqlite.cache.evictions`.
### `invalidate(): void`
Clears all entries. Calls `stmt.finalize()` on each held statement before clearing. Called automatically by `SqliteAdapter.migrate()`.
### `size: number`
Current number of cached entries.
### `maxSize: number`
Maximum capacity as set at construction.
### `static resetInstruments(): void`
Clears the lazy OTel instruments singleton. Use in tests to ensure each test suite starts with a fresh counter baseline.
## Implementation Notes
LRU ordering is maintained with a parallel `string[]` array (`#lruOrder`) rather than a doubly-linked list. This is simpler and sufficient for cache sizes up to a few hundred entries — `Array.indexOf` and `splice` on an array of at most 128 strings add negligible overhead compared to a SQLite query round-trip. For much larger caches a linked-list implementation would be preferable.
`finalize()` is called defensively with a try/catch because `bun:sqlite` statements may already be finalised if the underlying database was closed before the cache was cleared.

View File

@@ -0,0 +1,64 @@
# conformance-suite
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides `runConformanceSuite`, a driver-agnostic test factory that accepts a `DriverFactory` callback and executes a standard suite of CRUD, transaction, and rollback assertions against any SQLite driver. Running the same suite against both the Bun driver and a WASM driver ensures that both runtimes satisfy the same behavioural contract, catching driver-specific regressions before they reach production.
The suite is intentionally minimal — it tests the lowest-level SQL operations that the adapter layer depends on. Higher-level tests (prepared-statement cache behaviour, migration runner idempotency, OTel counter increments) live in their respective unit test files.
## Features
- `runConformanceSuite(name, factory)` — registers a Vitest `describe` block named `${name} conformance`
- Tests: table creation, SELECT, INSERT with parameters, transaction commit, transaction rollback, UPDATE, DELETE
- `DriverFactory` interface — a single `create()` method returning `{ sqlite, close }`
- Runs under the Bun driver via `bun-conformance.test.ts`
## Usage: Basic Example
```ts
import { runConformanceSuite } from "@suaveplan/db-sqlite/src/conformance/conformance-suite";
import { createBunDriver } from "@suaveplan/db-sqlite/drivers/bun-driver";
runConformanceSuite("Bun driver", {
async create() {
return createBunDriver({ filename: ":memory:" });
},
});
```
## Usage: Advanced Example
```ts
import { runConformanceSuite } from "@suaveplan/db-sqlite/src/conformance/conformance-suite";
// Custom driver shim for testing
runConformanceSuite("Custom driver", {
async create() {
const driver = await MyCustomDriver.open(":memory:");
return { sqlite: driver.raw, close: async () => driver.close() };
},
});
```
## API Reference
### `DriverFactory`
```ts
interface DriverFactory {
create(): Promise<{ sqlite: any; close: () => Promise<void> }>;
}
```
### `runConformanceSuite(name: string, factory: DriverFactory): void`
Registers a Vitest `describe` block with 5 test cases covering the core SQLite contract.
## Implementation Notes
The conformance suite uses raw `sqlite.run()` / `sqlite.query().all()` calls rather than going through `SqliteAdapter` or Drizzle. This keeps the suite focused on driver-level semantics rather than adapter-layer concerns, making it useful for validating new driver implementations independently of the adapter code.
Each test creates fresh tables with unique names to avoid interference between tests within the same suite run.

View File

@@ -0,0 +1,83 @@
# connection
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides thin singleton-management wrappers for `SqliteAdapter`, built on top of the `@suaveplan/db` singleton helpers. These functions let callers share a single connected `SqliteAdapter` instance across an application without manually tracking the singleton lifecycle or passing the adapter through every layer of the call stack.
The helpers default the singleton name to `"default"` so simple applications need only call `getDefaultSqlite()` with no arguments. More complex applications with multiple databases use explicit names.
## Features
- `getDefaultSqlite(options?, config?, name?)` — creates-or-returns a connected `SqliteAdapter` singleton
- `closeSqlite(name?)` — disconnects and removes the named singleton
- `closeAllSqlite()` — disconnects and removes all `SqliteAdapter` singletons
- `hasSqlite(name?)` — returns `true` if the named singleton exists and is connected
- Default name: `"default"` — single-database apps never need to supply a name
## Usage: Basic Example
```ts
import { getDefaultSqlite } from "@suaveplan/db-sqlite/connection/connection";
const adapter = await getDefaultSqlite();
const rows = await adapter.query("SELECT * FROM users");
```
## Usage: Advanced Example
```ts
import {
closeAllSqlite,
closeSqlite,
getDefaultSqlite,
hasSqlite,
} from "@suaveplan/db-sqlite/connection/connection";
// Primary database
const primary = await getDefaultSqlite(
{ filename: "/data/app.db" },
undefined,
"primary"
);
// Analytics read-only shard
const analytics = await getDefaultSqlite(
{ filename: "/data/analytics.db", readonly: true },
undefined,
"analytics"
);
console.log(hasSqlite("primary")); // true
console.log(hasSqlite("analytics")); // true
await closeSqlite("analytics");
console.log(hasSqlite("analytics")); // false
// Shutdown — closes all remaining singletons
await closeAllSqlite();
```
## API Reference
### `getDefaultSqlite(options?, config?, name?): Promise<SqliteAdapter>`
Creates a new `SqliteAdapter` using the given `options`, calls `connect(config)` on it, caches it under `name`, and returns it. Subsequent calls with the same `name` return the cached instance without reconnecting.
### `closeSqlite(name?): Promise<void>`
Disconnects and removes the singleton named `name`. No-op if no singleton by that name exists.
### `closeAllSqlite(): Promise<void>`
Disconnects and removes all singletons. Throws `AggregateError` if any disconnect fails.
### `hasSqlite(name?): boolean`
Returns `true` if a singleton exists under `name`.
## Implementation Notes
These functions delegate entirely to `@suaveplan/db`'s `getSingleton`, `closeSingleton`, `closeAllSingletons`, and `hasSingleton`. The `SqliteAdapter`-specific names (`getDefaultSqlite`, `closeSqlite`, etc.) exist for discoverability — callers working with SQLite need not know the generic singleton API. The return type is narrowed from `DatabaseAdapter` to `SqliteAdapter` via a cast so callers have access to SQLite-specific properties.

View File

@@ -0,0 +1,81 @@
# bun-driver
## Purpose
Provides `createBunDriver`, the server-side SQLite driver for `@suaveplan/db-sqlite`. It dynamically imports `bun:sqlite` (a built-in Bun module) and returns a thin wrapper holding the raw `Database` instance plus an idempotent `close()` function. The `isBunEnvironment()` guard ensures the driver is never accidentally called in a browser context — doing so raises `SQLITE.ENVIRONMENT_ERROR` immediately rather than surfacing a confusing module-not-found error at import time.
The dynamic `import("bun:sqlite")` pattern keeps the module evaluatable in non-Bun environments (so bundlers can tree-shake it out and browser builds do not fail), while still providing full type information from `@types/bun`.
## Features
- `createBunDriver(options?)` — opens an in-memory (default) or file-backed SQLite database via `bun:sqlite`
- `isBunEnvironment()` — detects Bun runtime via `process.versions.bun`
- `BunDriverResult` — `{ sqlite, close }` shape; `close()` is idempotent (safe to call multiple times)
- Throws `SqliteError(SQLITE.ENVIRONMENT_ERROR)` when called outside Bun
- Throws `SqliteError(SQLITE.DRIVER_INIT_FAILED)` if `Database` constructor throws (e.g. bad path, locked file)
- `readonly` option forwarded to `bun:sqlite` `Database` constructor
## Usage: Basic Example
```ts
import { createBunDriver } from "@suaveplan/db-sqlite/drivers/bun-driver";
const { sqlite, close } = await createBunDriver();
// sqlite is a bun:sqlite Database instance — run raw SQL or pass to Drizzle
await close();
```
## Usage: Advanced Example
```ts
import { createBunDriver, isBunEnvironment } from "@suaveplan/db-sqlite/drivers/bun-driver";
if (!isBunEnvironment()) {
throw new Error("Server-side only path");
}
const { sqlite, close } = await createBunDriver({
filename: "/data/app.db",
readonly: false,
});
try {
sqlite.run("CREATE TABLE IF NOT EXISTS kv (key TEXT PRIMARY KEY, value TEXT)");
sqlite.run("INSERT OR REPLACE INTO kv VALUES (?, ?)", ["hello", "world"]);
} finally {
await close(); // second call is a no-op
await close(); // safe
}
```
## API Reference
### `isBunEnvironment(): boolean`
Returns `true` when `process.versions.bun` is defined. Safe to call in any environment.
### `createBunDriver(options?: BunDriverOptions): Promise<BunDriverResult>`
Opens a SQLite database. Throws `SqliteError(SQLITE.ENVIRONMENT_ERROR)` outside Bun, `SqliteError(SQLITE.DRIVER_INIT_FAILED)` on open failure.
**Parameters**
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `options.filename` | `string` | `":memory:"` | File path or `":memory:"` |
| `options.readonly` | `boolean` | `false` | Open in read-only mode |
### `BunDriverResult`
```ts
interface BunDriverResult {
sqlite: Database; // bun:sqlite Database instance
close: () => Promise<void>; // idempotent — safe to call multiple times
}
```
## Implementation Notes
`close()` sets a `closed` flag so repeated calls are no-ops rather than throwing "database already closed". This is intentional: callers cleaning up in `finally` blocks should not have to track whether they already closed.
The `bun:sqlite` import is deferred via `await import("bun:sqlite")` inside the function body so that bundlers processing non-Bun targets (e.g. a Vite browser build) can externalize or ignore the module without causing a build error at parse time. The `isBunEnvironment()` guard before the import provides an early exit in the rare case this driver is invoked in a test environment that stubs the process object.

View File

@@ -0,0 +1,89 @@
# wasm-driver
## Purpose
Provides `createWasmDriver`, the browser-side SQLite driver for `@suaveplan/db-sqlite`. It spawns a Web Worker running `wasm-worker.ts`, which loads `@sqlite.org/sqlite-wasm` and manages a SQLite database instance. Communication between the caller and the worker uses a message-passing protocol with sequential IDs mapped to `Promise` resolve/reject pairs, giving a fully async interface without blocking the main thread.
OPFS (Origin Private File System) persistence is attempted automatically when a non-`:memory:` filename is supplied and the worker context supports `sqlite3.oo1.OpfsDb`. When OPFS headers (`Cross-Origin-Opener-Policy: same-origin`, `Cross-Origin-Embedder-Policy: require-corp`) are absent, the worker falls back to in-memory storage with a console warning.
## Features
- `createWasmDriver(options?, workerFactory?)` — spawns worker, awaits ready handshake, returns `{ db, close }`
- Injectable `workerFactory` parameter for test isolation — avoids real Worker spawning in Vitest
- `WasmProxyDb` — `{ query(sql, params?), execute(sql, params?) }` interface returned by the driver
- `close()` is idempotent and drains all pending requests with rejection before terminating the worker
- Worker init failure terminates the worker and throws `SqliteError(SQLITE.DRIVER_INIT_FAILED)`
- Worker runtime errors reject all pending promises with `SqliteError(SQLITE.DRIVER_INIT_FAILED)`
## Usage: Basic Example
```ts
import { createWasmDriver } from "@suaveplan/db-sqlite/drivers/wasm-driver";
const { db, close } = await createWasmDriver();
await db.execute("CREATE TABLE kv (key TEXT PRIMARY KEY, value TEXT)", []);
await db.execute("INSERT INTO kv VALUES (?, ?)", ["hello", "world"]);
const rows = await db.query("SELECT * FROM kv WHERE key = ?", ["hello"]);
await close();
```
## Usage: Advanced Example
```ts
import { createWasmDriver } from "@suaveplan/db-sqlite/drivers/wasm-driver";
// With OPFS persistence (requires COOP/COEP headers on the page)
const { db, close } = await createWasmDriver({ filename: "app.db" });
try {
await db.execute("BEGIN IMMEDIATE", []);
await db.execute("INSERT INTO users VALUES (?, ?)", [1, "Alice"]);
await db.execute("COMMIT", []);
} catch (err) {
await db.execute("ROLLBACK", []);
throw err;
} finally {
await close();
}
```
## API Reference
### `createWasmDriver<TSchema>(options?, workerFactory?): Promise<WasmDriverResult<TSchema>>`
Spawns the WASM worker and awaits the `ready` handshake before resolving.
**Parameters**
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `options.filename` | `string` | `undefined` (→ `:memory:`) | OPFS file path or omit for in-memory |
| `workerFactory` | `() => Worker` | `defaultWorkerFactory` | Injectable for testing |
**Throws** `SqliteError(SQLITE.DRIVER_INIT_FAILED)` if the worker fails to initialise.
### `WasmProxyDb<TSchema>`
```ts
interface WasmProxyDb<TSchema> {
query(sql: string, params?: unknown[]): Promise<unknown[][]>;
execute(sql: string, params?: unknown[]): Promise<{ rowsAffected: number }>;
}
```
### `WasmDriverResult<TSchema>`
```ts
interface WasmDriverResult<TSchema> {
db: WasmProxyDb<TSchema>;
close: () => Promise<void>;
}
```
## Implementation Notes
The worker URL is constructed with `new URL("./wasm-worker.js", import.meta.url)` so bundlers (Vite, webpack) can resolve it as a worker entry point and emit the correct asset path. The `.js` extension (not `.ts`) is used because the compiled output is what the browser loads at runtime.
The pending-promise map (`Map<id, {resolve, reject}>`) ensures that each in-flight request receives exactly one response, and that a stale response for a cancelled request is silently ignored. On `close()`, all pending promises are rejected before worker termination to prevent callers from hanging indefinitely.
OPFS support requires `Cross-Origin-Opener-Policy: same-origin` and `Cross-Origin-Embedder-Policy: require-corp` response headers. The worker automatically falls back to in-memory storage without these headers rather than crashing, since detecting header presence from inside a Worker is unreliable.

View File

@@ -0,0 +1,81 @@
# wasm-worker
## Purpose
Web Worker implementation for the WASM SQLite driver. This module runs inside a dedicated worker thread created by `createWasmDriver` and handles all direct SQLite operations using `@sqlite.org/sqlite-wasm`. It communicates with the main thread via a simple message-passing protocol, keeping the WASM heap and SQLite connection completely isolated from the main thread's event loop.
Running SQLite operations in a worker prevents blocking the UI thread during expensive queries and migrations, and satisfies browser restrictions around synchronous WASM I/O on the main thread (required for OPFS-backed databases via `sqlite3_vfs`).
## Features
- Loads `@sqlite.org/sqlite-wasm` lazily on `init` message
- Supports `init`, `query`, `execute`, `begin_tx`, `commit_tx`, `rollback_tx`, and `close` message types
- Sequential request IDs for reliable request-response correlation
- Returns structured `{ id, ok, result }` or `{ id, ok: false, error }` responses
- Opens either an in-memory database (default) or a named database when `filename` is provided
## Usage: Basic Example
This module is not imported directly. It is instantiated by `createWasmDriver` as a Web Worker:
```ts
// Worker is created internally by the driver
import { createWasmDriver } from "@suaveplan/db-sqlite/adapter";
const { db, close } = await createWasmDriver();
await db.execute("CREATE TABLE t (id INTEGER PRIMARY KEY)");
const rows = await db.query("SELECT * FROM t");
await close();
```
## Usage: Advanced Example
For testing purposes, the worker's message protocol can be exercised directly by creating a mock worker that implements the same interface. The `createWasmDriver` function accepts an injectable `workerFactory` parameter for this:
```ts
import { createWasmDriver } from "@suaveplan/db-sqlite";
function mockWorkerFactory(): Worker {
let onmessage: ((ev: MessageEvent) => void) | null = null;
const post = (data: unknown) => { if (onmessage) onmessage({ data } as MessageEvent); };
return {
set onmessage(fn) { onmessage = fn; },
postMessage(msg: unknown) {
const m = msg as { type: string; id: number };
if (m.type === "init") post({ id: m.id, ok: true, result: { type: "ready" } });
if (m.type === "query") post({ id: m.id, ok: true, result: [] });
},
terminate() {},
} as unknown as Worker;
}
const { db, close } = await createWasmDriver(undefined, mockWorkerFactory);
```
## API Reference
### Message Protocol (inbound → worker)
| `type` | Extra fields | Description |
|---------------|----------------------------|-----------------------------------------|
| `init` | `filename?: string` | Load WASM, open database, reply ready |
| `query` | `sql: string`, `params?` | Run `all()`, reply with rows array |
| `execute` | `sql: string`, `params?` | Run `run()`, reply with `rowsAffected` |
| `begin_tx` | — | `BEGIN IMMEDIATE` |
| `commit_tx` | — | `COMMIT` |
| `rollback_tx` | — | `ROLLBACK` |
| `close` | — | Close database, terminate worker |
### Message Protocol (outbound ← worker)
All responses carry `{ id: number, ok: boolean }`.
- Success: `{ id, ok: true, result: <payload> }`
- Failure: `{ id, ok: false, error: "<message>" }`
## Implementation Notes
The `/// <reference lib="webworker" />` triple-slash directive at the top of the file provides TypeScript access to `self`, `MessageEvent`, `postMessage`, and other Web Worker globals without requiring the file to be compiled with `"lib": ["WebWorker"]` globally — that would pollute the non-worker files.
The module uses the `@sqlite.org/sqlite-wasm` default export which is a zero-argument async factory: `const sqlite3 = await initSqlite()`. Previous versions of this package accepted a configuration argument; the current API does not.

View File

@@ -0,0 +1,93 @@
# errors
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines the structured error hierarchy for `@suaveplan/db-sqlite`. All runtime failures — connection problems, query errors, migration failures, environment mismatches — are reported as `SqliteError` instances carrying a stable `SQLITE.*` code from the `SQLITE_CODES` registry. This makes error handling programmatic and refactor-safe: callers switch on `err.code` rather than matching message strings.
The companion `mapSqliteCode` helper normalises raw error values thrown by `bun:sqlite` (which surfaces result-code strings like `SQLITE_BUSY` or `SQLITE_CONSTRAINT`) into the same stable code namespace, so adapters never leak driver-specific strings into calling code.
## Features
- `SQLITE_CODES` registry — 10 stable `SQLITE.*` code strings defined via `defineCodes`
- `SqliteError extends SuaveplanError<SqliteCode>` — carries `code`, `message`, `cause`, and full `SuaveplanError` wire-format serialisation
- `mapSqliteCode(err)` — maps raw `bun:sqlite` result codes and message substrings to stable `SqliteCode` values; falls back to `SQLITE.QUERY_FAILED`
- Type alias `SqliteCode` — union of all valid code strings
## Usage: Basic Example
```ts
import { SqliteError, SQLITE_CODES } from "@suaveplan/db-sqlite/errors/errors";
throw new SqliteError(
SQLITE_CODES.NOT_CONNECTED,
"Cannot query: adapter is not connected"
);
```
## Usage: Advanced Example
```ts
import { mapSqliteCode, SqliteError, SQLITE_CODES } from "@suaveplan/db-sqlite/errors/errors";
async function safeQuery(db: unknown, sql: string): Promise<unknown[]> {
try {
// ... execute query
return [];
} catch (raw) {
const code = mapSqliteCode(raw);
if (code === SQLITE_CODES.BUSY) {
// caller can retry on BUSY
throw new SqliteError(code, "Database is busy — retry after a moment", { cause: raw instanceof Error ? raw : undefined });
}
throw new SqliteError(code, `Query failed: ${raw instanceof Error ? raw.message : String(raw)}`, {
cause: raw instanceof Error ? raw : undefined,
});
}
}
```
## API Reference
### `SQLITE_CODES`
Registry object produced by `defineCodes("SQLITE", [...])`. Keys and values:
| Key | Value |
|-----|-------|
| `NOT_CONNECTED` | `"SQLITE.NOT_CONNECTED"` |
| `QUERY_FAILED` | `"SQLITE.QUERY_FAILED"` |
| `EXECUTE_FAILED` | `"SQLITE.EXECUTE_FAILED"` |
| `TRANSACTION_FAILED` | `"SQLITE.TRANSACTION_FAILED"` |
| `MIGRATION_FAILED` | `"SQLITE.MIGRATION_FAILED"` |
| `ENVIRONMENT_ERROR` | `"SQLITE.ENVIRONMENT_ERROR"` |
| `BUSY` | `"SQLITE.BUSY"` |
| `CONSTRAINT` | `"SQLITE.CONSTRAINT"` |
| `CORRUPT` | `"SQLITE.CORRUPT"` |
| `DRIVER_INIT_FAILED` | `"SQLITE.DRIVER_INIT_FAILED"` |
### `SqliteError`
```ts
class SqliteError extends SuaveplanError<SqliteCode> {
constructor(code: SqliteCode, message: string, options?: SuaveplanErrorOptions): SqliteError
}
```
Inherits `.code`, `.message`, `.cause`, `.name` from `SuaveplanError`. Fully serialisable to the `@suaveplan/error` wire format.
### `mapSqliteCode(err: unknown): SqliteCode`
Maps a raw thrown value from `bun:sqlite` to a stable `SqliteCode`. Checks `err.message` for known result-code strings (`SQLITE_BUSY`, `SQLITE_LOCKED`, `SQLITE_CONSTRAINT`, `SQLITE_CORRUPT`, `SQLITE_NOTADB`). Returns `SQLITE.QUERY_FAILED` as the safe default when no pattern matches.
### `SqliteCode`
Type alias: `(typeof SQLITE_CODES)[keyof typeof SQLITE_CODES]` — the union of all ten code strings.
## Implementation Notes
The `noUselessConstructor` biome rule is suppressed on `SqliteError` because `SuaveplanError` uses a `protected` constructor; the subclass constructor exists solely to expose a `public` entry point — without it TypeScript raises TS2674.
`mapSqliteCode` checks `err instanceof SqliteError` first so re-thrown errors are not re-wrapped. The result-code detection loop converts both the error message and the raw value to upper-case before matching, handling both `"SQLITE_BUSY: database is locked"` messages and bare result-code strings.

View File

@@ -0,0 +1,84 @@
# migration
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`MigrationRunner` applies forward-only SQL migration files to a SQLite database. It maintains a ledger table (`__sqlite_migrations` by default) that records which migration files have been applied, making runs idempotent — re-running the same set of migration files produces the same database state regardless of how many times it is called.
Each migration is executed inside a `BEGIN IMMEDIATE` transaction. A failure mid-migration triggers an automatic `ROLLBACK`, ensuring the database is never left in a partially-applied state. A `dryRun()` mode reads and validates files but makes no database changes, letting callers preview what would be applied.
## Features
- Forward-only, filename-ordered migration application (`.sql` files sorted lexicographically)
- Idempotent: skips already-applied migrations via ledger table lookup
- Transactional: each migration runs in `BEGIN IMMEDIATE` / `COMMIT` / `ROLLBACK`
- Dry-run mode: previews pending migrations without modifying the database
- Injectable `Clock` for deterministic `applied_at` timestamps in tests
- Configurable `migrationsFolder`, `migrationsTable`, and `dryRun` via `MigrationRunnerOptionsSchema`
- Throws `SqliteError(SQLITE.MIGRATION_FAILED)` with original cause on any failure
## Usage: Basic Example
```ts
import { createBunDriver } from "@suaveplan/db-sqlite/drivers/bun-driver";
import { MigrationRunner } from "@suaveplan/db-sqlite/migration/migration";
const { sqlite, close } = await createBunDriver({ filename: "app.db" });
try {
const runner = new MigrationRunner(sqlite, { migrationsFolder: "./db/migrations" });
const result = await runner.run();
console.log(`Applied ${result.applied} migration(s):`, result.migrations);
} finally {
await close();
}
```
## Usage: Advanced Example
```ts
import { MigrationRunner } from "@suaveplan/db-sqlite/migration/migration";
// Dry-run to preview without applying
const runner = new MigrationRunner(sqlite, {
migrationsFolder: "./db/migrations",
migrationsTable: "custom_ledger",
});
const preview = await runner.dryRun();
console.log("Would apply:", preview.migrations);
if (preview.applied > 0) {
const result = await runner.run();
console.log("Applied:", result.migrations);
}
```
## API Reference
### `new MigrationRunner(sqlite, options?, clock?)`
Constructs a `MigrationRunner`. Validates `options` via `MigrationRunnerOptionsSchema`.
| Parameter | Type | Description |
|-----------|------|-------------|
| `sqlite` | `bun:sqlite Database` | Database instance to migrate |
| `options` | `Partial<MigrationRunnerOptions>` | Folder, table, dry-run settings |
| `clock` | `Clock` | Injectable clock for `applied_at` (default: `Date.now`) |
### `run(options?: Partial<MigrationRunnerOptions>): Promise<MigrationResult>`
Reads `.sql` files from `migrationsFolder`, skips already-applied entries, applies each in order, records to ledger. Returns `{ applied: number, migrations: string[] }`.
### `dryRun(options?: Partial<MigrationRunnerOptions>): Promise<MigrationResult>`
Same as `run()` but makes no database changes. Useful for change-preview UIs and CI checks.
## Implementation Notes
Migrations are sorted lexicographically by filename. Conventional naming (`001_init.sql`, `002_add_users.sql`) produces chronological order. Non-`.sql` files in the folder are silently ignored.
The `#isMigrationApplied` check uses a raw SQL query rather than a prepared statement to avoid circular dependency with `PreparedStatementCache` — the migration runner runs before the adapter is fully connected and the cache is available.
The `applied_at` column stores a Unix millisecond timestamp as `INTEGER` rather than ISO 8601 text. This keeps sorting and comparison simple and avoids timezone ambiguity. Consumers that need a `Date` object divide by 1000 or use `new Date(applied_at)`.

View File

@@ -0,0 +1,65 @@
# otel
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
OpenTelemetry instrumentation for `@suaveplan/db-sqlite`. The `withQuerySpan` function wraps any async database operation in a named OTel span, setting `db.system=sqlite` and `db.statement` (sanitised) as span attributes, recording exceptions on failure, and emitting latency and count metrics. It is a no-op when no OTel SDK is registered — the `@opentelemetry/api` no-op provider handles all calls silently.
`sanitizeSql` strips string literals and numeric values from SQL strings before attaching them to span attributes, preventing PII leakage into telemetry backends. Parameterised queries still record their structure (`SELECT * FROM users WHERE id = ?`) without revealing actual parameter values.
## Features
- `withQuerySpan(operationName, rawSql, fn)` — wraps fn in a named span with `db.system=sqlite` and sanitised `db.statement`
- `sanitizeSql(sql)` — replaces string literals, quoted identifiers, and bare numbers with `?` placeholders
- Histograms: `sqlite.query.duration_ms` (labels: `operation`)
- Counters: `sqlite.query.count`, `sqlite.error.count` (labels: `operation`)
- `resetOtelInstruments()` — clears the lazy instruments singleton for test isolation
## Usage: Basic Example
```ts
import { withQuerySpan } from "@suaveplan/db-sqlite/otel";
const rows = await withQuerySpan(
"db.query",
"SELECT * FROM users WHERE id = 42",
async () => sqlite.query("SELECT * FROM users WHERE id = ?").all([42])
);
```
## Usage: Advanced Example
```ts
import { sanitizeSql, withQuerySpan } from "@suaveplan/db-sqlite/otel";
// Preview sanitisation before instrumentation
const safe = sanitizeSql("SELECT * FROM t WHERE name = 'Alice' AND age = 30");
// => "SELECT * FROM t WHERE name = ? AND age = ?"
const result = await withQuerySpan("db.execute", safe, async () => {
sqlite.run("INSERT INTO events VALUES (?, ?)", ["login", 1234567890]);
return { rowsAffected: 1 };
});
```
## API Reference
### `withQuerySpan<T>(operationName, rawSql, fn): Promise<T>`
Starts a span named `operationName`, sets `db.system=sqlite` and `db.statement=sanitizeSql(rawSql)`, then calls `fn()`. On success records latency and increments `sqlite.query.count`. On failure records exception, sets `ERROR` status, and increments `sqlite.error.count`. Always ends the span.
### `sanitizeSql(sql: string): string`
Replaces single-quoted strings, double-quoted strings, and numeric literals (integer and decimal) with `?`. Safe to call on arbitrary SQL strings — does not parse SQL, only applies a regex substitution.
### `resetOtelInstruments(): void`
Clears the lazy OTel instruments singleton. Use in tests so each suite starts with fresh counter baselines. Also called internally by `SqliteAdapter.disconnect()`.
## Implementation Notes
The instruments lazy singleton is initialised on the first `withQuerySpan` call rather than at module load to ensure the OTel provider is registered before the meter is obtained. Tests that call `resetOtelInstruments()` between suites prevent counter state from leaking across test files.
`sanitizeSql` uses a single-pass regex and is intentionally simple — it does not understand SQL syntax, so complex nested strings or edge-case identifiers may not be fully sanitised. This is acceptable for telemetry purposes where approximate sanitisation is sufficient.

View File

@@ -0,0 +1,71 @@
# schemas
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Zod schemas and inferred TypeScript types for all `@suaveplan/db-sqlite` configuration objects. Centralising validation in one module ensures that every entry point — `createBunDriver`, `createWasmDriver`, `SqliteAdapter`, `MigrationRunner` — applies the same rules with the same error messages, rather than duplicating ad-hoc checks across modules.
All schemas use `.strict()` where applicable so extra unknown properties are rejected at runtime, preventing silent misconfiguration from typos in option objects.
## Features
- `BunDriverOptionsSchema` — validates `filename` (optional string) and `readonly` (optional boolean, default `false`)
- `WasmDriverOptionsSchema` — validates `filename` (optional string) for OPFS persistence
- `SqliteAdapterOptionsSchema` — alias for `BunDriverOptionsSchema`; adapter and driver share the same shape
- `MigrationRunnerOptionsSchema` — validates `migrationsFolder` (default `"./migrations"`), `migrationsTable` (default `"__sqlite_migrations"`), `dryRun` (default `false`)
- Inferred TypeScript types exported for all four schemas
## Usage: Basic Example
```ts
import { BunDriverOptionsSchema } from "@suaveplan/db-sqlite/schemas";
const opts = BunDriverOptionsSchema.parse({ filename: ":memory:" });
// opts.readonly === false (default applied)
```
## Usage: Advanced Example
```ts
import { MigrationRunnerOptionsSchema } from "@suaveplan/db-sqlite/schemas";
const opts = MigrationRunnerOptionsSchema.parse({
migrationsFolder: "./db/migrations",
dryRun: true,
});
// opts.migrationsTable === "__sqlite_migrations" (default applied)
// opts.dryRun === true
```
## API Reference
### `BunDriverOptionsSchema`
`z.object({ filename?: string, readonly?: boolean }).strict()` — `readonly` defaults to `false`.
### `WasmDriverOptionsSchema`
`z.object({ filename?: string }).strict()` — no defaults; absence means in-memory.
### `SqliteAdapterOptionsSchema`
Alias for `BunDriverOptionsSchema`. Both driver and adapter accept the same option shape.
### `MigrationRunnerOptionsSchema`
`z.object({ migrationsFolder?: string, migrationsTable?: string, dryRun?: boolean }).strict()`.
Defaults: `migrationsFolder = "./migrations"`, `migrationsTable = "__sqlite_migrations"`, `dryRun = false`.
### Type aliases
- `BunDriverOptions = z.infer<typeof BunDriverOptionsSchema>`
- `WasmDriverOptions = z.infer<typeof WasmDriverOptionsSchema>`
- `SqliteAdapterOptions = BunDriverOptions`
- `MigrationRunnerOptions = z.infer<typeof MigrationRunnerOptionsSchema>`
## Implementation Notes
`SqliteAdapterOptions` is deliberately an alias for `BunDriverOptions` rather than a separate schema so that passing an `SqliteAdapterOptions` object to `createBunDriver` is type-safe without casting. If the WASM adapter ever gains `readonly` support the alias can be split without a breaking change to the option names callers already use.

View File

@@ -0,0 +1,68 @@
# unsafe
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `unsafe` subpath exports `rawQuery` — a direct SQL execution function that bypasses the Drizzle ORM layer entirely. It is intentionally segregated behind the `@suaveplan/db-sqlite/unsafe` subpath import to make its use explicit and auditable: any import of this path stands out in code review as a deliberate bypass of the normal query builder.
**Security warning:** `rawQuery` provides no SQL injection protection. Never pass user-supplied or externally-sourced strings as the `sql` argument. Use only for migration scripts, low-level tooling, or read-only administrative queries where the SQL is fully under developer control.
## Features
- `rawQuery<T>(db, sql, params?)` — executes a prepared statement on any `bun:sqlite`-compatible Database instance
- Returns rows typed as `T[]` (caller asserts the row shape)
- Maps execution errors to `SqliteError(SQLITE.QUERY_FAILED)`
- Supports parameterised queries via the `params` array — always use `?` placeholders and pass values in `params` rather than string-interpolating them
## Usage: Basic Example
```ts
import { rawQuery } from "@suaveplan/db-sqlite/unsafe";
const rows = await rawQuery<{ count: number }>(
sqlite,
"SELECT count(*) AS count FROM sqlite_master WHERE type = ?",
["table"]
);
console.log(rows[0]?.count);
```
## Usage: Advanced Example
```ts
import { rawQuery } from "@suaveplan/db-sqlite/unsafe";
// Used in a migration helper to read schema state before Drizzle is set up
async function tableExists(sqlite: unknown, name: string): Promise<boolean> {
const rows = await rawQuery<{ found: number }>(
sqlite,
"SELECT 1 AS found FROM sqlite_master WHERE type = 'table' AND name = ?",
[name]
);
return rows.length > 0;
}
```
## API Reference
### `rawQuery<T>(db, sql, params?): Promise<T[]>`
Executes `sql` as a prepared statement on `db`. Throws `SqliteError(SQLITE.QUERY_FAILED)` on error.
**Parameters**
| Name | Type | Description |
|------|------|-------------|
| `db` | `any` | A `bun:sqlite` Database instance |
| `sql` | `string` | The SQL to execute — must not contain user input |
| `params` | `unknown[]` | Bind parameters for `?` placeholders |
**Returns** `Promise<T[]>` — rows shaped as `T`
## Implementation Notes
`rawQuery` uses `db.prepare(sql).all(params)` rather than `db.query(sql).all(params)` to be consistent with the caching strategy used by `SqliteAdapter`. The `params` spread form (`stmt.all(...params)`) passes each parameter as a positional argument, matching `bun:sqlite`'s expected calling convention.
The `db` parameter is typed as `any` to accept both `bun:sqlite Database` instances and test doubles without requiring a concrete import of `bun:sqlite` types from this module.