Files
suaveplan/openspec/specs/database/adapter/spec.md
2026-05-27 23:29:53 +01:00

83 lines
3.3 KiB
Markdown

# Spec: database/adapter
**Package**: `@suaveplan/db`
**Version**: `0.1.0`
**Status**: Shipped 2026-04-23
## Purpose
Universal relational database adapter interface for the genesis monorepo. Provides the `DatabaseAdapter` contract (extending `DatabaseLike` from `@suaveplan/types`), injectable connection singletons, structured `DB.*` error codes, Zod-validated config types, a `MigrationRunner` interface, and full OpenTelemetry instrumentation.
## Public API
### `DatabaseAdapter` (interface, extends `DatabaseLike`)
```ts
interface DatabaseAdapter extends DatabaseLike {
connect(config?: Partial<DatabaseConfig>, opts?: DbOperationOptions): Promise<void>;
disconnect(opts?: DbOperationOptions): Promise<void>;
query<T>(sql: string, params?: readonly unknown[], opts?: DbOperationOptions): Promise<T[]>;
execute(sql: string, params?: readonly unknown[], opts?: DbOperationOptions): Promise<{ rowsAffected: number }>;
transaction<T>(fn: (tx: DatabaseLike) => Promise<T>, opts?: DbOperationOptions): Promise<T>;
migrate(config?: MigrationConfig, opts?: DbOperationOptions): Promise<MigrationResult>;
readonly isConnected: boolean;
}
```
### Connection singletons
```ts
getSingleton(name, factory, config?, opts?, registry?): Promise<DatabaseAdapter>
closeSingleton(name, opts?, registry?): Promise<void>
hasSingleton(name, registry?): boolean
closeAllSingletons(opts?, registry?): Promise<void>
```
`registry` defaults to module-level `Map`; pass an explicit `Map` for test isolation.
### Error codes (`DB_CODES`)
| Code | When thrown |
|---|---|
| `DB.CONNECTION_FAILED` | `connect()` fails |
| `DB.NOT_CONNECTED` | Operation before `connect()` |
| `DB.QUERY_FAILED` | `query()` or `execute()` throws |
| `DB.TRANSACTION_FAILED` | `transaction()` throws |
| `DB.MIGRATION_FAILED` | Migration fails |
`DatabaseError extends SuaveplanError<DbCode>` · `MigrationError extends DatabaseError` (adds `migration: string`)
### Config types
- `DatabaseConfig` — `filename?`, `url?`, `readonlyMode?` (default `false`)
- `MigrationConfig` — `migrationsFolder?` (default `"./drizzle"`), `migrationsTable?` (default `"__drizzle_migrations"`)
- `MigrationResult` — `{ applied: number, migrations: string[] }`
- `MigrationStatus` — `{ name: string, applied: boolean, appliedAt?: Date }`
Zod schemas: `DatabaseConfigSchema`, `MigrationConfigSchema` (both `.strict()`).
### `MigrationRunner` (interface)
```ts
interface MigrationRunner {
pendingMigrations(config?: MigrationConfig): Promise<MigrationStatus[]>;
applyMigration(name: string, sql: string, opts?: DbOperationOptions): Promise<void>;
run(config?: MigrationConfig, opts?: DbOperationOptions): Promise<MigrationResult>;
}
```
### OTel instrumentation
- `withDbSpan(name, fn, attrs?)` — wraps async fn in a named OTel span
- `recordQueryDuration(ms, operation)` — histogram `db.query.duration_ms`
- `recordConnectionDelta(±1, dbName)` — UpDownCounter `db.connection.count`
- `recordTransaction(outcome)` — counter `db.transaction.count`
- `recordMigration(name)` — counter `db.migration.count`
- `resetInstruments()` — clears lazy singleton (test isolation)
## Quality
- 100% Istanbul coverage (statements/branches/functions/lines)
- 63 unit tests + property-based tests (fast-check)
- Benchmarks (mitata): `src/__benchmarks__/singleton.bench.ts`, `otel.bench.ts`