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.

use struct serialization with emit_null_optional_fields

- define Hrana protocol types (Value, Stmt, ExecuteReq, CloseReq)
- use optional args field (?[]const Value) with emit_null_optional_fields=false
- jw.write(struct) instead of manual begin/end calls
- add link to Hrana spec in comments

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

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

zzstoatzz 90c92091 39b3981d

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