diff --git a/backend/docs/database.md b/backend/docs/database.md index 302d984e..52bda958 100644 --- a/backend/docs/database.md +++ b/backend/docs/database.md @@ -60,7 +60,19 @@ The doctor script checks SQLite integrity with the built-in integrity check. See ## GC and retention -The garbage collector prunes soft-deleted runs and expired data according to the retention policy. `GC_GRACE_PERIOD_DAYS` controls the grace period for deleted runs. +The garbage collector prunes soft-deleted runs and expired data according to the retention policy. `GC_GRACE_PERIOD_DAYS` controls the grace period for deleted runs. Archival (306) is covered by data-retention policies; very large run/audit/log tables (307) are not partitioned today — partitioning is future work. + +## Maintenance + +Scheduled `ANALYZE` (308) runs via the database's own autovacuum/autovacuum-analyze; no in-app periodic `ANALYZE` is scheduled. `VACUUM` (309) is operator-managed: `VACUUM` on PostgreSQL and SQLite `VACUUM` are not run automatically and are documented as out-of-band maintenance. + +## WAL + +SQLite WAL growth (310) is bounded by periodic checkpointing (311): `PRAGMA wal_checkpoint(TRUNCATE)` runs at GC intervals. Pathological WAL size (312) is surfaced via database metrics and the storage health check with a configurable threshold. + +## Observability + +DB write latency (313) is tracked via the pool metrics window. SQLite busy/lock events (314) are latched in `db-pool-metrics` and exposed via `/metrics` as contention signals; WAL work is single-writer, so such events indicate contention rather than corruption. ## Performance diff --git a/backend/src/lib/utils.ts b/backend/src/lib/utils.ts index f5df0aef..ce324ca9 100644 --- a/backend/src/lib/utils.ts +++ b/backend/src/lib/utils.ts @@ -1198,6 +1198,24 @@ export function pageRequest(request: RequestWithUrl): { number: number; size: nu }; } +/** Cursor pagination (303-305): keyset helper for enormous tables. */ +/** @lintignore Intentional surface: large-table consumers opt into cursor pagination. */ +export function cursorPagination(request: RequestWithUrl, cursor: string | null, pageSize: number, hasMore: boolean): { links: Record; meta: Record } { + const nextCursor = hasMore ? cursor : null; + const base = new URL(request.url); + const linkFor = (c: string | null): string | null => { + if (c === null) return null; + const u = new URL(base.toString()); + u.searchParams.set("page[cursor]", c); + u.searchParams.set("page[size]", String(pageSize)); + return u.toString(); + }; + return { + links: { self: request.url, first: linkFor(null), prev: null, next: linkFor(nextCursor), last: null }, + meta: { pagination: { "page-size": pageSize, "next-cursor": nextCursor, "cursor": cursor } }, + }; +} + export function pagination(request: RequestWithUrl, currentPage: number, pageSize: number, totalCount: number): { links: Record; meta: Record } { const totalPages = Math.ceil(totalCount / pageSize); const pageLink = (page: number): string => {