diff options
| author | 2020-10-24 21:33:43 +0200 | |
|---|---|---|
| committer | 2020-10-24 21:33:43 +0200 | |
| commit | a1dccc71b1b03af08c81afd6c75d3e6fce1bb892 (patch) | |
| tree | 49d999c8ba9afa63809dc92144eeec62240b56cc | |
| download | zig-sqlite-a1dccc71b1b03af08c81afd6c75d3e6fce1bb892.tar.gz zig-sqlite-a1dccc71b1b03af08c81afd6c75d3e6fce1bb892.tar.xz zig-sqlite-a1dccc71b1b03af08c81afd6c75d3e6fce1bb892.zip | |
initial commit
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | build.zig | 26 | ||||
| -rw-r--r-- | src/main.zig | 509 |
3 files changed, 536 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..48318b2 --- /dev/null +++ b/.gitignore | |||
| @@ -0,0 +1 @@ | |||
| /build_runner.zig | |||
diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..6d419e5 --- /dev/null +++ b/build.zig | |||
| @@ -0,0 +1,26 @@ | |||
| 1 | const std = @import("std"); | ||
| 2 | const Builder = std.build.Builder; | ||
| 3 | |||
| 4 | fn linkAll(obj: *std.build.LibExeObjStep) void { | ||
| 5 | obj.linkLibC(); | ||
| 6 | obj.linkSystemLibrary("sqlite3"); | ||
| 7 | } | ||
| 8 | |||
| 9 | pub fn build(b: *Builder) void { | ||
| 10 | const mode = b.standardReleaseOptions(); | ||
| 11 | |||
| 12 | const lib = b.addStaticLibrary("zig-sqlite", "src/main.zig"); | ||
| 13 | lib.setBuildMode(mode); | ||
| 14 | linkAll(lib); | ||
| 15 | lib.install(); | ||
| 16 | |||
| 17 | const is_ci = b.option(bool, "is_ci", "Identifies if it runs in a CI environment") orelse false; | ||
| 18 | |||
| 19 | var main_tests = b.addTest("src/main.zig"); | ||
| 20 | main_tests.setBuildMode(mode); | ||
| 21 | main_tests.addBuildOption(bool, "is_ci", is_ci); | ||
| 22 | linkAll(main_tests); | ||
| 23 | |||
| 24 | const test_step = b.step("test", "Run library tests"); | ||
| 25 | test_step.dependOn(&main_tests.step); | ||
| 26 | } | ||
diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..d57e73a --- /dev/null +++ b/src/main.zig | |||
| @@ -0,0 +1,509 @@ | |||
| 1 | const std = @import("std"); | ||
| 2 | const build_options = @import("build_options"); | ||
| 3 | const debug = std.debug; | ||
| 4 | const mem = std.mem; | ||
| 5 | const testing = std.testing; | ||
| 6 | |||
| 7 | const c = @cImport({ | ||
| 8 | @cInclude("sqlite3.h"); | ||
| 9 | }); | ||
| 10 | |||
| 11 | const logger = std.log.scoped(.sqlite); | ||
| 12 | |||
| 13 | /// Db is a wrapper around a SQLite database, providing high-level functions to executing queries. | ||
| 14 | /// A Db can be opened with a file database or a in-memory database: | ||
| 15 | /// | ||
| 16 | /// // File database | ||
| 17 | /// var db: sqlite.Db = undefined; | ||
| 18 | /// try db.init(allocator, .{ .File = "/tmp/data.db" }); | ||
| 19 | /// | ||
| 20 | /// // In memory database | ||
| 21 | /// var db: sqlite.Db = undefined; | ||
| 22 | /// try db.init(allocator, .{ .Memory={} }); | ||
| 23 | /// | ||
| 24 | pub const Db = struct { | ||
| 25 | const Self = @This(); | ||
| 26 | |||
| 27 | allocator: *mem.Allocator, | ||
| 28 | db: *c.sqlite3, | ||
| 29 | |||
| 30 | /// Mode determines how the database will be opened. | ||
| 31 | pub const Mode = union(enum) { | ||
| 32 | File: []const u8, | ||
| 33 | Memory, | ||
| 34 | }; | ||
| 35 | |||
| 36 | /// init creates a database with the provided `mode`. | ||
| 37 | pub fn init(self: *Self, allocator: *mem.Allocator, mode: Mode) !void { | ||
| 38 | self.allocator = allocator; | ||
| 39 | |||
| 40 | switch (mode) { | ||
| 41 | .File => |path| { | ||
| 42 | logger.info("opening {}", .{path}); | ||
| 43 | |||
| 44 | // Need a null-terminated string here. | ||
| 45 | const pathZ = try allocator.dupeZ(u8, path); | ||
| 46 | defer allocator.free(pathZ); | ||
| 47 | |||
| 48 | var db: ?*c.sqlite3 = undefined; | ||
| 49 | const result = c.sqlite3_open_v2( | ||
| 50 | pathZ, | ||
| 51 | &db, | ||
| 52 | c.SQLITE_OPEN_READWRITE | c.SQLITE_OPEN_CREATE, | ||
| 53 | null, | ||
| 54 | ); | ||
| 55 | if (result != c.SQLITE_OK or db == null) { | ||
| 56 | logger.warn("unable to open database, result: {}", .{result}); | ||
| 57 | return error.CannotOpenDatabase; | ||
| 58 | } | ||
| 59 | |||
| 60 | self.db = db.?; | ||
| 61 | }, | ||
| 62 | .Memory => { | ||
| 63 | logger.info("opening in memory", .{}); | ||
| 64 | |||
| 65 | var db: ?*c.sqlite3 = undefined; | ||
| 66 | const result = c.sqlite3_open_v2( | ||
| 67 | ":memory:", | ||
| 68 | &db, | ||
| 69 | c.SQLITE_OPEN_READWRITE | c.SQLITE_OPEN_MEMORY, | ||
| 70 | null, | ||
| 71 | ); | ||
| 72 | if (result != c.SQLITE_OK or db == null) { | ||
| 73 | logger.warn("unable to open database, result: {}", .{result}); | ||
| 74 | return error.CannotOpenDatabase; | ||
| 75 | } | ||
| 76 | |||
| 77 | self.db = db.?; | ||
| 78 | }, | ||
| 79 | } | ||
| 80 | } | ||
| 81 | |||
| 82 | /// deinit closes the database. | ||
| 83 | pub fn deinit(self: *Self) void { | ||
| 84 | _ = c.sqlite3_close(self.db); | ||
| 85 | } | ||
| 86 | |||
| 87 | /// exec is a convenience function which prepares a statement and executes it directly. | ||
| 88 | pub fn exec(self: *Self, comptime query: []const u8, values: anytype) !void { | ||
| 89 | var stmt = try self.prepare(query, values); | ||
| 90 | defer stmt.deinit(); | ||
| 91 | try stmt.exec(); | ||
| 92 | } | ||
| 93 | |||
| 94 | /// prepare prepares a statement for the `query` provided. | ||
| 95 | /// | ||
| 96 | /// The query is analysed at comptime to search for bind markers. | ||
| 97 | /// prepare enforces having as much fields in the `values` tuple as there are bind markers. | ||
| 98 | /// | ||
| 99 | /// Example usage: | ||
| 100 | /// | ||
| 101 | /// var stmt = try db.prepare("INSERT INTO foo(id, name) VALUES(?, ?)", .{ | ||
| 102 | /// .id = 3540, | ||
| 103 | /// .name = "Eminem", | ||
| 104 | /// }); | ||
| 105 | /// defer stmt.deinit(); | ||
| 106 | /// | ||
| 107 | /// Note that the name of the fields in the tuple are irrelevant, only the types are. | ||
| 108 | pub fn prepare(self: *Self, comptime query: []const u8, values: anytype) !Statement { | ||
| 109 | return Statement.prepare(self, 0, query, values); | ||
| 110 | } | ||
| 111 | |||
| 112 | /// rowsAffected returns the number of rows affected by the last statement executed. | ||
| 113 | pub fn rowsAffected(self: *Self) usize { | ||
| 114 | return @intCast(usize, c.sqlite3_changes(self.db)); | ||
| 115 | } | ||
| 116 | }; | ||
| 117 | |||
| 118 | /// Statement is a wrapper around a SQLite statement, providing high-level functions to execute | ||
| 119 | /// a statement and retrieve rows for SELECT queries. | ||
| 120 | /// | ||
| 121 | /// The exec function can be used to execute a query which does not return rows: | ||
| 122 | /// | ||
| 123 | /// var stmt = try db.prepare("UPDATE foo SET id = ? WHERE name = ?", .{ | ||
| 124 | /// .id = 200, | ||
| 125 | /// .name = "José", | ||
| 126 | /// }); | ||
| 127 | /// defer stmt.deinit(); | ||
| 128 | /// | ||
| 129 | /// The one function can be used to select a single row: | ||
| 130 | /// | ||
| 131 | /// var stmt = try db.prepare("SELECT name FROM foo WHERE id = ?", .{ .id = 200 }); | ||
| 132 | /// defer stmt.deinit(); | ||
| 133 | /// | ||
| 134 | /// const Row = struct { id: usize }; | ||
| 135 | /// const row = try stmt.one(Row .{}); | ||
| 136 | /// | ||
| 137 | /// The all function can be used to select all rows: | ||
| 138 | /// | ||
| 139 | /// var stmt = try db.prepare("SELECT name FROM foo", .{}); | ||
| 140 | /// defer stmt.deinit(); | ||
| 141 | /// | ||
| 142 | /// const Row = struct { id: usize }; | ||
| 143 | /// const rows = try stmt.all(Row .{}); | ||
| 144 | /// | ||
| 145 | /// Look at aach function for more complete documentation. | ||
| 146 | /// | ||
| 147 | pub const Statement = struct { | ||
| 148 | const Self = @This(); | ||
| 149 | |||
| 150 | stmt: *c.sqlite3_stmt, | ||
| 151 | |||
| 152 | fn prepare(db: *Db, flags: c_uint, comptime query: []const u8, values: anytype) !Self { | ||
| 153 | const StructType = @typeInfo(@TypeOf(values)).Struct; | ||
| 154 | comptime { | ||
| 155 | const bind_parameter_count = std.mem.count(u8, query, "?"); | ||
| 156 | if (bind_parameter_count != StructType.fields.len) { | ||
| 157 | @compileError("bind parameter count != number of fields in tuple/struct"); | ||
| 158 | } | ||
| 159 | } | ||
| 160 | |||
| 161 | // prepare | ||
| 162 | |||
| 163 | var stmt = blk: { | ||
| 164 | var tmp: ?*c.sqlite3_stmt = undefined; | ||
| 165 | const result = c.sqlite3_prepare_v3( | ||
| 166 | db.db, | ||
| 167 | query.ptr, | ||
| 168 | @intCast(c_int, query.len), | ||
| 169 | flags, | ||
| 170 | &tmp, | ||
| 171 | null, | ||
| 172 | ); | ||
| 173 | if (result != c.SQLITE_OK) { | ||
| 174 | logger.warn("unable to prepare statement, result: {}", .{result}); | ||
| 175 | return error.CannotPrepareStatement; | ||
| 176 | } | ||
| 177 | break :blk tmp.?; | ||
| 178 | }; | ||
| 179 | |||
| 180 | // Bind | ||
| 181 | |||
| 182 | inline for (StructType.fields) |struct_field, _i| { | ||
| 183 | const i = @as(usize, _i); | ||
| 184 | const field_type_info = @typeInfo(struct_field.field_type); | ||
| 185 | const field_value = @field(values, struct_field.name); | ||
| 186 | const column = i + 1; | ||
| 187 | |||
| 188 | switch (struct_field.field_type) { | ||
| 189 | []const u8, []u8 => { | ||
| 190 | _ = c.sqlite3_bind_text(stmt, column, field_value.ptr, @intCast(c_int, field_value.len), null); | ||
| 191 | }, | ||
| 192 | else => switch (field_type_info) { | ||
| 193 | .Int, .ComptimeInt => _ = c.sqlite3_bind_int64(stmt, column, @intCast(c_longlong, field_value)), | ||
| 194 | .Float, .ComptimeFloat => _ = c.sqlite3_bind_double(stmt, column, field_value), | ||
| 195 | .Array => |arr| { | ||
| 196 | switch (arr.child) { | ||
| 197 | u8 => { | ||
| 198 | const data: []const u8 = field_value[0..field_value.len]; | ||
| 199 | |||
| 200 | _ = c.sqlite3_bind_text(stmt, column, data.ptr, @intCast(c_int, data.len), null); | ||
| 201 | }, | ||
| 202 | else => @compileError("cannot populate field " ++ field.name ++ " of type array of " ++ @typeName(arr.child)), | ||
| 203 | } | ||
| 204 | }, | ||
| 205 | else => @compileError("cannot bind field " ++ struct_field.name ++ " of type " ++ @typeName(struct_field.field_type)), | ||
| 206 | }, | ||
| 207 | } | ||
| 208 | } | ||
| 209 | |||
| 210 | return Self{ | ||
| 211 | .stmt = stmt, | ||
| 212 | }; | ||
| 213 | } | ||
| 214 | |||
| 215 | pub fn deinit(self: *Self) void { | ||
| 216 | const result = c.sqlite3_finalize(self.stmt); | ||
| 217 | if (result != c.SQLITE_OK) { | ||
| 218 | logger.err("unable to finalize prepared statement, result: {}", .{result}); | ||
| 219 | } | ||
| 220 | } | ||
| 221 | |||
| 222 | pub fn exec(self: *Self) !void { | ||
| 223 | const result = c.sqlite3_step(self.stmt); | ||
| 224 | switch (result) { | ||
| 225 | c.SQLITE_DONE => {}, | ||
| 226 | c.SQLITE_BUSY => return error.SQLiteBusy, | ||
| 227 | else => std.debug.panic("invalid result {}", .{result}), | ||
| 228 | } | ||
| 229 | } | ||
| 230 | |||
| 231 | /// one reads a single row from the result set of this statement. | ||
| 232 | /// | ||
| 233 | /// The data in the row is used to populate a value of the type `Type`. | ||
| 234 | /// This means that `Type` must have as many fields as is returned in the query | ||
| 235 | /// executed by this statement. | ||
| 236 | /// This also means that the type of each field must be compatible with the SQLite type. | ||
| 237 | /// | ||
| 238 | /// Here is an example of how to use an anonymous struct type: | ||
| 239 | /// | ||
| 240 | /// const row = try stmt.one( | ||
| 241 | /// struct { | ||
| 242 | /// id: usize, | ||
| 243 | /// name: []const u8, | ||
| 244 | /// age: usize, | ||
| 245 | /// }, | ||
| 246 | /// .{ .allocator = allocator }, | ||
| 247 | /// ); | ||
| 248 | /// | ||
| 249 | /// The `options` tuple is used to provide additional state in some cases, for example | ||
| 250 | /// an allocator used to read text and blobs. | ||
| 251 | /// | ||
| 252 | pub fn one(self: *Self, comptime Type: type, options: anytype) !?Type { | ||
| 253 | const TypeInfo = @typeInfo(Type); | ||
| 254 | |||
| 255 | var result = c.sqlite3_step(self.stmt); | ||
| 256 | |||
| 257 | switch (TypeInfo) { | ||
| 258 | .Int => return switch (result) { | ||
| 259 | c.SQLITE_ROW => try self.readInt(Type, options), | ||
| 260 | c.SQLITE_DONE => null, | ||
| 261 | else => std.debug.panic("invalid result {}", .{result}), | ||
| 262 | }, | ||
| 263 | .Struct => return switch (result) { | ||
| 264 | c.SQLITE_ROW => try self.readStruct(Type, options), | ||
| 265 | c.SQLITE_DONE => null, | ||
| 266 | else => std.debug.panic("invalid result {}", .{result}), | ||
| 267 | }, | ||
| 268 | else => @compileError("cannot read into type " ++ @typeName(Type)), | ||
| 269 | } | ||
| 270 | } | ||
| 271 | |||
| 272 | /// all reads all rows from the result set of this statement. | ||
| 273 | /// | ||
| 274 | /// The data in each row is used to populate a value of the type `Type`. | ||
| 275 | /// This means that `Type` must have as many fields as is returned in the query | ||
| 276 | /// executed by this statement. | ||
| 277 | /// This also means that the type of each field must be compatible with the SQLite type. | ||
| 278 | /// | ||
| 279 | /// Here is an example of how to use an anonymous struct type: | ||
| 280 | /// | ||
| 281 | /// const rows = try stmt.all( | ||
| 282 | /// struct { | ||
| 283 | /// id: usize, | ||
| 284 | /// name: []const u8, | ||
| 285 | /// age: usize, | ||
| 286 | /// }, | ||
| 287 | /// .{ .allocator = allocator }, | ||
| 288 | /// ); | ||
| 289 | /// | ||
| 290 | /// The `options` tuple is used to provide additional state in some cases. | ||
| 291 | /// Note that for this function the allocator is mandatory. | ||
| 292 | /// | ||
| 293 | pub fn all(self: *Self, comptime Type: type, options: anytype) ![]Type { | ||
| 294 | const TypeInfo = @typeInfo(Type); | ||
| 295 | |||
| 296 | var rows = std.ArrayList(Type).init(options.allocator); | ||
| 297 | |||
| 298 | var result = c.sqlite3_step(self.stmt); | ||
| 299 | while (result == c.SQLITE_ROW) : (result = c.sqlite3_step(self.stmt)) { | ||
| 300 | const columns = c.sqlite3_column_count(self.stmt); | ||
| 301 | |||
| 302 | var value = switch (TypeInfo) { | ||
| 303 | .Int => blk: { | ||
| 304 | debug.assert(columns == 1); | ||
| 305 | break :blk try self.readInt(Type, options); | ||
| 306 | }, | ||
| 307 | .Struct => blk: { | ||
| 308 | std.debug.assert(columns == @typeInfo(Type).Struct.fields.len); | ||
| 309 | break :blk try self.readStruct(Type, options); | ||
| 310 | }, | ||
| 311 | else => @compileError("cannot read into type " ++ @typeName(Type)), | ||
| 312 | }; | ||
| 313 | |||
| 314 | try rows.append(value); | ||
| 315 | } | ||
| 316 | |||
| 317 | if (result != c.SQLITE_DONE) { | ||
| 318 | logger.err("unable to iterate, result: {}", .{result}); | ||
| 319 | return error.SQLiteStepError; | ||
| 320 | } | ||
| 321 | |||
| 322 | return rows.span(); | ||
| 323 | } | ||
| 324 | |||
| 325 | fn readInt(self: *Self, comptime Type: type, options: anytype) !Type { | ||
| 326 | const n = c.sqlite3_column_int64(self.stmt, 0); | ||
| 327 | return @intCast(Type, n); | ||
| 328 | } | ||
| 329 | |||
| 330 | fn readStruct(self: *Self, comptime Type: type, options: anytype) !Type { | ||
| 331 | var value: Type = undefined; | ||
| 332 | |||
| 333 | inline for (@typeInfo(Type).Struct.fields) |field, _i| { | ||
| 334 | const i = @as(usize, _i); | ||
| 335 | const field_type_info = @typeInfo(field.field_type); | ||
| 336 | |||
| 337 | switch (field.field_type) { | ||
| 338 | []const u8, []u8 => { | ||
| 339 | const data = c.sqlite3_column_blob(self.stmt, i); | ||
| 340 | if (data == null) { | ||
| 341 | @field(value, field.name) = ""; | ||
| 342 | } else { | ||
| 343 | const size = @intCast(usize, c.sqlite3_column_bytes(self.stmt, i)); | ||
| 344 | |||
| 345 | var tmp = try options.allocator.alloc(u8, size); | ||
| 346 | mem.copy(u8, tmp, @ptrCast([*c]const u8, data)[0..size]); | ||
| 347 | |||
| 348 | @field(value, field.name) = tmp; | ||
| 349 | } | ||
| 350 | }, | ||
| 351 | else => switch (field_type_info) { | ||
| 352 | .Int => { | ||
| 353 | const n = c.sqlite3_column_int64(self.stmt, i); | ||
| 354 | @field(value, field.name) = @intCast(field.field_type, n); | ||
| 355 | }, | ||
| 356 | .Float => { | ||
| 357 | const f = c.sqlite3_column_double(self.stmt, i); | ||
| 358 | @field(value, field.name) = f; | ||
| 359 | }, | ||
| 360 | .Void => { | ||
| 361 | @field(value, field.name) = {}; | ||
| 362 | }, | ||
| 363 | .Array => |arr| { | ||
| 364 | switch (arr.child) { | ||
| 365 | u8 => { | ||
| 366 | const data = c.sqlite3_column_blob(self.stmt, i); | ||
| 367 | const size = @intCast(usize, c.sqlite3_column_bytes(self.stmt, i)); | ||
| 368 | |||
| 369 | if (size > @as(usize, arr.len)) return error.ArrayTooSmall; | ||
| 370 | |||
| 371 | mem.copy(u8, @field(value, field.name)[0..], @ptrCast([*c]const u8, data)[0..size]); | ||
| 372 | }, | ||
| 373 | else => @compileError("cannot populate field " ++ field.name ++ " of type array of " ++ @typeName(arr.child)), | ||
| 374 | } | ||
| 375 | }, | ||
| 376 | else => @compileError("cannot populate field " ++ field.name ++ " of type " ++ @typeName(field.field_type)), | ||
| 377 | }, | ||
| 378 | } | ||
| 379 | } | ||
| 380 | |||
| 381 | return value; | ||
| 382 | } | ||
| 383 | }; | ||
| 384 | |||
| 385 | test "sqlite: statement exec" { | ||
| 386 | var arena = std.heap.ArenaAllocator.init(testing.allocator); | ||
| 387 | defer arena.deinit(); | ||
| 388 | var allocator = &arena.allocator; | ||
| 389 | |||
| 390 | var db: Db = undefined; | ||
| 391 | try db.init(testing.allocator, dbMode()); | ||
| 392 | |||
| 393 | // Create the tables | ||
| 394 | |||
| 395 | comptime const all_ddl = &[_][]const u8{ | ||
| 396 | \\CREATE TABLE user( | ||
| 397 | \\ id integer PRIMARY KEY, | ||
| 398 | \\ name text, | ||
| 399 | \\ age integer | ||
| 400 | \\) | ||
| 401 | , | ||
| 402 | \\CREATE TABLE article( | ||
| 403 | \\ id integer PRIMARY KEY, | ||
| 404 | \\ author_id integer, | ||
| 405 | \\ data text, | ||
| 406 | \\ FOREIGN KEY(author_id) REFERENCES user(id) | ||
| 407 | \\) | ||
| 408 | }; | ||
| 409 | inline for (all_ddl) |ddl| { | ||
| 410 | var stmt = try db.prepare(ddl, .{}); | ||
| 411 | defer stmt.deinit(); | ||
| 412 | try stmt.exec(); | ||
| 413 | } | ||
| 414 | |||
| 415 | // Add data | ||
| 416 | |||
| 417 | const User = struct { | ||
| 418 | id: usize, | ||
| 419 | name: []const u8, | ||
| 420 | age: usize, | ||
| 421 | }; | ||
| 422 | |||
| 423 | const users = &[_]User{ | ||
| 424 | .{ .id = 20, .name = "Vincent", .age = 33 }, | ||
| 425 | .{ .id = 40, .name = "Julien", .age = 35 }, | ||
| 426 | .{ .id = 60, .name = "José", .age = 40 }, | ||
| 427 | }; | ||
| 428 | |||
| 429 | for (users) |user| { | ||
| 430 | try db.exec("INSERT INTO user(id, name, age) VALUES(?, ?, ?)", user); | ||
| 431 | |||
| 432 | const rows_inserted = db.rowsAffected(); | ||
| 433 | testing.expectEqual(@as(usize, 1), rows_inserted); | ||
| 434 | } | ||
| 435 | |||
| 436 | // Read a single user | ||
| 437 | |||
| 438 | { | ||
| 439 | var stmt = try db.prepare("SELECT id, name, age FROM user WHERE id = ?", .{ .id = 20 }); | ||
| 440 | defer stmt.deinit(); | ||
| 441 | |||
| 442 | var rows = try stmt.all(User, .{ .allocator = allocator }); | ||
| 443 | for (rows) |row| { | ||
| 444 | testing.expectEqual(users[0].id, row.id); | ||
| 445 | testing.expectEqualStrings(users[0].name, row.name); | ||
| 446 | testing.expectEqual(users[0].age, row.age); | ||
| 447 | } | ||
| 448 | } | ||
| 449 | |||
| 450 | // Read all users | ||
| 451 | |||
| 452 | { | ||
| 453 | var stmt = try db.prepare("SELECT id, name, age FROM user", .{}); | ||
| 454 | defer stmt.deinit(); | ||
| 455 | |||
| 456 | var rows = try stmt.all(User, .{ .allocator = allocator }); | ||
| 457 | testing.expectEqual(@as(usize, 3), rows.len); | ||
| 458 | for (rows) |row, i| { | ||
| 459 | const exp = users[i]; | ||
| 460 | testing.expectEqual(exp.id, row.id); | ||
| 461 | testing.expectEqualStrings(exp.name, row.name); | ||
| 462 | testing.expectEqual(exp.age, row.age); | ||
| 463 | } | ||
| 464 | } | ||
| 465 | |||
| 466 | // Test with anonymous structs | ||
| 467 | |||
| 468 | { | ||
| 469 | var stmt = try db.prepare("SELECT id, name, age FROM user WHERE id = ?", .{ .id = 20 }); | ||
| 470 | defer stmt.deinit(); | ||
| 471 | |||
| 472 | var row = try stmt.one( | ||
| 473 | struct { | ||
| 474 | id: usize, | ||
| 475 | name: []const u8, | ||
| 476 | age: usize, | ||
| 477 | }, | ||
| 478 | .{ .allocator = allocator }, | ||
| 479 | ); | ||
| 480 | testing.expect(row != null); | ||
| 481 | |||
| 482 | const exp = users[0]; | ||
| 483 | testing.expectEqual(exp.id, row.?.id); | ||
| 484 | testing.expectEqualStrings(exp.name, row.?.name); | ||
| 485 | testing.expectEqual(exp.age, row.?.age); | ||
| 486 | } | ||
| 487 | |||
| 488 | // Test with a single integer | ||
| 489 | |||
| 490 | { | ||
| 491 | var stmt = try db.prepare("SELECT age FROM user WHERE id = ?", .{ .id = 20 }); | ||
| 492 | defer stmt.deinit(); | ||
| 493 | |||
| 494 | var age = try stmt.one(usize, .{}); | ||
| 495 | testing.expect(age != null); | ||
| 496 | |||
| 497 | testing.expectEqual(@as(usize, 33), age.?); | ||
| 498 | } | ||
| 499 | } | ||
| 500 | |||
| 501 | pub fn dbMode() Db.Mode { | ||
| 502 | return if (build_options.is_ci) blk: { | ||
| 503 | break :blk .{ .Memory = {} }; | ||
| 504 | } else blk: { | ||
| 505 | const path = "/tmp/zig-sqlite.db"; | ||
| 506 | std.fs.cwd().deleteFile(path) catch {}; | ||
| 507 | break :blk .{ .File = path }; | ||
| 508 | }; | ||
| 509 | } | ||