# OpenSpec Change: enterprise-db - **Change ID**: enterprise-db - **Package**: `@suaveplan/db` - **Category**: universal - **Target path**: `universal/database/db` - **Status**: IN PROGRESS - **Date**: 2026-04-23 - **Reference spec**: `/mnt/s/web/suaveplan/enterprise/db.md` ## Summary `@suaveplan/db` is the universal relational database interface for the genesis monorepo. It ships the abstract `DatabaseAdapter` contract, structured errors with stable `DB.*` codes, Zod-validated config types, injectable connection-singleton helpers, and an `AbortSignal`-aware migration runner interface. The package is the stable foundation that `@suaveplan/db-sqlite` (and future Postgres / Turso adapters) build upon — its public API surface must be locked before any concrete adapter ships. This genesis build replaces the main-branch implementation with a fully enterprise-quality package: `DatabaseAdapter` extends `DatabaseLike` from `@suaveplan/types`, errors extend `@suaveplan/error` with a stable code registry, every async entrypoint accepts an `AbortSignal`, all public types have Zod schemas, and OTel spans / histograms / counters are wired throughout. Connection singletons shed global module state in favour of injectable registry maps so they are safe in test isolation and serverless environments. ## Current-state audit (genesis) The package does not exist on genesis yet — this is a fresh build. Main branch ships `packages/universal/db` with the surface listed below; genesis rewrites from scratch. ## Available workspace deps on genesis | Package | Ref | Role | |---|---|---| | `@suaveplan/error` | `workspace:^` | Structured error base class + `defineCodes` | | `@suaveplan/types` | `workspace:^` | `DatabaseLike` minimal adapter contract | | `@suaveplan/events` | `workspace:^` | Optional lifecycle event bus (connect / disconnect / error events) | | `zod` | `catalog:forms` | Runtime schema validation | | `@opentelemetry/api` | `catalog:observability` | OTel spans, counters, histograms (no-op when no exporter) | | `tslib` | `catalog:build` | TypeScript helpers | Dev deps (all `catalog:testing`): `vitest`, `@vitest/coverage-istanbul`, `fast-check`, `mitata`. ## Preserved public API surface (from main branch) All of the following symbols are preserved verbatim or as strict supersets. ### `DatabaseAdapter` (interface) ```ts // main-branch shape — genesis extends this with DatabaseLike export interface DatabaseAdapter { connect(config?: Partial): Promise; disconnect(): Promise; query>(sql: string, params?: unknown[]): Promise; execute(sql: string, params?: unknown[]): Promise; transaction(fn: () => Promise): Promise; migrate(config?: MigrationConfig): Promise; readonly isConnected: boolean; } ``` ### Connection singleton helpers ```ts getSingleton( name: string, factory: () => DatabaseAdapter, config?: Partial ): Promise; closeSingleton(name: string): Promise; hasSingleton(name: string): boolean; closeAllSingletons(): Promise; ``` ### Errors ```ts class DatabaseError extends Error { readonly code: string; readonly cause?: unknown; constructor(message: string, code: string, cause?: Error); } class MigrationError extends DatabaseError { readonly migration: string; constructor(migration: string, detail: string, cause?: Error); } ``` ### Config and result types ```ts interface DatabaseConfig { filename?: string; // embedded (SQLite, ':memory:') url?: string; // network (postgres://, ...) readonlyMode?: boolean; } interface MigrationConfig { migrationsFolder?: string; // default './drizzle' migrationsTable?: string; // default '__drizzle_migrations' } interface MigrationStatus { name: string; applied: boolean; appliedAt?: Date; } interface MigrationResult { applied: number; migrations: string[]; } ``` ## New additions for genesis ### `DatabaseAdapter` extends `DatabaseLike` `DatabaseLike` (from `@suaveplan/types`) defines `query`, `execute`, `transaction`, and `close`. The genesis `DatabaseAdapter` extends this contract and adds: - `connect(config?, opts?: { signal?: AbortSignal }): Promise` — lifecycle open - `disconnect(opts?: { signal?: AbortSignal }): Promise` — lifecycle close (maps to `DatabaseLike.close`) - `migrate(config?: MigrationConfig, opts?: { signal?: AbortSignal }): Promise` - `readonly isConnected: boolean` — connection state predicate `query`, `execute`, and `transaction` gain an optional trailing options object `{ signal?: AbortSignal }` while remaining backward-compatible. The `transaction` callback receives a `DatabaseLike` (the active transaction handle) matching `DatabaseLike`'s own signature — fixing the main-branch deviation where `fn` accepted no argument. ### Structured errors extending `@suaveplan/error` `DatabaseError` extends `SuaveplanError` (from `@suaveplan/error`) with stable codes registered via `defineCodes`: | Code | When thrown | |---|---| | `DB.CONNECTION_FAILED` | `connect()` fails | | `DB.NOT_CONNECTED` | Operation attempted before `connect()` | | `DB.QUERY_FAILED` | `query()` or `execute()` throws | | `DB.TRANSACTION_FAILED` | `transaction()` throws or rolls back | | `DB.MIGRATION_FAILED` | `migrate()` fails on a specific migration | `MigrationError` extends `DatabaseError` with `DB.MIGRATION_FAILED` and adds a `migration: string` field (the migration name). ### Zod schemas `DatabaseConfigSchema` and `MigrationConfigSchema` (exported from `*.schemas.ts` siblings). Validated at the boundary of `getSingleton` and `migrate` helpers. ### Injectable registry for singleton helpers `getSingleton`, `closeSingleton`, `hasSingleton`, and `closeAllSingletons` accept an optional `registry: Map` parameter. When omitted a module-level default map is used (preserving the main-branch behaviour). This allows test suites to pass isolated registries with zero cross-test bleed. ### OTel instrumentation All public async entrypoints are wrapped in `@opentelemetry/api` spans. Additionally: - **Histogram** `db.query.duration_ms` — labelled by `db.operation` (`query` / `execute`) - **Counter** `db.connection.count` — incremented on successful `connect`, decremented on `disconnect` - **Counter** `db.transaction.count` — labelled by outcome (`committed` / `rolled_back`) - **Counter** `db.migration.count` — incremented per applied migration All instrumentation is lazy-initialised; no global OTel state at module load. ### AbortSignal propagation Every async method (`connect`, `disconnect`, `query`, `execute`, `transaction`, `migrate`) accepts `opts?: { signal?: AbortSignal }`. The concrete adapter is responsible for honouring the signal — the abstract contract documents the expectation. Abort throws `DatabaseError` with code `DB.QUERY_FAILED` (or the relevant operation code) and `cause` set to an `AbortError`. ### Property-based tests (fast-check) Fast-check arbitraries model the connection-state machine: - An unconnected adapter may not have `query`, `execute`, `transaction`, or `migrate` succeed (invariant). - `connect → disconnect → connect` is idempotent (no state leak). - `getSingleton` with the same name and registry always returns the same reference. - `closeAllSingletons` leaves the registry empty regardless of concurrency order. ### Benchmarks (mitata) `src/__benchmarks__/adapter.bench.ts` — baseline numbers for singleton lookup, OTel span overhead, schema validation. Published in README. ## Impact - Adds one new package: `universal/database/db` - Adds one new spec capability: `database/adapter` - Unblocks `@suaveplan/db-sqlite` (direct dep) and every higher-layer package that injects `DatabaseLike` - No changes to existing packages; `@suaveplan/types.DatabaseLike` is already shipped