diff options
| -rw-r--r-- | README.md | 216 |
1 files changed, 170 insertions, 46 deletions
| @@ -19,17 +19,23 @@ This package is a thin wrapper around [sqlite](https://sqlite.org/index.html)'s | |||
| 19 | 19 | ||
| 20 | Since there's no package manager for Zig yet, the recommended way is to use a git submodule: | 20 | Since there's no package manager for Zig yet, the recommended way is to use a git submodule: |
| 21 | 21 | ||
| 22 | $ git submodule add https://git.sr.ht/~vrischmann/zig-sqlite src/sqlite | 22 | ```bash |
| 23 | $ git submodule add https://git.sr.ht/~vrischmann/zig-sqlite src/sqlite | ||
| 24 | ``` | ||
| 23 | 25 | ||
| 24 | Then add the following to your `build.zig` target(s): | 26 | Then add the following to your `build.zig` target(s): |
| 25 | 27 | ||
| 26 | exe.linkLibC(); | 28 | ```zig |
| 27 | exe.linkSystemLibrary("sqlite3"); | 29 | exe.linkLibC(); |
| 28 | exe.addPackage(.{ .name = "sqlite", .path = "src/sqlite/sqlite.zig" }); | 30 | exe.linkSystemLibrary("sqlite3"); |
| 31 | exe.addPackage(.{ .name = "sqlite", .path = "src/sqlite/sqlite.zig" }); | ||
| 32 | ``` | ||
| 29 | 33 | ||
| 30 | Now you should be able to import sqlite like this: | 34 | Now you should be able to import sqlite like this: |
| 31 | 35 | ||
| 32 | const sqlite = @import("sqlite"); | 36 | ```zig |
| 37 | const sqlite = @import("sqlite"); | ||
| 38 | ``` | ||
| 33 | 39 | ||
| 34 | ## Usage | 40 | ## Usage |
| 35 | 41 | ||
| @@ -37,8 +43,10 @@ Now you should be able to import sqlite like this: | |||
| 37 | 43 | ||
| 38 | You must create and initialize an instance of `sqlite.Db`: | 44 | You must create and initialize an instance of `sqlite.Db`: |
| 39 | 45 | ||
| 40 | var db: sqlite.Db = undefined; | 46 | ```zig |
| 41 | try db.init(allocator, .{ .mode = sqlite.Db.Mode{ .File = "/home/vincent/mydata.db" } }); | 47 | var db: sqlite.Db = undefined; |
| 48 | try db.init(allocator, .{ .mode = sqlite.Db.Mode{ .File = "/home/vincent/mydata.db" } }); | ||
| 49 | ``` | ||
| 42 | 50 | ||
| 43 | The `init` method takes an allocator and an optional tuple which will used to configure sqlite. | 51 | The `init` method takes an allocator and an optional tuple which will used to configure sqlite. |
| 44 | 52 | ||
| @@ -48,12 +56,14 @@ Right now the only member used in that tuple is `mode` which defines if the sqli | |||
| 48 | 56 | ||
| 49 | sqlite works exclusively by using prepared statements. The wrapper type is `sqlite.Statement`. Here is how you get one: | 57 | sqlite works exclusively by using prepared statements. The wrapper type is `sqlite.Statement`. Here is how you get one: |
| 50 | 58 | ||
| 51 | const query = | 59 | ```zig |
| 52 | \\SELECT id, name, age, salary FROM employees WHERE age > ? AND age < ? | 60 | const query = |
| 53 | ; | 61 | \\SELECT id, name, age, salary FROM employees WHERE age > ? AND age < ? |
| 62 | ; | ||
| 54 | 63 | ||
| 55 | var stmt = try db.prepare(query); | 64 | var stmt = try db.prepare(query); |
| 56 | defer stmt.deinit(); | 65 | defer stmt.deinit(); |
| 66 | ``` | ||
| 57 | 67 | ||
| 58 | The `Db.prepare` method takes a `comptime` query string. | 68 | The `Db.prepare` method takes a `comptime` query string. |
| 59 | 69 | ||
| @@ -61,17 +71,19 @@ The `Db.prepare` method takes a `comptime` query string. | |||
| 61 | 71 | ||
| 62 | For queries which do not return data (`INSERT`, `UPDATE`) you can use the `exec` method: | 72 | For queries which do not return data (`INSERT`, `UPDATE`) you can use the `exec` method: |
| 63 | 73 | ||
| 64 | const query = | 74 | ```zig |
| 65 | \\UPDATE foo SET salary = ? WHERE id = ? | 75 | const query = |
| 66 | ; | 76 | \\UPDATE foo SET salary = ? WHERE id = ? |
| 77 | ; | ||
| 67 | 78 | ||
| 68 | var stmt = try db.prepare(query); | 79 | var stmt = try db.prepare(query); |
| 69 | defer stmt.deinit(); | 80 | defer stmt.deinit(); |
| 70 | 81 | ||
| 71 | try stmt.exec({ | 82 | try stmt.exec({ |
| 72 | .salary = 20000, | 83 | .salary = 20000, |
| 73 | .id = 40, | 84 | .id = 40, |
| 74 | }); | 85 | }); |
| 86 | ``` | ||
| 75 | 87 | ||
| 76 | See the section "Bind parameters and resultset rows" for more information on the types mapping rules. | 88 | See the section "Bind parameters and resultset rows" for more information on the types mapping rules. |
| 77 | 89 | ||
| @@ -79,26 +91,28 @@ See the section "Bind parameters and resultset rows" for more information on the | |||
| 79 | 91 | ||
| 80 | For queries which do return data you can use the `all` method: | 92 | For queries which do return data you can use the `all` method: |
| 81 | 93 | ||
| 82 | const query = | 94 | ```zig |
| 83 | \\SELECT id, name, age, salary FROM employees WHERE age > ? AND age < ? | 95 | const query = |
| 84 | ; | 96 | \\SELECT id, name, age, salary FROM employees WHERE age > ? AND age < ? |
| 85 | 97 | ; | |
| 86 | var stmt = try db.prepare(query); | 98 | |
| 87 | defer stmt.deinit(); | 99 | var stmt = try db.prepare(query); |
| 88 | 100 | defer stmt.deinit(); | |
| 89 | const rows = try stmt.all( | 101 | |
| 90 | struct { | 102 | const rows = try stmt.all( |
| 91 | id: usize, | 103 | struct { |
| 92 | name: []const u8, | 104 | id: usize, |
| 93 | age: u16, | 105 | name: []const u8, |
| 94 | salary: u32, | 106 | age: u16, |
| 95 | }, | 107 | salary: u32, |
| 96 | .{ .allocator = allocator }, | 108 | }, |
| 97 | .{ .age1 = 20, .age2 = 40 }, | 109 | .{ .allocator = allocator }, |
| 98 | ); | 110 | .{ .age1 = 20, .age2 = 40 }, |
| 99 | for (rows) |row| { | 111 | ); |
| 100 | std.log.debug("id: {} ; name: {}; age: {}; salary: {}", .{ row.id, row.name, row.age, row.salary }); | 112 | for (rows) |row| { |
| 101 | } | 113 | std.log.debug("id: {} ; name: {}; age: {}; salary: {}", .{ row.id, row.name, row.age, row.salary }); |
| 114 | } | ||
| 115 | ``` | ||
| 102 | 116 | ||
| 103 | The `all` method takes a type and an optional tuple. | 117 | The `all` method takes a type and an optional tuple. |
| 104 | 118 | ||
| @@ -127,8 +141,118 @@ Here are the rules for resultset rows: | |||
| 127 | 141 | ||
| 128 | Note that arrays must have a sentinel because we need a way to communicate where the data actually stops in the array, so for example use `[200:0]u8` for a `TEXT` field. | 142 | Note that arrays must have a sentinel because we need a way to communicate where the data actually stops in the array, so for example use `[200:0]u8` for a `TEXT` field. |
| 129 | 143 | ||
| 130 | ### Comptime checked statements | 144 | ## Comptime checks |
| 131 | 145 | ||
| 132 | Prepared statements contain _comptime_ metadata which is used to validate that every call to `exec`, `one` and `all` provides the appropriate number of bind parameters. | 146 | Prepared statements contain _comptime_ metadata which is used to validate every call to `exec`, `one` and `all` _at compile time_. |
| 133 | 147 | ||
| 134 | Right now there's no _type_ checking of bind parameters but it could probably be done. | 148 | ### Check the number of bind parameters. |
| 149 | |||
| 150 | The first check makes sure you provide the same number of bind parameters as there are bind markers in the query string. | ||
| 151 | |||
| 152 | Take the following code: | ||
| 153 | ```zig | ||
| 154 | var stmt = try db.prepare("SELECT id FROM user WHERE age > ? AND age < ? AND weight > ?"); | ||
| 155 | defer stmt.deinit(); | ||
| 156 | |||
| 157 | const rows = try stmt.all(usize, .{ .allocator = allocator }, .{ | ||
| 158 | .age_1 = 10, | ||
| 159 | .age_2 = 20, | ||
| 160 | }); | ||
| 161 | _ = rows; | ||
| 162 | ``` | ||
| 163 | It fails with this compilation error: | ||
| 164 | ``` | ||
| 165 | /home/vincent/dev/perso/libs/zig-sqlite/sqlite.zig:465:17: error: number of bind markers not equal to number of fields | ||
| 166 | @compileError("number of bind markers not equal to number of fields"); | ||
| 167 | ^ | ||
| 168 | /home/vincent/dev/perso/libs/zig-sqlite/sqlite.zig:543:22: note: called from here | ||
| 169 | self.bind(values); | ||
| 170 | ^ | ||
| 171 | /home/vincent/dev/perso/libs/zig-sqlite/sqlite.zig:619:41: note: called from here | ||
| 172 | var iter = try self.iterator(Type, values); | ||
| 173 | ^ | ||
| 174 | ./src/main.zig:16:30: note: called from here | ||
| 175 | const rows = try stmt.all(usize, .{ .allocator = allocator }, .{ | ||
| 176 | ^ | ||
| 177 | ./src/main.zig:5:29: note: called from here | ||
| 178 | pub fn main() anyerror!void { | ||
| 179 | ``` | ||
| 180 | |||
| 181 | ### Assign types to bind markers and check them. | ||
| 182 | |||
| 183 | The second (and more interesting) check makes sure you provide appropriately typed values as bind parameters. | ||
| 184 | |||
| 185 | This check is not automatic since with a standard SQL query we have no way to know the types of the bind parameters, to use it you must provide theses types in the SQL query with a custom syntax. | ||
| 186 | |||
| 187 | For example, take the same code as above but now we also bind the last parameter: | ||
| 188 | ```zig | ||
| 189 | var stmt = try db.prepare("SELECT id FROM user WHERE age > ? AND age < ? AND weight > ?"); | ||
| 190 | defer stmt.deinit(); | ||
| 191 | |||
| 192 | const rows = try stmt.all(usize, .{ .allocator = allocator }, .{ | ||
| 193 | .age_1 = 10, | ||
| 194 | .age_2 = 20, | ||
| 195 | .weight = false, | ||
| 196 | }); | ||
| 197 | _ = rows; | ||
| 198 | ``` | ||
| 199 | |||
| 200 | This compiles correctly even if the `weight` field in our `user` table is of the type `INTEGER`. | ||
| 201 | |||
| 202 | We can make sure the bind parameters have the right type if we rewrite the query like this: | ||
| 203 | ```zig | ||
| 204 | var stmt = try db.prepare("SELECT id FROM user WHERE age > ? AND age < ? AND weight > ?{usize}"); | ||
| 205 | defer stmt.deinit(); | ||
| 206 | |||
| 207 | const rows = try stmt.all(usize, .{ .allocator = allocator }, .{ | ||
| 208 | .age_1 = 10, | ||
| 209 | .age_2 = 20, | ||
| 210 | .weight = false, | ||
| 211 | }); | ||
| 212 | _ = rows; | ||
| 213 | |||
| 214 | ``` | ||
| 215 | Now this fails to compile: | ||
| 216 | ``` | ||
| 217 | /home/vincent/dev/perso/libs/zig-sqlite/sqlite.zig:485:25: error: value type bool is not the bind marker type usize | ||
| 218 | @compileError("value type " ++ @typeName(struct_field.field_type) ++ " is not the bind marker type " ++ @typeName(typ)); | ||
| 219 | ^ | ||
| 220 | /home/vincent/dev/perso/libs/zig-sqlite/sqlite.zig:557:22: note: called from here | ||
| 221 | self.bind(values); | ||
| 222 | ^ | ||
| 223 | /home/vincent/dev/perso/libs/zig-sqlite/sqlite.zig:633:41: note: called from here | ||
| 224 | var iter = try self.iterator(Type, values); | ||
| 225 | ^ | ||
| 226 | ./src/main.zig:16:30: note: called from here | ||
| 227 | const rows = try stmt.all(usize, .{ .allocator = allocator }, .{ | ||
| 228 | ^ | ||
| 229 | ./src/main.zig:5:29: note: called from here | ||
| 230 | pub fn main() anyerror!void { | ||
| 231 | ``` | ||
| 232 | The syntax is straightforward: a bind marker `?` followed by `{`, a Zig type name and finally `}`. | ||
| 233 | |||
| 234 | There are a limited number of types allowed currently: | ||
| 235 | * all [integer](https://ziglang.org/documentation/master/#Primitive-Types) types. | ||
| 236 | * all [arbitrary bit-width integer](https://ziglang.org/documentation/master/#Primitive-Types) types. | ||
| 237 | * all [float](https://ziglang.org/documentation/master/#Primitive-Types) types. | ||
| 238 | * bool. | ||
| 239 | * strings with `[]const u8` or `[]u8`. | ||
| 240 | * strings with `sqlite.Text`. | ||
| 241 | * blobs with `sqlite.Blob`. | ||
| 242 | |||
| 243 | It's probably possible to support arbitrary types if they can be marshaled to a sqlite type. This is something to investigate. | ||
| 244 | |||
| 245 | **NOTE**: this is done at compile time and is quite CPU intensive, therefore it's possible you'll have to play with [@setEvalBranchQuota](https://ziglang.org/documentation/master/#setEvalBranchQuota) to make it compile. | ||
| 246 | |||
| 247 | To finish our example, passing the proper type allows it compile: | ||
| 248 | ```zig | ||
| 249 | var stmt = try db.prepare("SELECT id FROM user WHERE age > ? AND age < ? AND weight > ?{usize}"); | ||
| 250 | defer stmt.deinit(); | ||
| 251 | |||
| 252 | const rows = try stmt.all(usize, .{ .allocator = allocator }, .{ | ||
| 253 | .age_1 = 10, | ||
| 254 | .age_2 = 20, | ||
| 255 | .weight = @as(usize, 200), | ||
| 256 | }); | ||
| 257 | _ = rows; | ||
| 258 | ``` | ||