1234567module storage89import os1011pub const schema_version = 112pub const file_name = '.koh'13pub const busy_timeout_ms = 500014151617const 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}818283pub 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}979899100pub fn open(root string) !Store {101 return open_at(os.join_path(root, file_name), root)102}103104105106pub 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}118119120pub fn create(root string) !Store {121 return create_at(os.join_path(root, file_name), root)122}123124125pub 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 147 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}164165pub fn (s &Store) begin() ! {166 s.db.exec('BEGIN IMMEDIATE')!167}168169pub fn (s &Store) commit() ! {170 s.db.exec('COMMIT')!171}172173pub fn (s &Store) rollback() {174 s.db.exec('ROLLBACK') or {}175}176177pub fn (s &Store) state(key string) ?string {178 mut stmt := s.db.prepare('SELECT value FROM state WHERE key = ?') or { return none }179 defer {180 stmt.finalize()181 }182 stmt.bind_text(1, key) or { return none }183 if stmt.step() or { return none } {184 return stmt.text(0)185 }186 return none187}188189pub fn (s &Store) set_state(key string, value string) ! {190 mut stmt := s.db.prepare('INSERT INTO state (key, value) VALUES (?, ?)191 ON CONFLICT(key) DO UPDATE SET value = excluded.value')!192 defer {193 stmt.finalize()194 }195 stmt.bind_text(1, key)!196 stmt.bind_text(2, value)!197 stmt.step()!198}199200pub fn (s &Store) remove_state(key string) ! {201 mut stmt := s.db.prepare('DELETE FROM state WHERE key = ?')!202 defer {203 stmt.finalize()204 }205 stmt.bind_text(1, key)!206 stmt.step()!207}208209210211pub fn (s &Store) backup(path string) ! {212 if os.exists(path) {213 return error('${path} already exists')214 }215 s.db.exec("VACUUM INTO '${path.replace("'", "''")}'")!216}217218pub fn (s &Store) integrity_check() ![]string {219 mut problems := []string{}220 mut stmt := s.db.prepare('PRAGMA integrity_check')!221 defer {222 stmt.finalize()223 }224 for stmt.step()! {225 line := stmt.text(0)226 if line != 'ok' {227 problems << line228 }229 }230 return problems231}