Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,341 @@
# Tasks: enterprise-db-sqlite
Status key: ✅ done | 🔄 in progress | ⬜ pending
---
## Pre-flight: dependency gate
- ⬜ P.1 Confirm `@suaveplan/db` is published on genesis (provides `DatabaseAdapter`,
`DatabaseConfig`, `MigrationConfig`, `MigrationResult`, `DatabaseError`,
`getSingleton`, `closeSingleton`, `closeAllSingletons`, `hasSingleton`).
Do not start Wave 1 until this is green.
- ⬜ P.2 Add `catalog:database` block to root `package.json`:
```jsonc
"database": {
"drizzle-orm": "<latest-exact>",
"@sqlite.org/sqlite-wasm": "<latest-exact>"
}
```
Verify no version floats; use exact versions in Bun `overrides` if needed.
---
## Wave 1 — Scaffold
- ⬜ 1.1 Create directory `universal/database/db-sqlite/`
- ⬜ 1.2 Create `package.json` with:
- `name: "@suaveplan/db-sqlite"`, `version: "0.1.0"`, `sideEffects: false`, `type: "module"`
- `dependencies`: `drizzle-orm: "catalog:database"`, `@sqlite.org/sqlite-wasm: "catalog:database"`,
`@suaveplan/db: "workspace:^"`, `@suaveplan/error: "workspace:^"`,
`@suaveplan/types: "workspace:^"`, `tslib: "catalog:build"`
- `devDependencies`: `@suaveplan/logger: "workspace:^"`, `@suaveplan/retry: "workspace:^"`,
`@types/bun: "catalog:"`, `@types/node: "catalog:"`, `vite: "catalog:build"`,
`vite-plugin-dts: "catalog:build"`, `vitest: "catalog:testing"`,
`@vitest/coverage-istanbul: "catalog:testing"`, `fast-check: "catalog:testing"`,
`mitata: "catalog:testing"`
- Conditional `exports` map:
```jsonc
{
"./package.json": "./package.json",
".": {
"import": { "types": "./dist/index.d.ts", "default": "./dist/index.js" }
},
"./adapter": {
"bun": { "types": "./dist/adapter.bun.d.ts", "default": "./dist/adapter.bun.js" },
"browser": { "types": "./dist/adapter.wasm.d.ts", "default": "./dist/adapter.wasm.js" },
"default": { "types": "./dist/adapter.bun.d.ts", "default": "./dist/adapter.bun.js" }
},
"./unsafe": {
"import": { "types": "./dist/unsafe/index.d.ts", "default": "./dist/unsafe/index.js" }
},
"./errors/errors": {
"import": { "types": "./dist/errors/errors.d.ts", "default": "./dist/errors/errors.js" }
},
"./cache/cache": {
"import": { "types": "./dist/cache/cache.d.ts", "default": "./dist/cache/cache.js" }
},
"./migration/migration": {
"import": { "types": "./dist/migration/migration.d.ts", "default": "./dist/migration/migration.js" }
},
"./drivers/bun-driver": {
"import": { "types": "./dist/drivers/bun-driver.d.ts", "default": "./dist/drivers/bun-driver.js" }
},
"./drivers/wasm-driver": {
"import": { "types": "./dist/drivers/wasm-driver.d.ts", "default": "./dist/drivers/wasm-driver.js" }
},
"./connection/connection": {
"import": { "types": "./dist/connection/connection.d.ts", "default": "./dist/connection/connection.js" }
}
}
```
- `scripts`: `build`, `lint`, `typecheck`, `test`, `test:coverage`, `docs:check`
- ⬜ 1.3 Create `tsconfig.json` (extends `@suaveplan/typescript-config/library.json`)
- ⬜ 1.4 Create `tsconfig.lib.json` (strict, composite, declaration emit, excludes tests)
- ⬜ 1.5 Create `vite.config.ts` — dual entry (`index.ts` + `unsafe/index.ts`), `lib` mode,
`external: ['bun:sqlite', '@sqlite.org/sqlite-wasm', 'drizzle-orm', ...]`
- ⬜ 1.6 Create `vitest.config.ts` — Node pool config via `createTestConfig()`:
- Primary config: `pool: 'forks'`, `setupFiles: ['@suaveplan/testing/polyfills']`
- Browser/WASM sub-config: `fileParallelism: false`, `environment: 'happy-dom'`
- ⬜ 1.7 Create `biome.json` inheriting root config
- ⬜ 1.8 Create `src/index.ts` barrel (empty, filled incrementally per wave)
---
## Wave 2 — Types and errors
- ⬜ 2.1 Create `src/errors/errors.ts`:
- `SQLITE_CODES` registry via `@suaveplan/error#defineCodes`:
`SQLITE.NOT_CONNECTED`, `SQLITE.QUERY_FAILED`, `SQLITE.EXECUTE_FAILED`,
`SQLITE.TRANSACTION_FAILED`, `SQLITE.MIGRATION_FAILED`, `SQLITE.ENVIRONMENT_ERROR`,
`SQLITE.BUSY`, `SQLITE.CONSTRAINT`, `SQLITE.CORRUPT`, `SQLITE.DRIVER_INIT_FAILED`
- `SqliteError extends SuaveplanError` with `code: keyof typeof SQLITE_CODES`
- Helper `mapSqliteCode(err: unknown): keyof typeof SQLITE_CODES` mapping raw
SQLite result-code strings/numbers to stable `SQLITE.*` codes
- ⬜ 2.2 Create `src/errors/errors.types.ts` — exported type aliases
- ⬜ 2.3 Create `src/errors/index.ts` barrel
- ⬜ 2.4 Create `src/errors/errors.md` co-located docs (≥200 words, two examples)
- ⬜ 2.5 Create `src/errors/errors.test.ts` — full coverage of error construction,
code mapping, cause-chain, Zod wire-format serialisation
- ⬜ 2.6 Create `src/schemas/schemas.ts` — Zod schemas:
- `BunDriverOptionsSchema`, `WasmDriverOptionsSchema`, `SqliteAdapterOptionsSchema`,
`MigrationRunnerOptionsSchema`, `DatabaseConfigSchema`
- ⬜ 2.7 Create `src/schemas/schemas.md`, `src/schemas/index.ts`, `src/schemas/schemas.test.ts`
---
## Wave 3 — Bun driver
- ⬜ 3.1 Create `src/drivers/bun-driver.ts`:
- `BunDriverOptions` interface: `filename?: string`, `readonly?: boolean`
- `BunDriverResult` interface: `sqlite: Database`, `close: () => Promise<void>`
- `isBunEnvironment(): boolean`
- `createBunDriver(options?: BunDriverOptions): Promise<BunDriverResult>`
— dynamic import of `bun:sqlite`; `Database(":memory:")` or `new Database(filename, { create: true, readonly })`
- ⬜ 3.2 Create `src/drivers/bun-driver.md` (purpose, OPFS note, examples)
- ⬜ 3.3 Create `src/drivers/index.ts` barrel (re-exports bun + wasm drivers)
- ⬜ 3.4 Create `src/drivers/bun-driver.test.ts` — full coverage:
- In-memory creation
- File-backed creation and close
- `isBunEnvironment()` returns `true` under Bun test runner
- `close()` is idempotent
---
## Wave 4 — WASM driver
- ⬜ 4.1 Create `src/drivers/wasm-worker.ts` (Web Worker entry):
- Handles `init` message: loads `@sqlite.org/sqlite-wasm`, installs OPFS SAH-pool VFS
if `sqlite3.oo1.OpfsDb` is available, falls back to in-memory `":memory:"` with
`console.warn` when OPFS headers are absent
- Handles `query` message: `exec` via OO1 API, returns `resultRows` in `array` mode
- Handles `close` message: `db.close()`, posts `{ type: 'closed' }`
- Handles `begin_tx` / `commit_tx` / `rollback_tx` messages for batched transactions
- ⬜ 4.2 Create `src/drivers/wasm-driver.ts`:
- `WasmDriverOptions`: `{ filename?: string }`
- `WasmDriverResult<TSchema>`: `{ db: SqliteRemoteDatabase<TSchema>; close: () => Promise<void> }`
- `createWasmDriver<TSchema>(options?, schema?): Promise<WasmDriverResult<TSchema>>`
— spawns `wasm-worker.ts` via `new Worker(new URL('./wasm-worker.js', import.meta.url), { type: 'module' })`
— awaits `{ type: 'ready' }` init handshake with reject-on-error + worker termination guard
— pending-promise map keyed by sequential `id`, resolved/rejected by worker response
— `close()` drains pending map with rejection, sends `close` message, awaits `closed`, then `worker.terminate()`
— wraps Drizzle `sqlite-proxy` driver
- ⬜ 4.3 Create `src/drivers/wasm-driver.md` — OPFS prerequisites (COOP/COEP headers),
bundler worker-URL support, fallback behaviour, close semantics
- ⬜ 4.4 Create `src/drivers/wasm-driver.test.ts` (vitest `environment: 'happy-dom'`,
`fileParallelism: false`):
- Worker init success path (mock Worker via DI or in-process worker shim)
- Worker init failure propagation + worker termination
- Query round-trip
- `close()` drains pending and terminates worker
- Note: full OPFS integration test runs only in a real browser / Playwright — unit
coverage uses an injectable worker factory to avoid WSL2 OPFS limitations
---
## Wave 5 — Prepared-statement cache
- ⬜ 5.1 Create `src/cache/cache.ts`:
- `PreparedStatementCache` — LRU eviction, configurable `maxSize` (default 128)
- `get(sql: string): Statement | undefined`
- `set(sql: string, stmt: Statement): void`
- `invalidate(): void` — called by `SqliteAdapter.migrate()`; closes all held statements
- `get size(): number`
- OTel counters: `sqlite.cache.hits`, `sqlite.cache.misses`, `sqlite.cache.evictions`
via `@opentelemetry/api` — no-op when no provider registered
- ⬜ 5.2 Create `src/cache/cache.types.ts`, `src/cache/index.ts`, `src/cache/cache.md`
- ⬜ 5.3 Create `src/cache/cache.test.ts` — LRU eviction order, invalidate clears,
OTel counter increments, property-based test (fast-check) for cache-size invariant
---
## Wave 6 — Migration runner
- ⬜ 6.1 Create `src/migration/migration.ts`:
- `MigrationRunnerOptions`: `{ migrationsFolder?: string; migrationsTable?: string; dryRun?: boolean; clock?: Clock }`
- `MigrationRunner` class:
- Constructor takes a `BunSQLiteDatabase` and `MigrationRunnerOptions`
- `run(): Promise<MigrationResult>` — reads migration files from folder, checks ledger
table for applied entries, applies in filename order (forward-only, idempotent),
writes ledger row per applied migration, returns `{ applied: number; migrations: string[] }`
- `dryRun(): Promise<MigrationResult>` — same logic without writing to the DB
- Ledger table: `__sqlite_migrations (id TEXT PRIMARY KEY, applied_at INTEGER NOT NULL)`
- Emits OTel span (`sqlite.migration.run`) with `migration.file` attribute per migration
- Throws `SqliteError(SQLITE.MIGRATION_FAILED)` on DDL error with original cause
- `MigrationRunnerOptionsSchema` (Zod) validates options at construction
- ⬜ 6.2 Create `src/migration/migration.types.ts`, `src/migration/index.ts`,
`src/migration/migration.md`
- ⬜ 6.3 Create `src/migration/migration.test.ts` — ledger idempotency (run twice = same
result), dry-run does not alter DB, missing folder error, partial-failure rollback,
injectable clock for `applied_at` assertions
---
## Wave 7 — OTel instrumentation
- ⬜ 7.1 Create `src/otel/otel.ts`:
- `withQuerySpan<T>(operationName: string, sanitisedSql: string, fn: () => Promise<T>): Promise<T>`
— creates child span under active context, sets `db.system=sqlite`, `db.statement`,
records exception + sets ERROR status on failure, no-op when no OTel provider
- `sanitizeSql(sql: string): string` — strips string literals and numeric parameters
to prevent PII leakage in span attributes
- Histograms: `sqlite.query.duration_ms` (labels: operation, result)
- Counters: `sqlite.query.count`, `sqlite.error.count`
- ⬜ 7.2 Create `src/otel/otel.md`, `src/otel/index.ts`
- ⬜ 7.3 Create `src/otel/otel.test.ts` — verify span creation, sanitizeSql strips
literals, no-op when no provider, histogram updated
---
## Wave 8 — SqliteAdapter
- ⬜ 8.1 Create `src/adapter/adapter.ts`:
- `SqliteAdapterOptions = BunDriverOptions`
- `SqliteAdapter<TSchema extends Record<string, unknown> = Record<string, never>>`
implements `DatabaseAdapter` from `@suaveplan/db`
- Constructor: validates options via `SqliteAdapterOptionsSchema`, throws
`SqliteError(SQLITE.ENVIRONMENT_ERROR)` if `!isBunEnvironment()`
- `protected _drizzle: BunSQLiteDatabase<TSchema> | null`
- `protected _sqlite: import('bun:sqlite').Database | null`
- `protected _connected: boolean`
- `protected _connectingPromise: Promise<void> | null` (idempotent concurrent connect)
- `protected _cache: PreparedStatementCache`
- `connect(config?: Partial<DatabaseConfig>): Promise<void>` — uses cache, sets
`_drizzle` via `drizzle(sqlite, { schema })`
- `disconnect(): Promise<void>` — closes sqlite, nulls state, calls `_cache.invalidate()`
- `query<T>(sql, params?, signal?: AbortSignal): Promise<T[]>` — wraps
`_cache.get/set` + `withQuerySpan`; checks `signal.aborted` before execution;
maps caught errors to `SqliteError` via `mapSqliteCode`
- `execute(sql, params?, signal?: AbortSignal): Promise<void>` — same pattern
- `transaction<T>(fn: () => Promise<T>): Promise<T>` — `BEGIN IMMEDIATE` / `COMMIT` /
`ROLLBACK`; wraps in `withQuerySpan('transaction', 'BEGIN IMMEDIATE')`
- `migrate(config?: MigrationConfig): Promise<MigrationResult>` — delegates to
`MigrationRunner`, calls `_cache.invalidate()` on success
- `get isConnected: boolean`
- `get drizzle: BunSQLiteDatabase<TSchema>` — asserts connected, returns `_drizzle!`
- `protected assertConnected(): void`
- ⬜ 8.2 Create `src/adapter/adapter.types.ts` (re-exports `BunSQLiteDatabase` type alias)
- ⬜ 8.3 Create `src/adapter/index.ts`, `src/adapter/adapter.md`
- ⬜ 8.4 Create `src/adapter/adapter.test.ts` — connect/disconnect lifecycle,
double-connect idempotency, drizzle getter throws before connect,
transaction commit and rollback, migration invalidates cache,
AbortSignal cancellation, error code mapping, property-based test for
concurrent-connect race (fast-check `schedulerFor`)
---
## Wave 9 — Connection singleton helpers
- ⬜ 9.1 Create `src/connection/connection.ts`:
- `getDefaultSqlite(options?: SqliteAdapterOptions, config?: Partial<DatabaseConfig>, name?: string): Promise<SqliteAdapter>`
— thin wrapper around `@suaveplan/db#getSingleton`
- Re-export `closeSingleton as closeSqlite`, `closeAllSingletons as closeAllSqlite`,
`hasSingleton as hasSqlite` from `@suaveplan/db`
- ⬜ 9.2 Create `src/connection/connection.md`, `src/connection/index.ts`
- ⬜ 9.3 Create `src/connection/connection.test.ts` — get creates adapter, second get
returns same instance, close removes it, `hasSqlite` reflects state
---
## Wave 10 — Unsafe subpath
- ⬜ 10.1 Create `src/unsafe/index.ts`:
- `rawQuery<T = Record<string, unknown>>(db: BunSQLiteDatabase<any> | SqliteRemoteDatabase<any>, sql: string, params?: unknown[]): Promise<T[]>`
- JSDoc warning: "Unsafe — bypasses Drizzle ORM; no SQL injection protection. Use only in migration scripts or low-level tooling."
- ⬜ 10.2 Create `src/unsafe/unsafe.md` — explicit security warning in Purpose section
- ⬜ 10.3 Create `src/unsafe/unsafe.test.ts` — basic execution, error propagation
---
## Wave 11 — Barrel and conformance suite
- ⬜ 11.1 Wire `src/index.ts` — export all public symbols from waves 2–10:
`SqliteAdapter`, `SqliteAdapterOptions`, `SqliteError`, `SQLITE_CODES`,
`createBunDriver`, `BunDriverOptions`, `BunDriverResult`, `isBunEnvironment`,
`createWasmDriver`, `WasmDriverOptions`, `WasmDriverResult`,
`getDefaultSqlite`, `closeSqlite`, `closeAllSqlite`, `hasSqlite`,
Zod schemas, `PreparedStatementCache`, `MigrationRunner`, `MigrationRunnerOptions`
- ⬜ 11.2 Create `src/conformance/conformance-suite.ts` — driver-agnostic test factory:
- Accepts a `DriverFactory` callback that returns `{ db: DrizzleClient; close(): Promise<void> }`
- Test groups: CRUD round-trip, transaction commit, transaction rollback, concurrent
writes (SQLITE_BUSY retry via `@suaveplan/retry`), schema migration idempotency,
prepared-statement cache hit ratio, `AbortSignal` cancellation mid-query
- ⬜ 11.3 Create `src/conformance/bun-conformance.test.ts` — runs suite with Bun driver
- ⬜ 11.4 Create `src/conformance/wasm-conformance.test.ts` — runs suite with WASM driver
(`fileParallelism: false`, `environment: 'happy-dom'`)
- ⬜ 11.5 Create `src/conformance/conformance-suite.md`
---
## Wave 12 — Property-based tests
- ⬜ 12.1 Add `src/adapter/adapter.property.test.ts` — fast-check arbitraries for:
- SQL parameterisation: verify no raw-string interpolation leaks by generating
adversarial inputs and confirming Drizzle always uses `?` binding
- Transaction isolation: interleaved concurrent transactions produce consistent
row counts (serialisability invariant under Bun's cooperative concurrency)
- Cache size invariant: `cache.size <= maxSize` after arbitrary insert/evict sequences
- ⬜ 12.2 Add `src/migration/migration.property.test.ts` — fast-check: arbitrary lists
of migration files always produce monotonically increasing ledger entries; re-running
produces identical result (idempotency law)
---
## Wave 13 — Benchmarks
- ⬜ 13.1 Create `src/__benchmarks__/sqlite.bench.ts` (mitata):
- Single-row insert (prepared vs. ad-hoc)
- Bulk insert — 1 000 rows in transaction
- Point lookup by primary key
- Full-table scan — 1 000 rows
- Cache-warmed vs. cold prepared statement
- ⬜ 13.2 Add `bench` script to `package.json`: `bun src/__benchmarks__/sqlite.bench.ts`
- ⬜ 13.3 Record baseline numbers in README under a `## Performance` heading
---
## Wave 14 — Documentation
- ⬜ 14.1 Write `README.md` at package root — sections: Purpose, Installation, Quick Start
(Bun server, browser WASM), API Reference (all exports), Conditional Exports map,
OPFS setup guide (COOP/COEP headers), Migration Runner usage, Unsafe subpath warning,
Performance numbers, Contributing
- ⬜ 14.2 Verify every `.ts` implementation file has a sibling `.md` with ≥200 words and
both a basic and an advanced usage example
- ⬜ 14.3 Run `bun run docs:check` and fix any missing-doc gaps
---
## Wave 15 — Pre-publish gate
- ⬜ 15.1 Run `bunx turbo lint --filter=@suaveplan/db-sqlite` — fix all Biome findings
- ⬜ 15.2 Run `bunx turbo typecheck --filter=@suaveplan/db-sqlite` — zero type errors
- ⬜ 15.3 Run `bunx turbo test --filter=@suaveplan/db-sqlite` — all tests green
- ⬜ 15.4 Run `bunx turbo test:coverage --filter=@suaveplan/db-sqlite` — Istanbul reports
100% statement, branch, function, line coverage on all touched modules
- ⬜ 15.5 Verify `sideEffects: false` tree-shake test passes
- ⬜ 15.6 Update `CLAUDE.md` package listing entry for `@suaveplan/db-sqlite`
- ⬜ 15.7 Update root `README.md` table row for `@suaveplan/db-sqlite`
- ⬜ 15.8 Run `bun scripts/publish.ts --package @suaveplan/db-sqlite`
- ⬜ 15.9 Move this change folder to
`openspec/archive/2026-04-23-enterprise-db-sqlite/`