storage.v

5.6 KB · raw

1//! The `.koh` file: schema, connection settings and machine-local state.2//!3//! One file at the repo root holds everything — objects, saves, events and4//! bookkeeping. There is no second source of truth, so there is nothing to5//! rebuild, verify against, or recover.67module storage89import os1011pub const schema_version = 112pub const file_name = '.koh'13pub const busy_timeout_ms = 50001415// Every fact Koh knows lives in these six tables. `state` and `file_cache` are16// machine-local: they are never synced and may be dropped at any time.17const schema = '18CREATE TABLE objects (19	hash       TEXT PRIMARY KEY,20	size       INTEGER NOT NULL,21	data       BLOB NOT NULL22);2324CREATE TABLE saves (25	id            TEXT PRIMARY KEY,26	parent        TEXT REFERENCES saves(id),27	message       TEXT NOT NULL,28	created_at    INTEGER NOT NULL,29	machine       TEXT NOT NULL,30	is_checkpoint INTEGER NOT NULL DEFAULT 0,31	size_bytes    INTEGER NOT NULL DEFAULT 0,32	agent         TEXT,33	session       TEXT,34	restores      TEXT,35	replayed_from TEXT36);3738CREATE TABLE save_files (39	save_id    TEXT NOT NULL REFERENCES saves(id),40	path       TEXT NOT NULL,41	hash       TEXT NOT NULL REFERENCES objects(hash),42	executable INTEGER NOT NULL DEFAULT 0,43	PRIMARY KEY (save_id, path)44);4546CREATE TABLE events (47	id         TEXT PRIMARY KEY,48	kind       TEXT NOT NULL,49	save_id    TEXT,50	path       TEXT,51	reason     TEXT,52	created_at INTEGER NOT NULL,53	machine    TEXT NOT NULL54);5556CREATE TABLE state (57	key   TEXT PRIMARY KEY,58	value TEXT NOT NULL59);6061CREATE TABLE file_cache (62	path     TEXT PRIMARY KEY,63	size     INTEGER NOT NULL,64	mtime_ns INTEGER NOT NULL,65	ctime_ns INTEGER NOT NULL,66	hash     TEXT NOT NULL67);6869CREATE INDEX saves_parent ON saves(parent);70CREATE INDEX save_files_hash ON save_files(hash);71'7273pub const head_key = 'head'7475@[heap]76pub struct Store {77pub mut:78	db   Db79	root string80}8182// find_root walks up from `start` until it finds a `.koh` file.83pub fn find_root(start string) ?string {84	mut dir := os.real_path(start)85	for {86		if os.is_file(os.join_path(dir, file_name)) {87			return dir88		}89		parent := os.dir(dir)90		if parent == dir || parent == '' {91			return none92		}93		dir = parent94	}95	return none96}9798// open connects to the repo at `root`. A missing `.koh` is an error, not a99// silent creation: `koh init` is the only thing that creates a repo.100pub fn open(root string) !Store {101	return open_at(os.join_path(root, file_name), root)102}103104// open_at connects to one database file that stands in for the repo's `.koh`.105// Nothing but migration needs that.106pub fn open_at(path string, root string) !Store {107	if !os.is_file(path) {108		return error('no ${file_name} in ${root} — run "koh init"')109	}110	db := connect(path, busy_timeout_ms)!111	apply_settings(db)!112	check_version(db)!113	return Store{114		db:   db115		root: root116	}117}118119// create writes a new `.koh` in `root`.120pub fn create(root string) !Store {121	return create_at(os.join_path(root, file_name), root)122}123124// create_at writes a fresh schema into an explicit file.125pub fn create_at(path string, root string) !Store {126	if os.is_file(path) {127		return error('${path} already exists')128	}129	db := connect(path, busy_timeout_ms)!130	apply_settings(db)!131	db.exec('BEGIN IMMEDIATE')!132	db.exec(schema)!133	db.exec('PRAGMA user_version = ${schema_version}')!134	db.exec('COMMIT')!135	return Store{136		db:   db137		root: root138	}139}140141pub fn (s &Store) close() {142	s.db.close()143}144145fn apply_settings(db &Db) ! {146	// WAL for crash safety, full sync so a committed save is on disk, foreign147	// keys so a half-written save cannot exist.148	db.exec('PRAGMA journal_mode=WAL')!149	db.exec('PRAGMA synchronous=FULL')!150	db.exec('PRAGMA foreign_keys=ON')!151}152153fn check_version(db &Db) ! {154	mut stmt := db.prepare('PRAGMA user_version')!155	version := if stmt.step()! { stmt.int(0) } else { 0 }156	stmt.finalize()157	if version > schema_version {158		return error('this .koh was written by a newer koh (schema ${version}, this build knows ${schema_version})')159	}160	if version < schema_version {161		return error('this .koh needs an upgrade koh does not know how to do (schema ${version})')162	}163}164165// What any write says after waiting busy_timeout_ms for another writer.166pub const busy = 'another koh command is writing to this repo — wait for it to finish, then run this again'167168pub fn (s &Store) begin() ! {169	s.db.exec('BEGIN IMMEDIATE')!170}171172pub fn (s &Store) commit() ! {173	s.db.exec('COMMIT')!174}175176pub fn (s &Store) rollback() {177	s.db.exec('ROLLBACK') or {}178}179180pub fn (s &Store) state(key string) ?string {181	mut stmt := s.db.prepare('SELECT value FROM state WHERE key = ?') or { return none }182	defer {183		stmt.finalize()184	}185	stmt.bind_text(1, key) or { return none }186	if stmt.step() or { return none } {187		return stmt.text(0)188	}189	return none190}191192pub fn (s &Store) set_state(key string, value string) ! {193	mut stmt := s.db.prepare('INSERT INTO state (key, value) VALUES (?, ?)194		ON CONFLICT(key) DO UPDATE SET value = excluded.value')!195	defer {196		stmt.finalize()197	}198	stmt.bind_text(1, key)!199	stmt.bind_text(2, value)!200	stmt.step()!201}202203pub fn (s &Store) remove_state(key string) ! {204	mut stmt := s.db.prepare('DELETE FROM state WHERE key = ?')!205	defer {206		stmt.finalize()207	}208	stmt.bind_text(1, key)!209	stmt.step()!210}211212// backup writes a consistent copy of the database. `VACUUM INTO` never copies213// a WAL mid-write, which a raw file copy would.214pub fn (s &Store) backup(path string) ! {215	if os.exists(path) {216		return error('${path} already exists')217	}218	s.db.exec("VACUUM INTO '${path.replace("'", "''")}'")!219}220221pub fn (s &Store) integrity_check() ![]string {222	mut problems := []string{}223	mut stmt := s.db.prepare('PRAGMA integrity_check')!224	defer {225		stmt.finalize()226	}227	for stmt.step()! {228		line := stmt.text(0)229		if line != 'ok' {230			problems << line231		}232	}233	return problems234}