prefect server in zig

implement variables API

full CRUD for variables with endpoints:
- POST /api/variables/ (create)
- POST /api/variables/filter (list)
- POST /api/variables/count
- GET /api/variables/{id}
- GET /api/variables/name/{name}
- PATCH /api/variables/{id}
- PATCH /api/variables/name/{name}
- DELETE /api/variables/{id}
- DELETE /api/variables/name/{name}

adds variable table to both sqlite and postgres schemas with:
- id, name (unique), value (json), tags (json array)
- created/updated timestamps

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+604 -9
+16 -9
ROADMAP.md
··· 83 83 - [ ] POST /api/automations/filter 84 84 85 85 ### variables 86 - - [ ] POST /api/variables/ 87 - - [ ] POST /api/variables/filter 86 + - [x] POST /api/variables/ 87 + - [x] POST /api/variables/filter 88 + - [x] POST /api/variables/count 89 + - [x] GET /api/variables/{id} 90 + - [x] GET /api/variables/name/{name} 91 + - [x] PATCH /api/variables/{id} 92 + - [x] PATCH /api/variables/name/{name} 93 + - [x] DELETE /api/variables/{id} 94 + - [x] DELETE /api/variables/name/{name} 88 95 89 96 ### events 90 97 - [ ] POST /api/events/filter ··· 140 147 - [ ] concurrency_limit table 141 148 - [ ] artifact table 142 149 - [ ] automation table 143 - - [ ] variable table 150 + - [x] variable table 144 151 - [ ] log table (currently in-memory only) 145 152 146 153 ### database backends ··· 165 172 ## notes 166 173 167 174 priority order for next work: 168 - 1. variables (simple CRUD, useful standalone, exercises db layer) 169 - 2. deployments (needed for scheduled/triggered runs) 170 - 3. work_pools (needed for worker-based execution) 171 - 4. scheduler service (needed for deployment schedules) 175 + 1. deployments (needed for scheduled/triggered runs) 176 + 2. work_pools (needed for worker-based execution) 177 + 3. scheduler service (needed for deployment schedules) 172 178 173 - ### what's working (6.5x faster than python, 3.8x less memory) 179 + ### what's working (6.5x faster than python) 174 180 - flow/flow_run/task_run lifecycle 175 181 - blocks (types, schemas, documents) 176 - - events (ingest via websocket, persist, broadcast) 182 + - variables (full CRUD) 183 + - events (ingest via websocket, persist, broadcast with backfill) 177 184 - dual database backends (sqlite/postgres) 178 185 - dual message brokers (memory/redis)
+3
src/api/routes.zig
··· 10 10 pub const block_types = @import("block_types.zig"); 11 11 pub const block_schemas = @import("block_schemas.zig"); 12 12 pub const block_documents = @import("block_documents.zig"); 13 + pub const variables = @import("variables.zig"); 13 14 14 15 pub fn handle(r: zap.Request) !void { 15 16 const target = r.path orelse "/"; ··· 42 43 try block_schemas.handle(r); 43 44 } else if (std.mem.startsWith(u8, target, "/api/block_documents") or std.mem.startsWith(u8, target, "/block_documents")) { 44 45 try block_documents.handle(r); 46 + } else if (std.mem.startsWith(u8, target, "/api/variables") or std.mem.startsWith(u8, target, "/variables")) { 47 + try variables.handle(r); 45 48 } else { 46 49 try sendNotFound(r); 47 50 }
+413
src/api/variables.zig
··· 1 + const std = @import("std"); 2 + const zap = @import("zap"); 3 + const mem = std.mem; 4 + const json = std.json; 5 + 6 + const db = @import("../db/sqlite.zig"); 7 + const uuid_util = @import("../utilities/uuid.zig"); 8 + const time_util = @import("../utilities/time.zig"); 9 + const json_util = @import("../utilities/json.zig"); 10 + 11 + pub fn handle(r: zap.Request) !void { 12 + const target = r.path orelse "/"; 13 + const method = r.method orelse "GET"; 14 + 15 + // POST /variables/filter - list 16 + if (mem.eql(u8, method, "POST") and mem.endsWith(u8, target, "/filter")) { 17 + try filter(r); 18 + return; 19 + } 20 + 21 + // POST /variables/count 22 + if (mem.eql(u8, method, "POST") and mem.endsWith(u8, target, "/count")) { 23 + try countVariables(r); 24 + return; 25 + } 26 + 27 + // POST /variables/ - create 28 + if (mem.eql(u8, method, "POST") and (mem.eql(u8, target, "/variables/") or mem.eql(u8, target, "/api/variables/"))) { 29 + try createVariable(r); 30 + return; 31 + } 32 + 33 + // GET /variables/name/{name} 34 + if (mem.eql(u8, method, "GET") and mem.indexOf(u8, target, "/name/") != null) { 35 + const name = extractName(target) orelse { 36 + json_util.sendStatus(r, "{\"detail\":\"name required\"}", .bad_request); 37 + return; 38 + }; 39 + try getByName(r, name); 40 + return; 41 + } 42 + 43 + // PATCH /variables/name/{name} 44 + if (mem.eql(u8, method, "PATCH") and mem.indexOf(u8, target, "/name/") != null) { 45 + const name = extractName(target) orelse { 46 + json_util.sendStatus(r, "{\"detail\":\"name required\"}", .bad_request); 47 + return; 48 + }; 49 + try updateByName(r, name); 50 + return; 51 + } 52 + 53 + // DELETE /variables/name/{name} 54 + if (mem.eql(u8, method, "DELETE") and mem.indexOf(u8, target, "/name/") != null) { 55 + const name = extractName(target) orelse { 56 + json_util.sendStatus(r, "{\"detail\":\"name required\"}", .bad_request); 57 + return; 58 + }; 59 + try deleteByName(r, name); 60 + return; 61 + } 62 + 63 + // GET /variables/{id} 64 + if (mem.eql(u8, method, "GET")) { 65 + const id = extractId(target) orelse { 66 + json_util.sendStatus(r, "{\"detail\":\"variable id required\"}", .bad_request); 67 + return; 68 + }; 69 + try getById(r, id); 70 + return; 71 + } 72 + 73 + // PATCH /variables/{id} 74 + if (mem.eql(u8, method, "PATCH")) { 75 + const id = extractId(target) orelse { 76 + json_util.sendStatus(r, "{\"detail\":\"variable id required\"}", .bad_request); 77 + return; 78 + }; 79 + try updateById(r, id); 80 + return; 81 + } 82 + 83 + // DELETE /variables/{id} 84 + if (mem.eql(u8, method, "DELETE")) { 85 + const id = extractId(target) orelse { 86 + json_util.sendStatus(r, "{\"detail\":\"variable id required\"}", .bad_request); 87 + return; 88 + }; 89 + try deleteById(r, id); 90 + return; 91 + } 92 + 93 + json_util.sendStatus(r, "{\"detail\":\"not implemented\"}", .not_implemented); 94 + } 95 + 96 + fn extractId(target: []const u8) ?[]const u8 { 97 + const prefix = if (mem.startsWith(u8, target, "/api/variables/")) "/api/variables/" else "/variables/"; 98 + if (target.len > prefix.len) { 99 + return target[prefix.len..]; 100 + } 101 + return null; 102 + } 103 + 104 + fn extractName(target: []const u8) ?[]const u8 { 105 + const idx = mem.indexOf(u8, target, "/name/") orelse return null; 106 + const start = idx + "/name/".len; 107 + if (start < target.len) { 108 + return target[start..]; 109 + } 110 + return null; 111 + } 112 + 113 + fn createVariable(r: zap.Request) !void { 114 + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 115 + defer arena.deinit(); 116 + const alloc = arena.allocator(); 117 + 118 + const body = r.body orelse { 119 + json_util.sendStatus(r, "{\"detail\":\"failed to read body\"}", .bad_request); 120 + return; 121 + }; 122 + 123 + const parsed = json.parseFromSlice(json.Value, alloc, body, .{}) catch { 124 + json_util.sendStatus(r, "{\"detail\":\"invalid json\"}", .bad_request); 125 + return; 126 + }; 127 + 128 + const obj = parsed.value.object; 129 + 130 + const name = switch (obj.get("name") orelse { 131 + json_util.sendStatus(r, "{\"detail\":\"name required\"}", .bad_request); 132 + return; 133 + }) { 134 + .string => |s| s, 135 + else => { 136 + json_util.sendStatus(r, "{\"detail\":\"name must be string\"}", .bad_request); 137 + return; 138 + }, 139 + }; 140 + 141 + // check for existing 142 + if (db.variables.getByName(alloc, name) catch null) |_| { 143 + json_util.sendStatus(r, "{\"detail\":\"Variable with this name already exists\"}", .conflict); 144 + return; 145 + } 146 + 147 + const value_json = stringifyField(alloc, obj.get("value"), "null"); 148 + const tags_json = stringifyField(alloc, obj.get("tags"), "[]"); 149 + 150 + var new_id_buf: [36]u8 = undefined; 151 + const new_id = uuid_util.generate(&new_id_buf); 152 + 153 + db.variables.insert(new_id, name, value_json, tags_json) catch { 154 + json_util.sendStatus(r, "{\"detail\":\"insert failed\"}", .internal_server_error); 155 + return; 156 + }; 157 + 158 + var ts_buf: [32]u8 = undefined; 159 + const now = time_util.timestamp(&ts_buf); 160 + 161 + const variable = db.variables.VariableRow{ 162 + .id = new_id, 163 + .created = now, 164 + .updated = now, 165 + .name = name, 166 + .value = value_json, 167 + .tags = tags_json, 168 + }; 169 + 170 + const resp = writeVariable(alloc, variable) catch { 171 + json_util.sendStatus(r, "{\"detail\":\"serialize error\"}", .internal_server_error); 172 + return; 173 + }; 174 + json_util.sendStatus(r, resp, .created); 175 + } 176 + 177 + fn getById(r: zap.Request, id: []const u8) !void { 178 + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 179 + defer arena.deinit(); 180 + const alloc = arena.allocator(); 181 + 182 + if (db.variables.getById(alloc, id) catch null) |variable| { 183 + const resp = writeVariable(alloc, variable) catch { 184 + json_util.sendStatus(r, "{\"detail\":\"serialize error\"}", .internal_server_error); 185 + return; 186 + }; 187 + json_util.send(r, resp); 188 + } else { 189 + json_util.sendStatus(r, "{\"detail\":\"Variable not found\"}", .not_found); 190 + } 191 + } 192 + 193 + fn getByName(r: zap.Request, name: []const u8) !void { 194 + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 195 + defer arena.deinit(); 196 + const alloc = arena.allocator(); 197 + 198 + if (db.variables.getByName(alloc, name) catch null) |variable| { 199 + const resp = writeVariable(alloc, variable) catch { 200 + json_util.sendStatus(r, "{\"detail\":\"serialize error\"}", .internal_server_error); 201 + return; 202 + }; 203 + json_util.send(r, resp); 204 + } else { 205 + json_util.sendStatus(r, "{\"detail\":\"Variable not found\"}", .not_found); 206 + } 207 + } 208 + 209 + fn updateById(r: zap.Request, id: []const u8) !void { 210 + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 211 + defer arena.deinit(); 212 + const alloc = arena.allocator(); 213 + 214 + const body = r.body orelse { 215 + json_util.sendStatus(r, "{\"detail\":\"failed to read body\"}", .bad_request); 216 + return; 217 + }; 218 + 219 + const parsed = json.parseFromSlice(json.Value, alloc, body, .{}) catch { 220 + json_util.sendStatus(r, "{\"detail\":\"invalid json\"}", .bad_request); 221 + return; 222 + }; 223 + 224 + const obj = parsed.value.object; 225 + const new_name = getOptionalString(obj.get("name")); 226 + const value_json = stringifyFieldOptional(alloc, obj.get("value")); 227 + const tags_json = stringifyFieldOptional(alloc, obj.get("tags")); 228 + 229 + const updated = db.variables.updateById(id, new_name, value_json, tags_json) catch { 230 + json_util.sendStatus(r, "{\"detail\":\"update failed\"}", .internal_server_error); 231 + return; 232 + }; 233 + 234 + if (!updated) { 235 + json_util.sendStatus(r, "{\"detail\":\"Variable not found\"}", .not_found); 236 + return; 237 + } 238 + 239 + r.setStatus(.no_content); 240 + r.sendBody("") catch {}; 241 + } 242 + 243 + fn updateByName(r: zap.Request, name: []const u8) !void { 244 + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 245 + defer arena.deinit(); 246 + const alloc = arena.allocator(); 247 + 248 + const body = r.body orelse { 249 + json_util.sendStatus(r, "{\"detail\":\"failed to read body\"}", .bad_request); 250 + return; 251 + }; 252 + 253 + const parsed = json.parseFromSlice(json.Value, alloc, body, .{}) catch { 254 + json_util.sendStatus(r, "{\"detail\":\"invalid json\"}", .bad_request); 255 + return; 256 + }; 257 + 258 + const obj = parsed.value.object; 259 + const new_name = getOptionalString(obj.get("name")); 260 + const value_json = stringifyFieldOptional(alloc, obj.get("value")); 261 + const tags_json = stringifyFieldOptional(alloc, obj.get("tags")); 262 + 263 + const updated = db.variables.updateByName(name, new_name, value_json, tags_json) catch { 264 + json_util.sendStatus(r, "{\"detail\":\"update failed\"}", .internal_server_error); 265 + return; 266 + }; 267 + 268 + if (!updated) { 269 + json_util.sendStatus(r, "{\"detail\":\"Variable not found\"}", .not_found); 270 + return; 271 + } 272 + 273 + r.setStatus(.no_content); 274 + r.sendBody("") catch {}; 275 + } 276 + 277 + fn deleteById(r: zap.Request, id: []const u8) !void { 278 + const deleted = db.variables.deleteById(id) catch { 279 + json_util.sendStatus(r, "{\"detail\":\"delete failed\"}", .internal_server_error); 280 + return; 281 + }; 282 + 283 + if (!deleted) { 284 + json_util.sendStatus(r, "{\"detail\":\"Variable not found\"}", .not_found); 285 + return; 286 + } 287 + 288 + r.setStatus(.no_content); 289 + r.sendBody("") catch {}; 290 + } 291 + 292 + fn deleteByName(r: zap.Request, name: []const u8) !void { 293 + const deleted = db.variables.deleteByName(name) catch { 294 + json_util.sendStatus(r, "{\"detail\":\"delete failed\"}", .internal_server_error); 295 + return; 296 + }; 297 + 298 + if (!deleted) { 299 + json_util.sendStatus(r, "{\"detail\":\"Variable not found\"}", .not_found); 300 + return; 301 + } 302 + 303 + r.setStatus(.no_content); 304 + r.sendBody("") catch {}; 305 + } 306 + 307 + fn filter(r: zap.Request) !void { 308 + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 309 + defer arena.deinit(); 310 + const alloc = arena.allocator(); 311 + 312 + var limit: usize = 200; 313 + var offset: usize = 0; 314 + 315 + if (r.body) |body| { 316 + if (json.parseFromSlice(json.Value, alloc, body, .{})) |parsed| { 317 + const obj = parsed.value.object; 318 + if (obj.get("limit")) |v| { 319 + if (v == .integer) limit = @intCast(v.integer); 320 + } 321 + if (obj.get("offset")) |v| { 322 + if (v == .integer) offset = @intCast(v.integer); 323 + } 324 + } else |_| {} 325 + } 326 + 327 + const variables = db.variables.list(alloc, limit, offset) catch { 328 + json_util.sendStatus(r, "{\"detail\":\"database error\"}", .internal_server_error); 329 + return; 330 + }; 331 + 332 + var output: std.io.Writer.Allocating = .init(alloc); 333 + var jw: json.Stringify = .{ .writer = &output.writer }; 334 + 335 + jw.beginArray() catch { 336 + json_util.sendStatus(r, "{\"detail\":\"serialize error\"}", .internal_server_error); 337 + return; 338 + }; 339 + 340 + for (variables) |variable| { 341 + writeVariableObject(&jw, variable) catch continue; 342 + } 343 + 344 + jw.endArray() catch {}; 345 + 346 + json_util.send(r, output.toOwnedSlice() catch "[]"); 347 + } 348 + 349 + fn countVariables(r: zap.Request) !void { 350 + const cnt = db.variables.count() catch 0; 351 + var buf: [32]u8 = undefined; 352 + const resp = std.fmt.bufPrint(&buf, "{d}", .{cnt}) catch "0"; 353 + json_util.send(r, resp); 354 + } 355 + 356 + fn stringifyField(alloc: std.mem.Allocator, val: ?json.Value, default: []const u8) []const u8 { 357 + if (val) |v| { 358 + return std.fmt.allocPrint(alloc, "{f}", .{json.fmt(v, .{})}) catch default; 359 + } 360 + return default; 361 + } 362 + 363 + fn stringifyFieldOptional(alloc: std.mem.Allocator, val: ?json.Value) ?[]const u8 { 364 + if (val) |v| { 365 + return std.fmt.allocPrint(alloc, "{f}", .{json.fmt(v, .{})}) catch null; 366 + } 367 + return null; 368 + } 369 + 370 + fn getOptionalString(val: ?json.Value) ?[]const u8 { 371 + if (val) |v| { 372 + return switch (v) { 373 + .string => |s| s, 374 + else => null, 375 + }; 376 + } 377 + return null; 378 + } 379 + 380 + fn writeVariable(alloc: std.mem.Allocator, variable: db.variables.VariableRow) ![]const u8 { 381 + var output: std.io.Writer.Allocating = .init(alloc); 382 + var jw: json.Stringify = .{ .writer = &output.writer }; 383 + try writeVariableObject(&jw, variable); 384 + return output.toOwnedSlice(); 385 + } 386 + 387 + fn writeVariableObject(jw: *json.Stringify, variable: db.variables.VariableRow) !void { 388 + try jw.beginObject(); 389 + 390 + try jw.objectField("id"); 391 + try jw.write(variable.id); 392 + 393 + try jw.objectField("created"); 394 + try jw.write(variable.created); 395 + 396 + try jw.objectField("updated"); 397 + try jw.write(variable.updated); 398 + 399 + try jw.objectField("name"); 400 + try jw.write(variable.name); 401 + 402 + try jw.objectField("value"); 403 + try jw.beginWriteRaw(); 404 + try jw.writer.writeAll(variable.value); 405 + jw.endWriteRaw(); 406 + 407 + try jw.objectField("tags"); 408 + try jw.beginWriteRaw(); 409 + try jw.writer.writeAll(variable.tags); 410 + jw.endWriteRaw(); 411 + 412 + try jw.endObject(); 413 + }
+13
src/db/schema/postgres.zig
··· 144 144 \\) 145 145 , .{}); 146 146 147 + // variable table 148 + try backend.db.exec( 149 + \\CREATE TABLE IF NOT EXISTS variable ( 150 + \\ id TEXT PRIMARY KEY, 151 + \\ created TEXT DEFAULT NOW()::TEXT, 152 + \\ updated TEXT DEFAULT NOW()::TEXT, 153 + \\ name TEXT NOT NULL UNIQUE, 154 + \\ value JSONB DEFAULT 'null', 155 + \\ tags JSONB DEFAULT '[]' 156 + \\) 157 + , .{}); 158 + 147 159 // indexes 148 160 try backend.db.exec("CREATE INDEX IF NOT EXISTS ix_flow_run__state_type ON flow_run(state_type)", .{}); 149 161 try backend.db.exec("CREATE INDEX IF NOT EXISTS ix_flow_run__flow_id ON flow_run(flow_id)", .{}); ··· 159 171 try backend.db.exec("CREATE INDEX IF NOT EXISTS ix_block_document__block_type_id ON block_document(block_type_id)", .{}); 160 172 try backend.db.exec("CREATE INDEX IF NOT EXISTS ix_block_document__block_schema_id ON block_document(block_schema_id)", .{}); 161 173 try backend.db.exec("CREATE INDEX IF NOT EXISTS ix_block_document__name ON block_document(name)", .{}); 174 + try backend.db.exec("CREATE INDEX IF NOT EXISTS ix_variable__name ON variable(name)", .{}); 162 175 163 176 log.info("database", "postgres schema initialized", .{}); 164 177 }
+13
src/db/schema/sqlite.zig
··· 139 139 \\) 140 140 , .{}); 141 141 142 + // variable table 143 + try backend.db.exec( 144 + \\CREATE TABLE IF NOT EXISTS variable ( 145 + \\ id TEXT PRIMARY KEY, 146 + \\ created TEXT DEFAULT (datetime('now')), 147 + \\ updated TEXT DEFAULT (datetime('now')), 148 + \\ name TEXT NOT NULL UNIQUE, 149 + \\ value TEXT DEFAULT 'null', 150 + \\ tags TEXT DEFAULT '[]' 151 + \\) 152 + , .{}); 153 + 142 154 // indexes 143 155 try backend.db.exec("CREATE INDEX IF NOT EXISTS ix_flow_run__state_type ON flow_run(state_type)", .{}); 144 156 try backend.db.exec("CREATE INDEX IF NOT EXISTS ix_flow_run__flow_id ON flow_run(flow_id)", .{}); ··· 154 166 try backend.db.exec("CREATE INDEX IF NOT EXISTS ix_block_document__block_type_id ON block_document(block_type_id)", .{}); 155 167 try backend.db.exec("CREATE INDEX IF NOT EXISTS ix_block_document__block_schema_id ON block_document(block_schema_id)", .{}); 156 168 try backend.db.exec("CREATE INDEX IF NOT EXISTS ix_block_document__name ON block_document(name)", .{}); 169 + try backend.db.exec("CREATE INDEX IF NOT EXISTS ix_variable__name ON variable(name)", .{}); 157 170 158 171 log.info("database", "sqlite schema initialized", .{}); 159 172 }
+1
src/db/sqlite.zig
··· 14 14 pub const block_types = @import("block_types.zig"); 15 15 pub const block_schemas = @import("block_schemas.zig"); 16 16 pub const block_documents = @import("block_documents.zig"); 17 + pub const variables = @import("variables.zig"); 17 18 18 19 // re-export types for compatibility 19 20 pub const FlowRow = flows.FlowRow;
+145
src/db/variables.zig
··· 1 + const std = @import("std"); 2 + const Allocator = std.mem.Allocator; 3 + 4 + const backend = @import("backend.zig"); 5 + const log = @import("../logging.zig"); 6 + 7 + pub const VariableRow = struct { 8 + id: []const u8, 9 + created: []const u8, 10 + updated: []const u8, 11 + name: []const u8, 12 + value: []const u8, 13 + tags: []const u8, 14 + }; 15 + 16 + const Col = struct { 17 + const id: usize = 0; 18 + const created: usize = 1; 19 + const updated: usize = 2; 20 + const name: usize = 3; 21 + const value: usize = 4; 22 + const tags: usize = 5; 23 + }; 24 + 25 + const select_cols = "id, created, updated, name, value, tags"; 26 + 27 + fn rowFromResult(alloc: Allocator, r: anytype) !VariableRow { 28 + return VariableRow{ 29 + .id = try alloc.dupe(u8, r.text(Col.id)), 30 + .created = try alloc.dupe(u8, r.text(Col.created)), 31 + .updated = try alloc.dupe(u8, r.text(Col.updated)), 32 + .name = try alloc.dupe(u8, r.text(Col.name)), 33 + .value = try alloc.dupe(u8, r.text(Col.value)), 34 + .tags = try alloc.dupe(u8, r.text(Col.tags)), 35 + }; 36 + } 37 + 38 + pub fn getById(alloc: Allocator, id: []const u8) !?VariableRow { 39 + var r = backend.db.row( 40 + "SELECT " ++ select_cols ++ " FROM variable WHERE id = ?", 41 + .{id}, 42 + ) catch return null; 43 + 44 + if (r) |*row| { 45 + defer row.deinit(); 46 + return try rowFromResult(alloc, row); 47 + } 48 + return null; 49 + } 50 + 51 + pub fn getByName(alloc: Allocator, name: []const u8) !?VariableRow { 52 + var r = backend.db.row( 53 + "SELECT " ++ select_cols ++ " FROM variable WHERE name = ?", 54 + .{name}, 55 + ) catch return null; 56 + 57 + if (r) |*row| { 58 + defer row.deinit(); 59 + return try rowFromResult(alloc, row); 60 + } 61 + return null; 62 + } 63 + 64 + pub fn insert(id: []const u8, name: []const u8, value: []const u8, tags: []const u8) !void { 65 + backend.db.exec( 66 + "INSERT INTO variable (id, name, value, tags) VALUES (?, ?, ?, ?)", 67 + .{ id, name, value, tags }, 68 + ) catch |err| { 69 + log.err("database", "insert variable error: {}", .{err}); 70 + return err; 71 + }; 72 + } 73 + 74 + pub fn updateById(id: []const u8, name: ?[]const u8, value: ?[]const u8, tags: ?[]const u8) !bool { 75 + const affected = backend.db.execWithRowCount( 76 + "UPDATE variable SET name = COALESCE(?, name), value = COALESCE(?, value), tags = COALESCE(?, tags), updated = datetime('now') WHERE id = ?", 77 + .{ name, value, tags, id }, 78 + ) catch |err| { 79 + log.err("database", "update variable error: {}", .{err}); 80 + return err; 81 + }; 82 + return affected > 0; 83 + } 84 + 85 + pub fn updateByName(name: []const u8, new_name: ?[]const u8, value: ?[]const u8, tags: ?[]const u8) !bool { 86 + const affected = backend.db.execWithRowCount( 87 + "UPDATE variable SET name = COALESCE(?, name), value = COALESCE(?, value), tags = COALESCE(?, tags), updated = datetime('now') WHERE name = ?", 88 + .{ new_name, value, tags, name }, 89 + ) catch |err| { 90 + log.err("database", "update variable error: {}", .{err}); 91 + return err; 92 + }; 93 + return affected > 0; 94 + } 95 + 96 + pub fn deleteById(id: []const u8) !bool { 97 + const affected = backend.db.execWithRowCount( 98 + "DELETE FROM variable WHERE id = ?", 99 + .{id}, 100 + ) catch |err| { 101 + log.err("database", "delete variable error: {}", .{err}); 102 + return err; 103 + }; 104 + return affected > 0; 105 + } 106 + 107 + pub fn deleteByName(name: []const u8) !bool { 108 + const affected = backend.db.execWithRowCount( 109 + "DELETE FROM variable WHERE name = ?", 110 + .{name}, 111 + ) catch |err| { 112 + log.err("database", "delete variable error: {}", .{err}); 113 + return err; 114 + }; 115 + return affected > 0; 116 + } 117 + 118 + pub fn list(alloc: Allocator, limit: usize, offset: usize) ![]VariableRow { 119 + var results = std.ArrayListUnmanaged(VariableRow){}; 120 + errdefer results.deinit(alloc); 121 + 122 + var rows = backend.db.query( 123 + "SELECT " ++ select_cols ++ " FROM variable ORDER BY name ASC LIMIT ? OFFSET ?", 124 + .{ @as(i64, @intCast(limit)), @as(i64, @intCast(offset)) }, 125 + ) catch |err| { 126 + log.err("database", "list variables error: {}", .{err}); 127 + return err; 128 + }; 129 + defer rows.deinit(); 130 + 131 + while (rows.next()) |r| { 132 + try results.append(alloc, try rowFromResult(alloc, &r)); 133 + } 134 + 135 + return results.toOwnedSlice(alloc); 136 + } 137 + 138 + pub fn count() !usize { 139 + var r = backend.db.row("SELECT COUNT(*) FROM variable", .{}) catch return 0; 140 + if (r) |*row| { 141 + defer row.deinit(); 142 + return @intCast(row.int(0)); 143 + } 144 + return 0; 145 + }