carlos / todomvc-bakeoff Public

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

Plain git — no account needed to clone.

Download

Download this file

1import SQLiteNIO
2
3/// One todo item as the page shows it.
4public struct Todo: Sendable, Equatable {
5 public var id: Int
6 public var title: String
7 public var completed: Bool
8}
9
10/// The app's SQLite database: one file, WAL mode, one connection.
11///
12/// SQLiteNIO runs every call on its thread pool, and the one connection
13/// serialises them. Each write below is a single SQL statement, so two
14/// requests can never interleave half a change, and no transaction has to
15/// span an `await`.
16public final class Store: Sendable {
17 let db: SQLiteConnection
18
19 /// Schema changes, in order. Only ever append to this list; never edit
20 /// or remove an entry, and never drop or rewrite data.
21 /// `PRAGMA user_version` records how many have run.
22 static let migrations: [String] = [
23 """
24 CREATE TABLE lists (
25 id INTEGER PRIMARY KEY,
26 token TEXT NOT NULL UNIQUE,
27 created_at INTEGER NOT NULL DEFAULT (unixepoch())
28 );
29 CREATE TABLE todos (
30 id INTEGER PRIMARY KEY AUTOINCREMENT,
31 list_id INTEGER NOT NULL REFERENCES lists(id),
32 title TEXT NOT NULL,
33 completed INTEGER NOT NULL DEFAULT 0,
34 created_at INTEGER NOT NULL DEFAULT (unixepoch())
35 );
36 CREATE INDEX todos_by_list ON todos(list_id, id);
37 """,
38 ]
39
40 init(db: SQLiteConnection) {
41 self.db = db
42 }
43
44 /// Opens (creating if needed) the database at `path` and brings its
45 /// schema up to date.
46 public static func open(path: String) async throws -> Store {
47 let db = try await SQLiteConnection.open(storage: .file(path: path))
48 let store = Store(db: db)
49 do {
50 try await store.prepare()
51 } catch {
52 try? await db.close()
53 throw error
54 }
55 return store
56 }
57
58 func prepare() async throws {
59 _ = try await db.query("PRAGMA journal_mode = WAL")
60 _ = try await db.query("PRAGMA synchronous = NORMAL")
61 _ = try await db.query("PRAGMA busy_timeout = 5000")
62 _ = try await db.query("PRAGMA foreign_keys = ON")
63 let version = try await db.query("PRAGMA user_version").first?.column("user_version")?.integer ?? 0
64 for (index, sql) in Self.migrations.enumerated() where index >= version {
65 // The migration and its version bump commit together, or not at all.
66 try await db.withConnection { conn in
67 _ = try await conn.query("BEGIN IMMEDIATE")
68 do {
69 for statement in sql.split(separator: ";").map({ $0.trimmingWhitespace() }) where !statement.isEmpty {
70 _ = try await conn.query(String(statement))
71 }
72 _ = try await conn.query("PRAGMA user_version = \(index + 1)")
73 _ = try await conn.query("COMMIT")
74 } catch {
75 _ = try? await conn.query("ROLLBACK")
76 throw error
77 }
78 }
79 }
80 }
81
82 public func close() async throws {
83 try await db.close()
84 }
85
86 // MARK: Lists
87
88 /// The list a cookie token names, if the app ever issued it.
89 public func list(token: String) async throws -> Int? {
90 try await db.query("SELECT id FROM lists WHERE token = ?", [.text(token)])
91 .first?.column("id")?.integer
92 }
93
94 /// Records a new, empty list under `token` and returns its id.
95 public func createList(token: String) async throws -> Int {
96 let rows = try await db.query("INSERT INTO lists (token) VALUES (?) RETURNING id", [.text(token)])
97 guard let id = rows.first?.column("id")?.integer else { throw StoreError.noRowReturned }
98 return id
99 }
100
101 // MARK: Todos
102
103 public func todos(list: Int) async throws -> [Todo] {
104 try await db.query(
105 "SELECT id, title, completed FROM todos WHERE list_id = ? ORDER BY id",
106 [.integer(list)]
107 ).map { row in
108 Todo(
109 id: row.column("id")?.integer ?? 0,
110 title: row.column("title")?.string ?? "",
111 completed: row.column("completed")?.bool ?? false
112 )
113 }
114 }
115
116 public func add(list: Int, title: String) async throws {
117 _ = try await db.query("INSERT INTO todos (list_id, title) VALUES (?, ?)", [.integer(list), .text(title)])
118 }
119
120 /// Flips one item. False if the list has no such item.
121 public func toggle(list: Int, id: Int) async throws -> Bool {
122 try await changed(
123 "UPDATE todos SET completed = 1 - completed WHERE id = ? AND list_id = ? RETURNING id",
124 [.integer(id), .integer(list)]
125 )
126 }
127
128 /// Renames one item. False if the list has no such item.
129 public func rename(list: Int, id: Int, title: String) async throws -> Bool {
130 try await changed(
131 "UPDATE todos SET title = ? WHERE id = ? AND list_id = ? RETURNING id",
132 [.text(title), .integer(id), .integer(list)]
133 )
134 }
135
136 /// Deletes one item. False if the list has no such item.
137 public func destroy(list: Int, id: Int) async throws -> Bool {
138 try await changed(
139 "DELETE FROM todos WHERE id = ? AND list_id = ? RETURNING id",
140 [.integer(id), .integer(list)]
141 )
142 }
143
144 /// Completes every item, or, if every item already is, reopens them all.
145 public func toggleAll(list: Int) async throws {
146 _ = try await db.query(
147 """
148 UPDATE todos
149 SET completed = EXISTS (SELECT 1 FROM todos WHERE list_id = ?1 AND completed = 0)
150 WHERE list_id = ?1
151 """,
152 [.integer(list)]
153 )
154 }
155
156 public func clearCompleted(list: Int) async throws {
157 _ = try await db.query("DELETE FROM todos WHERE list_id = ? AND completed = 1", [.integer(list)])
158 }
159
160 private func changed(_ sql: String, _ binds: [SQLiteData]) async throws -> Bool {
161 try await !db.query(sql, binds).isEmpty
162 }
163}
164
165enum StoreError: Error {
166 case noRowReturned
167}
168
169extension Substring {
170 func trimmingWhitespace() -> Substring {
171 var s = self
172 while let c = s.first, c.isWhitespace { s.removeFirst() }
173 while let c = s.last, c.isWhitespace { s.removeLast() }
174 return s
175 }
176}
177