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

7.8 KiB

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)

// 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

getSingleton(
    name: string,
    factory: () => DatabaseAdapter,
    config?: Partial<DatabaseConfig>
): Promise<DatabaseAdapter>;

closeSingleton(name: string): Promise<void>;
hasSingleton(name: string): boolean;
closeAllSingletons(): Promise<void>;

Errors

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

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