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.

Revert "refactor Client.zig for idiomatic zig patterns"

This reverts commit 9af6ef294580de894fdc0abb9706470027db547f.

zzstoatzz 39b3981d 9af6ef29

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