carlos / todomvc-bakeoff Public

Clone
git clone https://amadan.net/carlos/todomvc-bakeoff

Plain git — no account needed to clone.

Download

Download this file

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
5const std = @import("std");
6const c = @import("sqlite");
7const Io = std.Io;
8const Allocator = std.mem.Allocator;
9
10pub const Error = error{Sqlite};
11
12pub 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.
19const 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
35pub 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
194const 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