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 path := os.join_path(root, file_name)102 if !os.is_file(path) {103 return error('no ${file_name} in ${root} — run "koh init"')104 }105 db := connect(path, busy_timeout_ms)!106 apply_settings(db)!107 check_version(db)!108 return Store{109 db: db110 root: root111 }112}113114115pub fn create(root string) !Store {116 path := os.join_path(root, file_name)117 if os.is_file(path) {118 return error('${path} already exists')119 }120 db := connect(path, busy_timeout_ms)!121 apply_settings(db)!122 db.exec('BEGIN IMMEDIATE')!123 db.exec(schema)!124 db.exec('PRAGMA user_version = ${schema_version}')!125 db.exec('COMMIT')!126 return Store{127 db: db128 root: root129 }130}131132pub fn (s &Store) close() {133 s.db.close()134}135136fn apply_settings(db &Db) ! {137 138 139 db.exec('PRAGMA journal_mode=WAL')!140 db.exec('PRAGMA synchronous=FULL')!141 db.exec('PRAGMA foreign_keys=ON')!142}143144fn check_version(db &Db) ! {145 mut stmt := db.prepare('PRAGMA user_version')!146 version := if stmt.step()! { stmt.int(0) } else { 0 }147 stmt.finalize()148 if version > schema_version {149 return error('this .koh was written by a newer koh (schema ${version}, this build knows ${schema_version})')150 }151 if version < schema_version {152 return error('this .koh needs an upgrade koh does not know how to do (schema ${version})')153 }154}155156pub fn (s &Store) begin() ! {157 s.db.exec('BEGIN IMMEDIATE')!158}159160pub fn (s &Store) commit() ! {161 s.db.exec('COMMIT')!162}163164pub fn (s &Store) rollback() {165 s.db.exec('ROLLBACK') or {}166}167168pub fn (s &Store) state(key string) ?string {169 mut stmt := s.db.prepare('SELECT value FROM state WHERE key = ?') or { return none }170 defer {171 stmt.finalize()172 }173 stmt.bind_text(1, key) or { return none }174 if stmt.step() or { return none } {175 return stmt.text(0)176 }177 return none178}179180pub fn (s &Store) set_state(key string, value string) ! {181 mut stmt := s.db.prepare('INSERT INTO state (key, value) VALUES (?, ?)182 ON CONFLICT(key) DO UPDATE SET value = excluded.value')!183 defer {184 stmt.finalize()185 }186 stmt.bind_text(1, key)!187 stmt.bind_text(2, value)!188 stmt.step()!189}190191pub fn (s &Store) remove_state(key string) ! {192 mut stmt := s.db.prepare('DELETE FROM state WHERE key = ?')!193 defer {194 stmt.finalize()195 }196 stmt.bind_text(1, key)!197 stmt.step()!198}199200201202pub fn (s &Store) backup(path string) ! {203 if os.exists(path) {204 return error('${path} already exists')205 }206 s.db.exec("VACUUM INTO '${path.replace("'", "''")}'")!207}208209pub fn (s &Store) integrity_check() ![]string {210 mut problems := []string{}211 mut stmt := s.db.prepare('PRAGMA integrity_check')!212 defer {213 stmt.finalize()214 }215 for stmt.step()! {216 line := stmt.text(0)217 if line != 'ok' {218 problems << line219 }220 }221 return problems222}