comptime sql bindings for zig
ziglang sql

add internal docs on comptime and design direction

- docs/comptime.md: research on zig comptime capabilities
- docs/design.md: potential directions for zql based on comptime

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

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

+338
+188
docs/comptime.md
··· 1 + # zig comptime 2 + 3 + internal research on zig's compile-time execution for informing zql's design. 4 + 5 + ## core mechanisms 6 + 7 + ### comptime parameters (generics) 8 + 9 + functions accept `comptime` parameters that must be known at compile time: 10 + 11 + ```zig 12 + fn max(comptime T: type, a: T, b: T) T { 13 + return if (a > b) a else b; 14 + } 15 + ``` 16 + 17 + this implements compile-time duck typing - if `T` doesn't support `>`, error at call site. 18 + 19 + ### comptime variables 20 + 21 + ```zig 22 + comptime var y: i32 = 1; 23 + y += 1; // evaluated during compilation 24 + ``` 25 + 26 + ### inline loops 27 + 28 + `inline for` and `inline while` unroll at compile time: 29 + 30 + ```zig 31 + inline for (std.meta.fields(T)) |field| { 32 + @field(value, field.name) = processField(field); 33 + } 34 + ``` 35 + 36 + ## type-level programming 37 + 38 + types are first-class values at comptime. functions can return types: 39 + 40 + ```zig 41 + fn ArrayList(comptime T: type) type { 42 + return struct { 43 + items: []T, 44 + len: usize, 45 + 46 + const Self = @This(); 47 + 48 + fn append(self: *Self, item: T) !void { 49 + // ... 50 + } 51 + }; 52 + } 53 + ``` 54 + 55 + ## reflection via @typeInfo 56 + 57 + inspect type structure at compile time: 58 + 59 + ```zig 60 + fn GetBiggerInt(comptime T: type) type { 61 + const info = @typeInfo(T).Int; 62 + return @Type(.{ 63 + .Int = .{ 64 + .bits = info.bits + 1, 65 + .signedness = info.signedness, 66 + }, 67 + }); 68 + } 69 + ``` 70 + 71 + ## key builtins 72 + 73 + | builtin | purpose | 74 + |---------|---------| 75 + | `@typeInfo(T)` | returns tagged union describing type structure | 76 + | `@Type(info)` | reifies a type from `@typeInfo` output | 77 + | `@TypeOf(expr)` | returns expression's type | 78 + | `@field(obj, name)` | dynamic field access (name must be comptime) | 79 + | `@hasField(T, name)` | checks if type has named field | 80 + | `@hasDecl(T, name)` | checks if type has named declaration | 81 + | `@compileError(msg)` | generates compile error with custom message | 82 + 83 + ## std.meta utilities 84 + 85 + - `std.meta.fields(T)` - returns struct/union/enum field information 86 + - `std.meta.fieldNames(T)` - returns slice of field name strings 87 + - `std.meta.FieldEnum(T)` - generates enum matching struct fields 88 + - `std.meta.hasFn(T, name)` - checks if type has a function declaration 89 + - `std.meta.eql(a, b)` - recursive structural equality 90 + - `std.meta.Tuple(&types)` - creates tuple type from array of types 91 + 92 + ## limitations 93 + 94 + | limitation | reason | workaround | 95 + |------------|--------|------------| 96 + | no I/O | hermetic, reproducible builds | use `build.zig` for external I/O | 97 + | no heap allocation | currently unsupported | proposals exist (#5873, #5881) | 98 + | `@Type` incomplete | not implemented for enums, unions, functions | use simpler type construction | 99 + | no runtime type info | types only exist at comptime | build custom RTTI at comptime | 100 + 101 + ## gotchas 102 + 103 + 1. **comptime doesn't cross function boundaries** without explicit marking 104 + 2. **`@field` requires comptime-known strings** - can't use runtime strings 105 + 3. **comptime pollution** - once something is comptime, everything it uses must be too 106 + 4. **branch quota** - loops default to 1000 backward branches, use `@setEvalBranchQuota()` 107 + 5. **no declaration-site type checking** - errors appear at call sites, not generic definitions 108 + 109 + ## patterns relevant to zql 110 + 111 + ### comptime string parsing 112 + 113 + std.fmt parses format strings at comptime, validating types before runtime: 114 + 115 + ```zig 116 + std.debug.print("Value: {d}, Name: {s}\n", .{ value, name }); 117 + // format string parsed at compile time; type mismatches caught during compilation 118 + ``` 119 + 120 + this is what zql does with SQL strings. 121 + 122 + ### perfect hash generation 123 + 124 + andrew kelley demonstrated O(1) string switches via comptime perfect hash search: 125 + 126 + ```zig 127 + const ph = perfectHash(&.{ "a", "ab", "abc" }); 128 + switch (ph.hash(target)) { 129 + ph.case("a") => handleA(), 130 + ph.case("ab") => handleAb(), 131 + else => unreachable, 132 + } 133 + ``` 134 + 135 + could be useful for column name lookups. 136 + 137 + ### comptime string interning 138 + 139 + leverage memoization to deduplicate strings: 140 + 141 + ```zig 142 + fn internString(comptime str: []const u8) []const u8 { 143 + return internStringBuffer(str.len, str[0..str.len].*); 144 + } 145 + 146 + fn internStringBuffer(comptime len: comptime_int, comptime items: [len]u8) []const u8 { 147 + comptime var storage: [len]u8 = items; 148 + return &storage; 149 + } 150 + ``` 151 + 152 + since comptime calls are memoized, identical strings return the same address. 153 + 154 + ### schema/orm patterns (tigerbeetle) 155 + 156 + ```zig 157 + fn DBType(comptime schema: Schema) type { 158 + return struct { 159 + tables: generateTables(schema), 160 + indexes: generateIndexes(schema), 161 + 162 + pub fn query(self: *@This(), comptime filter: Filter) Iterator { 163 + // ... 164 + } 165 + }; 166 + } 167 + ``` 168 + 169 + this is where zql could go - define schema once, generate queries. 170 + 171 + ## sources 172 + 173 + ### official 174 + - [zig language reference - comptime](https://ziglang.org/documentation/master/#comptime) 175 + 176 + ### andrew kelley 177 + - [string matching based on compile time perfect hashing](https://andrewkelley.me/post/string-matching-comptime-perfect-hashing-zig.html) 178 + - [zig blurs the line between compile-time and run-time](https://andrewkelley.me/post/zig-programming-language-blurs-line-compile-time-run-time.html) 179 + 180 + ### community 181 + - [what is zig's comptime? - loris cro](https://kristoff.it/blog/what-is-zig-comptime/) 182 + - [comptime zig orm - matklad](https://matklad.github.io/2025/03/19/comptime-zig-orm.html) 183 + - [things zig comptime won't do - matklad](https://matklad.github.io/2025/04/19/things-zig-comptime-wont-do.html) 184 + - [zig metaprogramming - openmymind](https://www.openmymind.net/Basic-MetaProgramming-in-Zig/) 185 + 186 + ### std library source 187 + - [std/meta.zig](https://github.com/ziglang/zig/blob/master/lib/std/meta.zig) 188 + - [std/fmt.zig](https://github.com/ziglang/zig/blob/master/lib/std/fmt.zig)
+150
docs/design.md
··· 1 + # zql design notes 2 + 3 + ## current state 4 + 5 + zql provides comptime utilities for SQL: 6 + 7 + 1. **parse** - extract column names and param names from SQL strings 8 + 2. **bind** - convert named struct args to positional tuple 9 + 3. **fromRow** - map row data to typed struct with validation 10 + 11 + ```zig 12 + const Q = zql.Query("SELECT id, name FROM users WHERE id = :id"); 13 + 14 + // bind: struct -> tuple in param order 15 + c.query(Q.positional, Q.bind(.{ .id = user_id })); 16 + 17 + // fromRow: row -> typed struct 18 + const user = Q.fromRow(User, row); 19 + ``` 20 + 21 + ## what comptime enables 22 + 23 + based on research (see comptime.md), zig's comptime can: 24 + 25 + - parse and validate strings at compile time 26 + - generate types from data 27 + - create perfect hash functions for O(1) lookups 28 + - build complex data structures via type functions 29 + 30 + ## potential directions 31 + 32 + ### 1. table definitions (orm-lite) 33 + 34 + define schema once, generate queries: 35 + 36 + ```zig 37 + const User = zql.Table("users", struct { 38 + id: i64, 39 + name: []const u8, 40 + email: []const u8, 41 + }); 42 + 43 + // generates: INSERT INTO users (id, name, email) VALUES (?, ?, ?) 44 + User.insert(.{ .id = 1, .name = "alice", .email = "a@b.com" }); 45 + 46 + // generates: SELECT id, name, email FROM users WHERE id = ? 47 + User.select().where(.{ .id = 1 }); 48 + 49 + // generates: UPDATE users SET name = ? WHERE id = ? 50 + User.update(.{ .name = "bob" }).where(.{ .id = 1 }); 51 + ``` 52 + 53 + pros: 54 + - single source of truth for schema 55 + - no manual SQL writing for CRUD 56 + - compile-time validation 57 + 58 + cons: 59 + - scope creep into ORM territory 60 + - complex queries still need raw SQL 61 + - migration story unclear 62 + 63 + ### 2. query composition 64 + 65 + build queries from reusable fragments: 66 + 67 + ```zig 68 + const base = zql.Select("d.uri, d.title, d.created_at") 69 + .from("documents d"); 70 + 71 + const withFts = base 72 + .join("documents_fts f ON d.uri = f.uri") 73 + .where("documents_fts MATCH :query"); 74 + 75 + const withTag = base 76 + .join("document_tags dt ON d.uri = dt.document_uri") 77 + .where("dt.tag = :tag"); 78 + 79 + const withBoth = withFts.and(withTag); 80 + ``` 81 + 82 + pros: 83 + - reduces duplication (DocsByTag, DocsByFts, DocsByFtsAndTag -> composable) 84 + - still explicit SQL, just structured 85 + 86 + cons: 87 + - complex API 88 + - might not cover all SQL patterns 89 + 90 + ### 3. schema validation 91 + 92 + define schema, validate queries reference real columns: 93 + 94 + ```zig 95 + const schema = zql.Schema{ 96 + .tables = .{ 97 + .users = .{ .id = .int, .name = .text, .email = .text }, 98 + .posts = .{ .id = .int, .user_id = .int, .title = .text }, 99 + }, 100 + }; 101 + 102 + // compile error if 'users' table doesn't have 'name' column 103 + const Q = schema.Query("SELECT name FROM users"); 104 + ``` 105 + 106 + pros: 107 + - catches typos at compile time 108 + - documents schema in code 109 + 110 + cons: 111 + - schema must be maintained in zig (duplication from DB) 112 + - no runtime schema introspection in zig 113 + 114 + ### 4. better parsing 115 + 116 + current parser is basic. could add: 117 + 118 + - sql syntax validation (not just name extraction) 119 + - join detection 120 + - subquery handling 121 + - aggregate function recognition 122 + 123 + ### 5. perfect hash for columns 124 + 125 + use comptime perfect hashing for O(1) column lookups: 126 + 127 + ```zig 128 + const Q = zql.Query("SELECT id, name, age FROM users"); 129 + // Q.columnIndex("name") could use perfect hash instead of linear search 130 + ``` 131 + 132 + probably premature optimization for typical column counts. 133 + 134 + ## recommendation 135 + 136 + start with **query composition** (#2). it: 137 + 138 + - solves a real pain point (duplicated query variants in leaflet-search) 139 + - stays close to SQL (no abstraction leap) 140 + - is incrementally adoptable 141 + - doesn't require schema definition 142 + 143 + table definitions (#1) are valuable but bigger scope. schema validation (#3) requires maintaining schema twice. 144 + 145 + ## open questions 146 + 147 + 1. how to handle dynamic WHERE clauses? (optional filters) 148 + 2. how to compose queries that return different column sets? 149 + 3. should zql own the execution layer or just generate SQL? 150 + 4. how to handle database-specific SQL dialects?