Initial Commit
This commit is contained in:
178
openspec/archive/2026-04-23-enterprise-db/proposal.md
Normal file
178
openspec/archive/2026-04-23-enterprise-db/proposal.md
Normal file
@@ -0,0 +1,178 @@
|
||||
# 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<DatabaseConfig>): Promise<void>;
|
||||
disconnect(): Promise<void>;
|
||||
query<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T[]>;
|
||||
execute(sql: string, params?: unknown[]): Promise<void>;
|
||||
transaction<T>(fn: () => Promise<T>): Promise<T>;
|
||||
migrate(config?: MigrationConfig): Promise<MigrationResult>;
|
||||
readonly isConnected: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
### Connection singleton helpers
|
||||
|
||||
```ts
|
||||
getSingleton(
|
||||
name: string,
|
||||
factory: () => DatabaseAdapter,
|
||||
config?: Partial<DatabaseConfig>
|
||||
): Promise<DatabaseAdapter>;
|
||||
|
||||
closeSingleton(name: string): Promise<void>;
|
||||
hasSingleton(name: string): boolean;
|
||||
closeAllSingletons(): Promise<void>;
|
||||
```
|
||||
|
||||
### 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<void>` — lifecycle open
|
||||
- `disconnect(opts?: { signal?: AbortSignal }): Promise<void>` — lifecycle close (maps to `DatabaseLike.close`)
|
||||
- `migrate(config?: MigrationConfig, opts?: { signal?: AbortSignal }): Promise<MigrationResult>`
|
||||
- `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<string, DatabaseAdapter>` 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
|
||||
Reference in New Issue
Block a user