queries.ts

3.4 KB · raw

1import type { Database } from "bun:sqlite";2import { inflateSync } from "node:zlib";3import { mirrorState } from "./store.ts";45export type Save = {6	id: string;7	parent: string;8	message: string;9	created_at: number;10	machine: string;11	agent: string;12	session: string;13	is_checkpoint: boolean;14};1516export type FileEntry = { path: string; hash: string; executable: boolean; size: number };1718export type RepoStats = { saves: number; lines: number; language: string; updated: number | null };1920const SAVE_COLUMNS = `id, COALESCE(parent, '') AS parent, message, created_at, machine,21	COALESCE(agent, '') AS agent, COALESCE(session, '') AS session, is_checkpoint = 1 AS is_checkpoint`;22const ID_PREFIX = /^[0-9a-f]{6,12}$/;2324// Checkpoints are working marks, not history (§21.4): every list and count below leaves them out.25const NOT_CHECKPOINT = "is_checkpoint = 0";2627export function headId(db: Database): string | null {28	return mirrorState(db, "head");29}3031/** A page of saves, newest first. Rows land oldest first, so rowid is the order of the line. */32export function history(db: Database, limit: number, offset: number): Save[] {33	return db34		.query<Save, [number, number]>(35			`SELECT ${SAVE_COLUMNS} FROM saves WHERE ${NOT_CHECKPOINT} ORDER BY rowid DESC LIMIT ? OFFSET ?`,36		)37		.all(limit, offset)38		.map(asSave);39}4041/** Every save, oldest first, for the timeline. */42export function timeline(db: Database): Save[] {43	return db44		.query<Save, []>(`SELECT ${SAVE_COLUMNS} FROM saves WHERE ${NOT_CHECKPOINT} ORDER BY rowid`)45		.all()46		.map(asSave);47}4849/** The save a unique prefix of 6–12 hex characters names (§6.1), or null when none or several do. */50export function saveByPrefix(db: Database, prefix: string): Save | null {51	if (!ID_PREFIX.test(prefix)) return null;52	const rows = db53		.query<Save, [string]>(`SELECT ${SAVE_COLUMNS} FROM saves WHERE id LIKE ? || '%' LIMIT 2`)54		.all(prefix);55	return rows.length === 1 ? asSave(rows[0] as Save) : null;56}5758export function filesAt(db: Database, saveId: string): FileEntry[] {59	return db60		.query<FileEntry, [string]>(61			`SELECT f.path, f.hash, f.executable = 1 AS executable, o.size FROM save_files f62			JOIN objects o ON o.hash = f.hash WHERE f.save_id = ? ORDER BY f.path`,63		)64		.all(saveId)65		.map((file) => ({ ...file, executable: Boolean(file.executable) }));66}6768export function fileAt(db: Database, saveId: string, path: string): FileEntry | null {69	const file = db70		.query<FileEntry, [string, string]>(71			`SELECT f.path, f.hash, f.executable = 1 AS executable, o.size FROM save_files f72			JOIN objects o ON o.hash = f.hash WHERE f.save_id = ? AND f.path = ?`,73		)74		.get(saveId, path);75	return file ? { ...file, executable: Boolean(file.executable) } : null;76}7778export function objectBytes(db: Database, hash: string): Uint8Array<ArrayBuffer> | null {79	const row = db80		.query<{ data: Uint8Array }, [string]>("SELECT data FROM objects WHERE hash = ?")81		.get(hash);82	return row ? new Uint8Array(inflateSync(row.data)) : null;83}8485export function repoStats(db: Database): RepoStats {86	const row = db87		.query<{ saves: number; updated: number | null }, []>(88			`SELECT COUNT(*) AS saves, MAX(created_at) AS updated FROM saves WHERE ${NOT_CHECKPOINT}`,89		)90		.get() ?? { saves: 0, updated: null };91	return {92		saves: row.saves,93		updated: row.updated,94		lines: Number(mirrorState(db, "lines") ?? 0),95		language: mirrorState(db, "language") ?? "",96	};97}9899function asSave(row: Save): Save {100	return { ...row, is_checkpoint: Boolean(row.is_checkpoint) };101}