| 1 | //! The todo store: one SQLite file in WAL mode, one connection shared by |
| 2 | //! every request behind a mutex. A todo app's queries take microseconds, |
| 3 | //! so a single serialised connection is simpler than a pool and plenty fast. |
| 4 | |
| 5 | const std = @import("std"); |
| 6 | const c = @import("sqlite"); |
| 7 | const Io = std.Io; |
| 8 | const Allocator = std.mem.Allocator; |
| 9 | |
| 10 | pub const Error = error{Sqlite}; |
| 11 | |
| 12 | pub const Todo = struct { |
| 13 | id: i64, |
| 14 | title: []const u8, |
| 15 | completed: bool, |
| 16 | }; |
| 17 | |
| 18 | /// Migrations only ever add. `PRAGMA user_version` records how many have run. |
| 19 | const migrations = [_][:0]const u8{ |
| 20 | \\CREATE TABLE lists ( |
| 21 | \\ id INTEGER PRIMARY KEY, |
| 22 | \\ token_hash TEXT NOT NULL UNIQUE, |
| 23 | \\ created_at INTEGER NOT NULL DEFAULT (unixepoch()) |
| 24 | \\); |
| 25 | \\CREATE TABLE todos ( |
| 26 | \\ id INTEGER PRIMARY KEY AUTOINCREMENT, |
| 27 | \\ list_id INTEGER NOT NULL REFERENCES lists(id), |
| 28 | \\ title TEXT NOT NULL, |
| 29 | \\ completed INTEGER NOT NULL DEFAULT 0 |
| 30 | \\); |
| 31 | \\CREATE INDEX todos_by_list ON todos(list_id, id); |
| 32 | , |
| 33 | }; |
| 34 | |
| 35 | pub const Store = struct { |
| 36 | db: *c.sqlite3, |
| 37 | mutex: Io.Mutex = .init, |
| 38 | io: Io, |
| 39 | |
| 40 | pub fn open(io: Io, path: [:0]const u8) Error!Store { |
| 41 | var db: ?*c.sqlite3 = null; |
| 42 | const flags = c.SQLITE_OPEN_READWRITE | c.SQLITE_OPEN_CREATE | c.SQLITE_OPEN_FULLMUTEX; |
| 43 | if (c.sqlite3_open_v2(path.ptr, &db, flags, null) != c.SQLITE_OK) { |
| 44 | if (db) |d| { |
| 45 | std.log.err("sqlite open {s}: {s}", .{ path, c.sqlite3_errmsg(d) }); |
| 46 | _ = c.sqlite3_close(d); |
| 47 | } |
| 48 | return error.Sqlite; |
| 49 | } |
| 50 | var store: Store = .{ .db = db.?, .io = io }; |
| 51 | errdefer store.close(); |
| 52 | _ = c.sqlite3_busy_timeout(store.db, 5000); |
| 53 | try store.exec("PRAGMA journal_mode = WAL"); |
| 54 | try store.exec("PRAGMA synchronous = NORMAL"); |
| 55 | try store.exec("PRAGMA foreign_keys = ON"); |
| 56 | try store.migrate(); |
| 57 | return store; |
| 58 | } |
| 59 | |
| 60 | pub fn close(s: *Store) void { |
| 61 | _ = c.sqlite3_close(s.db); |
| 62 | } |
| 63 | |
| 64 | fn migrate(s: *Store) Error!void { |
| 65 | var st = try s.prepare("PRAGMA user_version"); |
| 66 | const current: usize = if (try st.step()) @intCast(st.int(0)) else 0; |
| 67 | st.finalize(); |
| 68 | for (migrations[@min(current, migrations.len)..], current..) |sql, i| { |
| 69 | try s.exec("BEGIN"); |
| 70 | errdefer s.exec("ROLLBACK") catch {}; |
| 71 | try s.exec(sql); |
| 72 | var buf: [64]u8 = undefined; |
| 73 | const set = std.fmt.bufPrintSentinel(&buf, "PRAGMA user_version = {d}", .{i + 1}, 0) catch unreachable; |
| 74 | try s.exec(set); |
| 75 | try s.exec("COMMIT"); |
| 76 | } |
| 77 | } |
| 78 | |
| 79 | fn exec(s: *Store, sql: [:0]const u8) Error!void { |
| 80 | var err: [*c]u8 = null; |
| 81 | if (c.sqlite3_exec(s.db, sql.ptr, null, null, &err) != c.SQLITE_OK) { |
| 82 | std.log.err("sqlite: {s}", .{err}); |
| 83 | c.sqlite3_free(err); |
| 84 | return error.Sqlite; |
| 85 | } |
| 86 | } |
| 87 | |
| 88 | fn prepare(s: *Store, sql: [:0]const u8) Error!Stmt { |
| 89 | var stmt: ?*c.sqlite3_stmt = null; |
| 90 | if (c.sqlite3_prepare_v2(s.db, sql.ptr, @intCast(sql.len + 1), &stmt, null) != c.SQLITE_OK) { |
| 91 | std.log.err("sqlite prepare: {s}", .{c.sqlite3_errmsg(s.db)}); |
| 92 | return error.Sqlite; |
| 93 | } |
| 94 | return .{ .stmt = stmt.?, .db = s.db }; |
| 95 | } |
| 96 | |
| 97 | /// Runs one statement that returns no rows; answers how many rows it changed. |
| 98 | fn run(s: *Store, sql: [:0]const u8, args: anytype) Error!usize { |
| 99 | var st = try s.prepare(sql); |
| 100 | defer st.finalize(); |
| 101 | try st.bindAll(args); |
| 102 | while (try st.step()) {} |
| 103 | return @intCast(c.sqlite3_changes(s.db)); |
| 104 | } |
| 105 | |
| 106 | fn lock(s: *Store) void { |
| 107 | s.mutex.lockUncancelable(s.io); |
| 108 | } |
| 109 | |
| 110 | fn unlock(s: *Store) void { |
| 111 | s.mutex.unlock(s.io); |
| 112 | } |
| 113 | |
| 114 | // The queries the app runs. Every one that touches a todo is scoped |
| 115 | // by list, so one visitor can never reach another's items. |
| 116 | |
| 117 | pub fn findList(s: *Store, token_hash: []const u8) Error!?i64 { |
| 118 | s.lock(); |
| 119 | defer s.unlock(); |
| 120 | var st = try s.prepare("SELECT id FROM lists WHERE token_hash = ?"); |
| 121 | defer st.finalize(); |
| 122 | try st.bindAll(.{token_hash}); |
| 123 | return if (try st.step()) st.int(0) else null; |
| 124 | } |
| 125 | |
| 126 | pub fn createList(s: *Store, token_hash: []const u8) Error!i64 { |
| 127 | s.lock(); |
| 128 | defer s.unlock(); |
| 129 | _ = try s.run("INSERT INTO lists (token_hash) VALUES (?)", .{token_hash}); |
| 130 | return c.sqlite3_last_insert_rowid(s.db); |
| 131 | } |
| 132 | |
| 133 | /// Every todo in the list, oldest first, copied into `arena`. |
| 134 | pub fn todos(s: *Store, arena: Allocator, list: i64) (Error || Allocator.Error)![]Todo { |
| 135 | s.lock(); |
| 136 | defer s.unlock(); |
| 137 | var st = try s.prepare("SELECT id, title, completed FROM todos WHERE list_id = ? ORDER BY id"); |
| 138 | defer st.finalize(); |
| 139 | try st.bindAll(.{list}); |
| 140 | var out: std.ArrayList(Todo) = .empty; |
| 141 | while (try st.step()) { |
| 142 | try out.append(arena, .{ |
| 143 | .id = st.int(0), |
| 144 | .title = try arena.dupe(u8, st.text(1)), |
| 145 | .completed = st.int(2) != 0, |
| 146 | }); |
| 147 | } |
| 148 | return out.items; |
| 149 | } |
| 150 | |
| 151 | pub fn add(s: *Store, list: i64, title: []const u8) Error!void { |
| 152 | s.lock(); |
| 153 | defer s.unlock(); |
| 154 | _ = try s.run("INSERT INTO todos (list_id, title) VALUES (?, ?)", .{ list, title }); |
| 155 | } |
| 156 | |
| 157 | /// Each of these answers false when `id` isn't in `list`. |
| 158 | pub fn toggle(s: *Store, list: i64, id: i64) Error!bool { |
| 159 | s.lock(); |
| 160 | defer s.unlock(); |
| 161 | return try s.run("UPDATE todos SET completed = 1 - completed WHERE id = ? AND list_id = ?", .{ id, list }) > 0; |
| 162 | } |
| 163 | |
| 164 | pub fn rename(s: *Store, list: i64, id: i64, title: []const u8) Error!bool { |
| 165 | s.lock(); |
| 166 | defer s.unlock(); |
| 167 | return try s.run("UPDATE todos SET title = ? WHERE id = ? AND list_id = ?", .{ title, id, list }) > 0; |
| 168 | } |
| 169 | |
| 170 | pub fn destroy(s: *Store, list: i64, id: i64) Error!bool { |
| 171 | s.lock(); |
| 172 | defer s.unlock(); |
| 173 | return try s.run("DELETE FROM todos WHERE id = ? AND list_id = ?", .{ id, list }) > 0; |
| 174 | } |
| 175 | |
| 176 | /// Completes every item, or, if all already were, makes them all active. |
| 177 | pub fn toggleAll(s: *Store, list: i64) Error!void { |
| 178 | s.lock(); |
| 179 | defer s.unlock(); |
| 180 | _ = try s.run( |
| 181 | \\UPDATE todos SET completed = |
| 182 | \\ EXISTS (SELECT 1 FROM todos WHERE list_id = ?1 AND completed = 0) |
| 183 | \\WHERE list_id = ?1 |
| 184 | , .{list}); |
| 185 | } |
| 186 | |
| 187 | pub fn clearCompleted(s: *Store, list: i64) Error!void { |
| 188 | s.lock(); |
| 189 | defer s.unlock(); |
| 190 | _ = try s.run("DELETE FROM todos WHERE list_id = ? AND completed = 1", .{list}); |
| 191 | } |
| 192 | }; |
| 193 | |
| 194 | const Stmt = struct { |
| 195 | stmt: *c.sqlite3_stmt, |
| 196 | db: *c.sqlite3, |
| 197 | |
| 198 | fn finalize(st: *Stmt) void { |
| 199 | _ = c.sqlite3_finalize(st.stmt); |
| 200 | } |
| 201 | |
| 202 | /// Binds `args` in order. Text is bound without copying, so it must |
| 203 | /// outlive the statement, which every caller here guarantees. |
| 204 | fn bindAll(st: *Stmt, args: anytype) Error!void { |
| 205 | inline for (args, 1..) |arg, i| { |
| 206 | const rc = switch (@TypeOf(arg)) { |
| 207 | i64 => c.sqlite3_bind_int64(st.stmt, i, arg), |
| 208 | []const u8, []u8 => c.sqlite3_bind_text(st.stmt, i, arg.ptr, @intCast(arg.len), null), |
| 209 | else => @compileError("can't bind " ++ @typeName(@TypeOf(arg))), |
| 210 | }; |
| 211 | if (rc != c.SQLITE_OK) return error.Sqlite; |
| 212 | } |
| 213 | } |
| 214 | |
| 215 | fn step(st: *Stmt) Error!bool { |
| 216 | return switch (c.sqlite3_step(st.stmt)) { |
| 217 | c.SQLITE_ROW => true, |
| 218 | c.SQLITE_DONE => false, |
| 219 | else => { |
| 220 | std.log.err("sqlite step: {s}", .{c.sqlite3_errmsg(st.db)}); |
| 221 | return error.Sqlite; |
| 222 | }, |
| 223 | }; |
| 224 | } |
| 225 | |
| 226 | fn int(st: *Stmt, col: c_int) i64 { |
| 227 | return c.sqlite3_column_int64(st.stmt, col); |
| 228 | } |
| 229 | |
| 230 | fn text(st: *Stmt, col: c_int) []const u8 { |
| 231 | const ptr = c.sqlite3_column_text(st.stmt, col) orelse return ""; |
| 232 | return ptr[0..@intCast(c.sqlite3_column_bytes(st.stmt, col))]; |
| 233 | } |
| 234 | }; |
| 235 | |