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('END')!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}164165166pub 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) end() ! {173 s.db.exec('END')!174}175176pub fn (s &Store) rollback() {177 s.db.exec('ROLLBACK') or {}178}179180181182pub fn (s &Store) stripped_paths() ![]string {183 mut stmt := s.db.prepare("SELECT DISTINCT path FROM events WHERE kind = 'strip' AND IFNULL(save_id, '') = ''")!184 defer {185 stmt.finalize()186 }187 mut paths := []string{}188 for stmt.step()! {189 paths << stmt.text(0)190 }191 return paths192}193194pub fn (s &Store) state(key string) ?string {195 mut stmt := s.db.prepare('SELECT value FROM state WHERE key = ?') or { return none }196 defer {197 stmt.finalize()198 }199 stmt.bind_text(1, key) or { return none }200 if stmt.step() or { return none } {201 return stmt.text(0)202 }203 return none204}205206pub fn (s &Store) set_state(key string, value string) ! {207 mut stmt := s.db.prepare('INSERT INTO state (key, value) VALUES (?, ?)208 ON CONFLICT(key) DO UPDATE SET value = excluded.value')!209 defer {210 stmt.finalize()211 }212 stmt.bind_text(1, key)!213 stmt.bind_text(2, value)!214 stmt.step()!215}216217pub fn (s &Store) remove_state(key string) ! {218 mut stmt := s.db.prepare('DELETE FROM state WHERE key = ?')!219 defer {220 stmt.finalize()221 }222 stmt.bind_text(1, key)!223 stmt.step()!224}225226227228pub fn (s &Store) backup(path string) ! {229 if os.exists(path) {230 return error('${path} already exists')231 }232 s.db.exec("VACUUM INTO '${path.replace("'", "''")}'")!233}234235pub fn (s &Store) integrity_check() ![]string {236 mut problems := []string{}237 mut stmt := s.db.prepare('PRAGMA integrity_check')!238 defer {239 stmt.finalize()240 }241 for stmt.step()! {242 line := stmt.text(0)243 if line != 'ok' {244 problems << line245 }246 }247 return problems248}