comptime sql bindings for zig
ziglang sql
at main 7.9 kB view raw
1//! Query - comptime sql metadata extraction 2//! 3//! extracts parameter names, column names, and provides 4//! struct mapping utilities at compile time. 5 6const std = @import("std"); 7const parser = @import("parse.zig"); 8 9/// query metadata extracted at comptime 10pub fn Query(comptime sql: []const u8) type { 11 comptime { 12 const parsed = parser.parse(sql); 13 return struct { 14 pub const raw = sql; 15 pub const positional: []const u8 = parsed.positional[0..parsed.positional_len]; 16 pub const param_count = parsed.param_count; 17 pub const params: []const []const u8 = parsed.params[0..parsed.params_len]; 18 pub const columns: []const []const u8 = parsed.columns[0..parsed.columns_len]; 19 20 /// validate that args struct has all required params 21 pub fn validateArgs(comptime Args: type) void { 22 const fields = @typeInfo(Args).@"struct".fields; 23 inline for (params) |p| { 24 if (!hasField(fields, p)) { 25 @compileError("missing param :" ++ p ++ " in args struct"); 26 } 27 } 28 } 29 30 /// validate that struct fields match query columns 31 pub fn validateStruct(comptime T: type) void { 32 const fields = @typeInfo(T).@"struct".fields; 33 inline for (fields) |f| { 34 if (!hasColumn(f.name)) { 35 @compileError("struct field '" ++ f.name ++ "' not found in query columns"); 36 } 37 } 38 } 39 40 /// get column index by name at comptime 41 pub inline fn columnIndex(comptime name: []const u8) comptime_int { 42 inline for (columns, 0..) |col, i| { 43 if (comptime std.mem.eql(u8, col, name)) { 44 return i; 45 } 46 } 47 @compileError("column '" ++ name ++ "' not found in query"); 48 } 49 50 /// map row data to a struct using column names 51 /// row must have .text(idx) and .int(idx) methods 52 pub fn fromRow(comptime T: type, row: anytype) T { 53 comptime validateStruct(T); 54 var result: T = undefined; 55 const fields = @typeInfo(T).@"struct".fields; 56 inline for (fields) |f| { 57 const idx = comptime columnIndex(f.name); 58 @field(result, f.name) = switch (f.type) { 59 []const u8 => row.text(idx), 60 i64 => row.int(idx), 61 bool => row.int(idx) != 0, 62 else => @compileError("unsupported field type: " ++ @typeName(f.type)), 63 }; 64 } 65 return result; 66 } 67 68 /// bind args struct to positional tuple in param order 69 pub fn bind(args: anytype) BindTuple(@TypeOf(args)) { 70 comptime validateArgs(@TypeOf(args)); 71 var result: BindTuple(@TypeOf(args)) = undefined; 72 inline for (params, 0..) |p, i| { 73 result[i] = @field(args, p); 74 } 75 return result; 76 } 77 78 fn BindTuple(comptime Args: type) type { 79 const fields = @typeInfo(Args).@"struct".fields; 80 var types: [param_count]type = undefined; 81 inline for (params, 0..) |p, i| { 82 for (fields) |f| { 83 if (std.mem.eql(u8, f.name, p)) { 84 types[i] = f.type; 85 break; 86 } 87 } 88 } 89 return std.meta.Tuple(&types); 90 } 91 92 fn hasField(fields: anytype, name: []const u8) bool { 93 inline for (fields) |f| { 94 if (std.mem.eql(u8, f.name, name)) return true; 95 } 96 return false; 97 } 98 99 fn hasColumn(comptime name: []const u8) bool { 100 const cols = @This().columns; 101 inline for (cols) |col| { 102 if (std.mem.eql(u8, col, name)) return true; 103 } 104 return false; 105 } 106 }; 107 } 108} 109 110test "columns" { 111 const Q = Query("SELECT id, name, age FROM users"); 112 try std.testing.expectEqual(3, Q.columns.len); 113 try std.testing.expectEqualStrings("id", Q.columns[0]); 114 try std.testing.expectEqualStrings("name", Q.columns[1]); 115 try std.testing.expectEqualStrings("age", Q.columns[2]); 116} 117 118test "named params" { 119 const Q = Query("SELECT * FROM users WHERE id = :id AND age > :min_age"); 120 try std.testing.expectEqual(2, Q.params.len); 121 try std.testing.expectEqualStrings("id", Q.params[0]); 122 try std.testing.expectEqualStrings("min_age", Q.params[1]); 123} 124 125test "positional conversion" { 126 const Q = Query("SELECT * FROM users WHERE id = :id AND age > :min_age"); 127 try std.testing.expectEqualStrings("SELECT * FROM users WHERE id = ? AND age > ?", Q.positional); 128} 129 130test "columns with alias" { 131 const Q = Query("SELECT id, first_name AS name FROM users"); 132 try std.testing.expectEqual(2, Q.columns.len); 133 try std.testing.expectEqualStrings("id", Q.columns[0]); 134 try std.testing.expectEqualStrings("name", Q.columns[1]); 135} 136 137test "columns with function" { 138 const Q = Query("SELECT COUNT(*) AS count, MAX(age) AS max_age FROM users"); 139 try std.testing.expectEqual(2, Q.columns.len); 140 try std.testing.expectEqualStrings("count", Q.columns[0]); 141 try std.testing.expectEqualStrings("max_age", Q.columns[1]); 142} 143 144test "columnIndex" { 145 const Q = Query("SELECT id, name, age FROM users"); 146 // first verify columns work directly 147 try std.testing.expectEqual(3, Q.columns.len); 148 try std.testing.expectEqualStrings("id", Q.columns[0]); 149 150 // now try columnIndex 151 try std.testing.expectEqual(0, Q.columnIndex("id")); 152 try std.testing.expectEqual(1, Q.columnIndex("name")); 153 try std.testing.expectEqual(2, Q.columnIndex("age")); 154} 155 156test "validateStruct" { 157 const Q = Query("SELECT id, name, age FROM users"); 158 // verify columns first 159 try std.testing.expectEqual(3, Q.columns.len); 160 161 const User = struct { id: i64, name: []const u8, age: i64 }; 162 comptime Q.validateStruct(User); 163 164 const Partial = struct { id: i64, name: []const u8 }; 165 comptime Q.validateStruct(Partial); 166} 167 168test "bind" { 169 const Q = Query("INSERT INTO users (name, age) VALUES (:name, :age)"); 170 try std.testing.expectEqualStrings("INSERT INTO users (name, age) VALUES (?, ?)", Q.positional); 171 172 const args = Q.bind(.{ .name = "alice", .age = @as(i64, 25) }); 173 try std.testing.expectEqualStrings("alice", args[0]); 174 try std.testing.expectEqual(25, args[1]); 175 176 // order doesn't matter in input struct 177 const args2 = Q.bind(.{ .age = @as(i64, 30), .name = "bob" }); 178 try std.testing.expectEqualStrings("bob", args2[0]); 179 try std.testing.expectEqual(30, args2[1]); 180} 181 182test "fromRow" { 183 const Q = Query("SELECT id, name, age FROM users"); 184 185 // mock row matching leaflet-search's Row interface 186 const MockRow = struct { 187 texts: [3][]const u8, 188 ints: [3]i64, 189 190 pub fn text(self: @This(), idx: usize) []const u8 { 191 return self.texts[idx]; 192 } 193 pub fn int(self: @This(), idx: usize) i64 { 194 return self.ints[idx]; 195 } 196 }; 197 198 const row = MockRow{ 199 .texts = .{ "42", "alice", "25" }, 200 .ints = .{ 42, 0, 25 }, 201 }; 202 203 const User = struct { id: i64, name: []const u8, age: i64 }; 204 const user = Q.fromRow(User, row); 205 206 try std.testing.expectEqual(42, user.id); 207 try std.testing.expectEqualStrings("alice", user.name); 208 try std.testing.expectEqual(25, user.age); 209}