comptime sql bindings for zig
ziglang sql

update design doc with zig idiom research

- options structs preferred over fluent builders
- type-returning functions for generics
- explicit code over clever patterns
- recommendation: keep current simple Query approach
- composition via type-returning function if needed later

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

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

+74 -21
+74 -21
docs/design.md
··· 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 ··· 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
··· 60 - complex queries still need raw SQL 61 - migration story unclear 62 63 + ### 2. query composition (options struct pattern) 64 65 + zig idiom: use options structs, not fluent builders. 66 67 ```zig 68 + // NOT idiomatic zig: 69 + zql.Select("id").from("users").where("x = :x") 70 71 + // idiomatic zig - options struct: 72 + const Q = zql.Query(.{ 73 + .select = "d.uri, d.did, d.title, d.created_at", 74 + .from = "documents d", 75 + .joins = &.{ 76 + "LEFT JOIN publications p ON d.publication_uri = p.uri", 77 + }, 78 + .where = "d.uri = :uri", 79 + .order_by = "d.created_at DESC", 80 + .limit = 40, 81 + }); 82 + ``` 83 84 + for variants, use comptime conditionals: 85 86 + ```zig 87 + fn DocQuery(comptime opts: struct { 88 + fts: bool = false, 89 + tag: bool = false, 90 + }) type { 91 + return zql.Query(.{ 92 + .select = if (opts.fts) fts_columns else basic_columns, 93 + .from = if (opts.fts) "documents_fts f" else "documents d", 94 + .joins = buildJoins(opts), 95 + .where = buildWhere(opts), 96 + .order_by = if (opts.fts) "rank" else "d.created_at DESC", 97 + }); 98 + } 99 + 100 + // usage: 101 + const DocsByFts = DocQuery(.{ .fts = true }); 102 + const DocsByTag = DocQuery(.{ .tag = true }); 103 + const DocsByFtsAndTag = DocQuery(.{ .fts = true, .tag = true }); 104 ``` 105 106 pros: 107 + - follows zig idioms (options struct pattern) 108 + - explicit, readable 109 + - comptime conditional logic is clear 110 + - type-returning function pattern from std 111 112 cons: 113 + - more verbose than current raw SQL strings 114 + - query structure must fit the options model 115 116 ### 3. schema validation 117 ··· 157 158 probably premature optimization for typical column counts. 159 160 + ## zig idioms (from research) 161 + 162 + the zig community prefers: 163 + 164 + 1. **options structs** over fluent builders 165 + 2. **type-returning functions** for generics 166 + 3. **explicit code** over clever patterns 167 + 4. **comptime validation** via @compileError 168 + 169 + from the zig zen: 170 + - "favor reading code over writing code" 171 + - "only one obvious way to do things" 172 + - "communicate intent precisely" 173 + 174 ## recommendation 175 176 + **keep the current simple approach.** three explicit Query constants is more readable than a factory function with options. 177 + 178 + ```zig 179 + // current - explicit, clear 180 + const DocsByTag = zql.Query("SELECT ... WHERE tag = :tag ..."); 181 + const DocsByFts = zql.Query("SELECT ... WHERE MATCH :query ..."); 182 + const DocsByFtsAndTag = zql.Query("SELECT ... WHERE MATCH :query AND tag = :tag ..."); 183 + ``` 184 + 185 + this follows zig's preference for explicit over clever. the "duplication" is actually meaningful - these queries have different semantics. 186 187 + if we do add composition, use the **type-returning function pattern**: 188 + 189 + ```zig 190 + fn DocQuery(comptime opts: struct { fts: bool = false, tag: bool = false }) type { 191 + // comptime conditionals to build SQL 192 + return zql.Query(sql); 193 + } 194 + ``` 195 196 + but only if the duplication becomes a real maintenance burden. 197 198 ## open questions 199