Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion backend/docs/database.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
18 changes: 18 additions & 0 deletions backend/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string | null>; meta: Record<string, unknown> } {
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<string, string | null>; meta: Record<string, unknown> } {
const totalPages = Math.ceil(totalCount / pageSize);
const pageLink = (page: number): string => {
Expand Down
Loading