search for standard sites pub-search.waow.tech
search zig blog atproto
11
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor Client.zig for idiomatic zig patterns

- add module docstring with turso api link
- define request types as structs for json serialization
- use jw.write(struct) instead of verbose begin/end calls
- extract validateArgs function
- remove redundant comments (compile time is implied)

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

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

zzstoatzz 9af6ef29 6481db35

+66 -115
+66 -115
backend/src/db/Client.zig
··· 1 + //! Turso HTTP API client 2 + //! 3 + //! Turso pipeline API: https://docs.turso.tech/sdk/http/reference 4 + 1 5 const std = @import("std"); 2 6 const http = std.http; 3 7 const json = std.json; ··· 11 15 12 16 const Client = @This(); 13 17 18 + // Turso request types (mirrors API format) 19 + const TursoArg = struct { type: []const u8 = "text", value: []const u8 }; 20 + const TursoStmt = struct { sql: []const u8, args: ?[]const TursoArg = null }; 21 + const ExecuteRequest = struct { type: []const u8 = "execute", stmt: TursoStmt }; 22 + const CloseRequest = struct { type: []const u8 = "close" }; 23 + 14 24 const URL_BUF_SIZE = 512; 15 25 const AUTH_BUF_SIZE = 512; 16 26 17 - // fields 18 27 allocator: Allocator, 19 28 url: []const u8, 20 29 token: []const u8, ··· 31 40 return error.MissingEnv; 32 41 }; 33 42 34 - // strip libsql:// prefix if present 35 43 const libsql_prefix = "libsql://"; 36 44 const host = if (mem.startsWith(u8, url, libsql_prefix)) 37 45 url[libsql_prefix.len..] ··· 52 60 self.http_client.deinit(); 53 61 } 54 62 55 - /// Execute a query and return parsed results. 56 - /// Validates parameter count at compile time. 57 63 pub fn query(self: *Client, comptime sql: []const u8, args: anytype) !Result { 58 - const expected = comptime countPlaceholders(sql); 59 - const provided = comptime countArgsType(@TypeOf(args)); 60 - if (expected != provided) { 61 - @compileError(std.fmt.comptimePrint( 62 - "SQL has {} placeholders but {} args provided", 63 - .{ expected, provided }, 64 - )); 65 - } 64 + comptime validateArgs(sql, @TypeOf(args)); 66 65 const args_slice = try self.argsToSlice(args); 67 66 defer self.allocator.free(args_slice); 68 67 const response = try self.executeRaw(sql, args_slice); ··· 70 69 return Result.parse(self.allocator, response); 71 70 } 72 71 73 - /// Execute a statement, ignoring results. 74 - /// Validates parameter count at compile time. 75 72 pub fn exec(self: *Client, comptime sql: []const u8, args: anytype) !void { 76 - const expected = comptime countPlaceholders(sql); 77 - const provided = comptime countArgsType(@TypeOf(args)); 78 - if (expected != provided) { 79 - @compileError(std.fmt.comptimePrint( 80 - "SQL has {} placeholders but {} args provided", 81 - .{ expected, provided }, 82 - )); 83 - } 73 + comptime validateArgs(sql, @TypeOf(args)); 84 74 const args_slice = try self.argsToSlice(args); 85 75 defer self.allocator.free(args_slice); 86 76 const response = try self.executeRaw(sql, args_slice); 87 77 self.allocator.free(response); 88 78 } 89 79 90 - /// Statement for batch queries 91 80 pub const Statement = struct { 92 81 sql: []const u8, 93 82 args: []const []const u8 = &.{}, 94 83 }; 95 84 96 - /// Execute multiple queries in a single HTTP request 97 85 pub fn queryBatch(self: *Client, statements: []const Statement) !BatchResult { 98 86 const response = try self.executeBatchRaw(statements); 99 87 defer self.allocator.free(response); 100 88 return BatchResult.parse(self.allocator, response, statements.len); 101 89 } 102 90 103 - /// Convert tuple/struct args to slice, with comptime validation 104 91 fn argsToSlice(self: *Client, args: anytype) ![]const []const u8 { 105 92 const ArgsType = @TypeOf(args); 106 - const args_type_info = @typeInfo(ArgsType); 93 + const info = @typeInfo(ArgsType); 107 94 108 - // handle pointer to tuple (e.g., &.{a, b, c}) 109 - if (args_type_info == .pointer) { 110 - const child_info = @typeInfo(args_type_info.pointer.child); 111 - if (child_info == .@"struct") { 112 - const fields = child_info.@"struct".fields; 95 + if (info == .pointer) { 96 + const child = @typeInfo(info.pointer.child); 97 + if (child == .@"struct") { 98 + const fields = child.@"struct".fields; 113 99 const slice = try self.allocator.alloc([]const u8, fields.len); 114 100 inline for (fields, 0..) |field, i| { 115 101 slice[i] = @field(args.*, field.name); ··· 118 104 } 119 105 } 120 106 121 - // handle direct struct/tuple 122 - if (args_type_info == .@"struct") { 123 - const fields = args_type_info.@"struct".fields; 107 + if (info == .@"struct") { 108 + const fields = info.@"struct".fields; 124 109 const slice = try self.allocator.alloc([]const u8, fields.len); 125 110 inline for (fields, 0..) |field, i| { 126 111 slice[i] = @field(args, field.name); ··· 131 116 @compileError("args must be a tuple or pointer to tuple"); 132 117 } 133 118 134 - /// Execute and return raw JSON response (caller owns memory) 135 119 fn executeRaw(self: *Client, sql: []const u8, args: []const []const u8) ![]const u8 { 136 120 self.mutex.lock(); 137 121 defer self.mutex.unlock(); ··· 140 124 const url = std.fmt.bufPrint(&url_buf, "https://{s}/v2/pipeline", .{self.url}) catch 141 125 return error.UrlTooLong; 142 126 143 - // build request body 144 127 const body = try self.buildRequestBody(sql, args); 145 128 defer self.allocator.free(body); 146 129 ··· 173 156 return try response_body.toOwnedSlice(); 174 157 } 175 158 176 - /// Execute batch and return raw JSON response 177 159 fn executeBatchRaw(self: *Client, statements: []const Statement) ![]const u8 { 178 160 self.mutex.lock(); 179 161 defer self.mutex.unlock(); ··· 217 199 fn buildBatchRequestBody(self: *Client, statements: []const Statement) ![]const u8 { 218 200 var body: std.Io.Writer.Allocating = .init(self.allocator); 219 201 errdefer body.deinit(); 220 - 221 202 var jw: json.Stringify = .{ .writer = &body.writer }; 222 203 223 204 try jw.beginObject(); ··· 225 206 try jw.beginArray(); 226 207 227 208 for (statements) |stmt| { 228 - // execute statement 229 - try jw.beginObject(); 230 - try jw.objectField("type"); 231 - try jw.write("execute"); 232 - try jw.objectField("stmt"); 233 - try jw.beginObject(); 234 - try jw.objectField("sql"); 235 - try jw.write(stmt.sql); 236 - 237 - if (stmt.args.len > 0) { 238 - try jw.objectField("args"); 239 - try jw.beginArray(); 240 - for (stmt.args) |arg| { 241 - try jw.beginObject(); 242 - try jw.objectField("type"); 243 - try jw.write("text"); 244 - try jw.objectField("value"); 245 - try jw.write(arg); 246 - try jw.endObject(); 247 - } 248 - try jw.endArray(); 249 - } 250 - 251 - try jw.endObject(); // stmt 252 - try jw.endObject(); // execute request 209 + const turso_args = try self.toTursoArgs(stmt.args); 210 + defer self.allocator.free(turso_args); 211 + try jw.write(ExecuteRequest{ 212 + .stmt = .{ 213 + .sql = stmt.sql, 214 + .args = if (turso_args.len > 0) turso_args else null, 215 + }, 216 + }); 253 217 } 254 218 255 - // single close at the end 256 - try jw.beginObject(); 257 - try jw.objectField("type"); 258 - try jw.write("close"); 219 + try jw.write(CloseRequest{}); 220 + try jw.endArray(); 259 221 try jw.endObject(); 260 222 261 - try jw.endArray(); // requests 262 - try jw.endObject(); // root 263 - 264 223 return try body.toOwnedSlice(); 265 224 } 266 225 267 226 fn buildRequestBody(self: *Client, sql: []const u8, args: []const []const u8) ![]const u8 { 268 227 var body: std.Io.Writer.Allocating = .init(self.allocator); 269 228 errdefer body.deinit(); 270 - 271 229 var jw: json.Stringify = .{ .writer = &body.writer }; 230 + 231 + const turso_args = try self.toTursoArgs(args); 232 + defer self.allocator.free(turso_args); 272 233 273 234 try jw.beginObject(); 274 235 try jw.objectField("requests"); 275 236 try jw.beginArray(); 276 - 277 - // execute statement 278 - try jw.beginObject(); 279 - try jw.objectField("type"); 280 - try jw.write("execute"); 281 - try jw.objectField("stmt"); 282 - try jw.beginObject(); 283 - try jw.objectField("sql"); 284 - try jw.write(sql); 285 - 286 - if (args.len > 0) { 287 - try jw.objectField("args"); 288 - try jw.beginArray(); 289 - for (args) |arg| { 290 - try jw.beginObject(); 291 - try jw.objectField("type"); 292 - try jw.write("text"); 293 - try jw.objectField("value"); 294 - try jw.write(arg); 295 - try jw.endObject(); 296 - } 297 - try jw.endArray(); 298 - } 299 - 300 - try jw.endObject(); // stmt 301 - try jw.endObject(); // execute request 302 - 303 - // close statement 304 - try jw.beginObject(); 305 - try jw.objectField("type"); 306 - try jw.write("close"); 237 + try jw.write(ExecuteRequest{ 238 + .stmt = .{ 239 + .sql = sql, 240 + .args = if (turso_args.len > 0) turso_args else null, 241 + }, 242 + }); 243 + try jw.write(CloseRequest{}); 244 + try jw.endArray(); 307 245 try jw.endObject(); 308 246 309 - try jw.endArray(); // requests 310 - try jw.endObject(); // root 247 + return try body.toOwnedSlice(); 248 + } 311 249 312 - return try body.toOwnedSlice(); 250 + fn toTursoArgs(self: *Client, args: []const []const u8) ![]const TursoArg { 251 + if (args.len == 0) return &.{}; 252 + const turso_args = try self.allocator.alloc(TursoArg, args.len); 253 + for (args, 0..) |arg, i| { 254 + turso_args[i] = .{ .value = arg }; 255 + } 256 + return turso_args; 313 257 } 314 258 315 - // module-level helpers (don't need self) 259 + fn validateArgs(comptime sql: []const u8, comptime ArgsType: type) void { 260 + const expected = countPlaceholders(sql); 261 + const provided = countArgsType(ArgsType); 262 + if (expected != provided) { 263 + @compileError(std.fmt.comptimePrint( 264 + "SQL has {} placeholders but {} args provided", 265 + .{ expected, provided }, 266 + )); 267 + } 268 + } 316 269 317 - /// Count `?` placeholders in SQL at comptime 318 270 fn countPlaceholders(comptime sql: []const u8) usize { 319 271 var count: usize = 0; 320 272 for (sql) |c| { ··· 323 275 return count; 324 276 } 325 277 326 - /// Count args in a tuple type (handles both direct tuples and pointers to tuples) 327 278 fn countArgsType(comptime ArgsType: type) usize { 328 - const args_type_info = @typeInfo(ArgsType); 279 + const info = @typeInfo(ArgsType); 329 280 330 - if (args_type_info == .pointer) { 331 - const child_info = @typeInfo(args_type_info.pointer.child); 332 - if (child_info == .@"struct") { 333 - return child_info.@"struct".fields.len; 281 + if (info == .pointer) { 282 + const child = @typeInfo(info.pointer.child); 283 + if (child == .@"struct") { 284 + return child.@"struct".fields.len; 334 285 } 335 286 } 336 287 337 - if (args_type_info == .@"struct") { 338 - return args_type_info.@"struct".fields.len; 288 + if (info == .@"struct") { 289 + return info.@"struct".fields.len; 339 290 } 340 291 341 292 return 0;