Files
suaveplan/openspec/archive/2026-04-24-enterprise-db-sqlite/tasks.md
2026-05-27 23:29:53 +01:00

342 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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/`