import { Database } from 'bun:sqlite'; import { existsSync, readFileSync } from 'node:fs'; import { basename, extname, isAbsolute, join, relative, resolve } from 'node:path'; import type { InventoryType, ReferenceRepository } from '../../domain/model'; import type { BuildProfile, ReferenceManifest, ReferenceSnapshot } from './types'; import { MANIFEST_FILE, DATABASE_DIRECTORY } from './compiler'; const MAX_IN_BATCH = 500; type TypeRow = { type_id: number; name_json: string; category_id: number; volume: number | null }; function assertArtifactPath(root: string, database: string): string { if (!database || isAbsolute(database) || database.includes('\\') || database.split('/').includes('..')) throw new Error('active reference database path is not a safe relative artifact path'); const artifactRoot = resolve(root, DATABASE_DIRECTORY); const candidate = resolve(root, database); const remainder = relative(artifactRoot, candidate); if (!remainder || remainder.includes('/') || remainder.startsWith('..') || isAbsolute(remainder) || extname(candidate) !== '.sqlite' || !/^[a-f0-9]{64}\.sqlite$/.test(basename(candidate))) throw new Error('active reference database is outside the artifact directory'); return candidate; } /** Runtime reader: a read-only handle pinned to one immutable manifest artifact. */ export class SqliteReferenceRepository implements ReferenceRepository, ReferenceSnapshot { private readonly db: Database; private closed = false; readonly pinnedVersion: string; readonly databasePath: string; constructor(directory: string, expectedProfile?: BuildProfile) { const root = resolve(directory); const manifestPath = join(root, MANIFEST_FILE); if (!existsSync(manifestPath)) throw new Error(`reference manifest not found: ${manifestPath}`); const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as ReferenceManifest; if (manifest.format !== 1 || !manifest.active) throw new Error(`reference manifest has no active artifact: ${manifestPath}`); if (expectedProfile && manifest.active.profile !== expectedProfile) throw new Error(`reference artifact must use the ${expectedProfile} profile`); this.pinnedVersion = manifest.active.version; this.databasePath = assertArtifactPath(root, manifest.active.database); if (basename(this.databasePath) !== `${this.pinnedVersion}.sqlite`) throw new Error('active reference database filename does not match its manifest version'); if (!existsSync(this.databasePath)) throw new Error(`active reference database not found: ${this.databasePath}`); this.db = new Database(this.databasePath, { readonly: true, strict: true }); assertReaderSchema(this.db); } /** Open another independent pinned handle. The caller owns and closes it. */ pin(): SqliteReferenceSnapshot { this.assertOpen(); return new SqliteReferenceSnapshot(this.databasePath, this.pinnedVersion); } snapshot(): SqliteReferenceSnapshot { return this.pin(); } async types(ids: number[]): Promise { this.assertOpen(); return queryTypes(this.db, ids); } async version(): Promise { this.assertOpen(); return this.pinnedVersion; } close(): void { if (!this.closed) { this.closed = true; this.db.close(); } } [Symbol.dispose](): void { this.close(); } private assertOpen(): void { if (this.closed) throw new Error('reference repository is closed'); } } export class SqliteReferenceSnapshot implements ReferenceSnapshot { private readonly db: Database; private closed = false; constructor(readonly databasePath: string, readonly pinnedVersion: string) { this.db = new Database(databasePath, { readonly: true, strict: true }); assertReaderSchema(this.db); } async types(ids: number[]): Promise { this.assertOpen(); return queryTypes(this.db, ids); } async version(): Promise { this.assertOpen(); return this.pinnedVersion; } close(): void { if (!this.closed) { this.closed = true; this.db.close(); } } [Symbol.dispose](): void { this.close(); } private assertOpen(): void { if (this.closed) throw new Error('reference snapshot is closed'); } } /** Cheap metadata check only; full integrity_check belongs to the compiler. */ function assertReaderSchema(db: Database): void { const row = db.query<{ present: number }, []>("SELECT COUNT(*) AS present FROM sqlite_master WHERE type='table' AND name IN ('types','categories')").get(); if (!row || Number(row.present) !== 2) { db.close(); throw new Error('active reference database has an unexpected schema'); } } async function queryTypes(db: Database, requested: number[]): Promise { const unique = [...new Set(requested.filter((id) => Number.isSafeInteger(id) && id >= 0))]; if (!unique.length) return []; const rows: TypeRow[] = []; // SQLite has a finite host-parameter limit. Chunking also bounds preparation // cost for callers that pass a complete asset snapshot in one request. for (let offset = 0; offset < unique.length; offset += MAX_IN_BATCH) { const batch = unique.slice(offset, offset + MAX_IN_BATCH); const placeholders = batch.map(() => '?').join(','); rows.push(...db.query(`SELECT type_id,name_json,category_id,volume FROM types WHERE type_id IN (${placeholders})`).all(...batch)); } const byId = new Map(rows.map((row) => [row.type_id, row])); return unique.flatMap((id) => { const row = byId.get(id); if (!row) return []; const names = JSON.parse(row.name_json) as Record; return [{ id: row.type_id, name: names.en ?? Object.values(names)[0] ?? String(row.type_id), categoryId: row.category_id, ...(row.volume === null ? {} : { volume: row.volume }) }]; }); }