diff options
Diffstat (limited to 'sqlite.zig')
| -rw-r--r-- | sqlite.zig | 400 |
1 files changed, 252 insertions, 148 deletions
| @@ -120,6 +120,161 @@ pub const Db = struct { | |||
| 120 | } | 120 | } |
| 121 | }; | 121 | }; |
| 122 | 122 | ||
| 123 | /// Iterator allows iterating over a result set. | ||
| 124 | /// | ||
| 125 | /// Each call to `next` returns the next row of the result set, or null if the result set is exhausted. | ||
| 126 | /// Each row will have the type `Type` so the columns returned in the result set must be compatible with this type. | ||
| 127 | /// | ||
| 128 | /// Here is an example of how to use the iterator: | ||
| 129 | /// | ||
| 130 | /// const User = struct { | ||
| 131 | /// name: Text, | ||
| 132 | /// age: u16, | ||
| 133 | /// }; | ||
| 134 | /// | ||
| 135 | /// var stmt = try db.prepare("SELECT name, age FROM user"); | ||
| 136 | /// defer stmt.deinit(); | ||
| 137 | /// | ||
| 138 | /// var iter = try stmt.iterator(User, .{}); | ||
| 139 | /// while (true) { | ||
| 140 | /// const row: User = (try iter.next(.{})) orelse break; | ||
| 141 | /// ... | ||
| 142 | /// } | ||
| 143 | /// | ||
| 144 | /// The iterator _must not_ outlive the statement. | ||
| 145 | pub fn Iterator(comptime Type: type) type { | ||
| 146 | return struct { | ||
| 147 | const Self = @This(); | ||
| 148 | |||
| 149 | const TypeInfo = @typeInfo(Type); | ||
| 150 | |||
| 151 | stmt: *c.sqlite3_stmt, | ||
| 152 | |||
| 153 | pub fn next(self: *Self, options: anytype) !?Type { | ||
| 154 | var result = c.sqlite3_step(self.stmt); | ||
| 155 | if (result == c.SQLITE_DONE) { | ||
| 156 | return null; | ||
| 157 | } | ||
| 158 | |||
| 159 | if (result != c.SQLITE_ROW) { | ||
| 160 | logger.err("unable to iterate, result: {}", .{result}); | ||
| 161 | return error.SQLiteStepError; | ||
| 162 | } | ||
| 163 | |||
| 164 | const columns = c.sqlite3_column_count(self.stmt); | ||
| 165 | |||
| 166 | return switch (TypeInfo) { | ||
| 167 | .Int => blk: { | ||
| 168 | debug.assert(columns == 1); | ||
| 169 | break :blk try self.readInt(options); | ||
| 170 | }, | ||
| 171 | .Float => blk: { | ||
| 172 | debug.assert(columns == 1); | ||
| 173 | break :blk try self.readFloat(options); | ||
| 174 | }, | ||
| 175 | .Struct => blk: { | ||
| 176 | std.debug.assert(columns == TypeInfo.Struct.fields.len); | ||
| 177 | break :blk try self.readStruct(options); | ||
| 178 | }, | ||
| 179 | else => @compileError("cannot read into type " ++ @typeName(Type)), | ||
| 180 | }; | ||
| 181 | } | ||
| 182 | |||
| 183 | fn readInt(self: *Self, options: anytype) !Type { | ||
| 184 | const n = c.sqlite3_column_int64(self.stmt, 0); | ||
| 185 | return @intCast(Type, n); | ||
| 186 | } | ||
| 187 | |||
| 188 | fn readFloat(self: *Self, options: anytype) !Type { | ||
| 189 | const d = c.sqlite3_column_double(self.stmt, 0); | ||
| 190 | return @floatCast(Type, d); | ||
| 191 | } | ||
| 192 | |||
| 193 | const ReadBytesMode = enum { | ||
| 194 | Blob, | ||
| 195 | Text, | ||
| 196 | }; | ||
| 197 | |||
| 198 | fn readBytes(self: *Self, options: anytype, mode: ReadBytesMode, _i: usize, ptr: *[]const u8) !void { | ||
| 199 | const i = @intCast(c_int, _i); | ||
| 200 | switch (mode) { | ||
| 201 | .Blob => { | ||
| 202 | const data = c.sqlite3_column_blob(self.stmt, i); | ||
| 203 | if (data == null) ptr.* = ""; | ||
| 204 | |||
| 205 | const size = @intCast(usize, c.sqlite3_column_bytes(self.stmt, i)); | ||
| 206 | |||
| 207 | var tmp = try options.allocator.alloc(u8, size); | ||
| 208 | mem.copy(u8, tmp, @ptrCast([*c]const u8, data)[0..size]); | ||
| 209 | |||
| 210 | ptr.* = tmp; | ||
| 211 | }, | ||
| 212 | .Text => { | ||
| 213 | const data = c.sqlite3_column_text(self.stmt, i); | ||
| 214 | if (data == null) ptr.* = ""; | ||
| 215 | |||
| 216 | const size = @intCast(usize, c.sqlite3_column_bytes(self.stmt, i)); | ||
| 217 | |||
| 218 | var tmp = try options.allocator.alloc(u8, size); | ||
| 219 | mem.copy(u8, tmp, @ptrCast([*c]const u8, data)[0..size]); | ||
| 220 | |||
| 221 | ptr.* = tmp; | ||
| 222 | }, | ||
| 223 | } | ||
| 224 | } | ||
| 225 | |||
| 226 | fn readStruct(self: *Self, options: anytype) !Type { | ||
| 227 | var value: Type = undefined; | ||
| 228 | |||
| 229 | inline for (@typeInfo(Type).Struct.fields) |field, _i| { | ||
| 230 | const i = @as(usize, _i); | ||
| 231 | const field_type_info = @typeInfo(field.field_type); | ||
| 232 | |||
| 233 | switch (field.field_type) { | ||
| 234 | []const u8, []u8 => { | ||
| 235 | try self.readBytes(options, .Blob, i, &@field(value, field.name)); | ||
| 236 | }, | ||
| 237 | Blob => { | ||
| 238 | try self.readBytes(options, .Blob, i, &@field(value, field.name).data); | ||
| 239 | }, | ||
| 240 | Text => { | ||
| 241 | try self.readBytes(options, .Text, i, &@field(value, field.name).data); | ||
| 242 | }, | ||
| 243 | else => switch (field_type_info) { | ||
| 244 | .Int => { | ||
| 245 | const n = c.sqlite3_column_int64(self.stmt, i); | ||
| 246 | @field(value, field.name) = @intCast(field.field_type, n); | ||
| 247 | }, | ||
| 248 | .Float => { | ||
| 249 | const f = c.sqlite3_column_double(self.stmt, i); | ||
| 250 | @field(value, field.name) = f; | ||
| 251 | }, | ||
| 252 | .Void => { | ||
| 253 | @field(value, field.name) = {}; | ||
| 254 | }, | ||
| 255 | .Array => |arr| { | ||
| 256 | switch (arr.child) { | ||
| 257 | u8 => { | ||
| 258 | const data = c.sqlite3_column_blob(self.stmt, i); | ||
| 259 | const size = @intCast(usize, c.sqlite3_column_bytes(self.stmt, i)); | ||
| 260 | |||
| 261 | if (size > @as(usize, arr.len)) return error.ArrayTooSmall; | ||
| 262 | |||
| 263 | mem.copy(u8, @field(value, field.name)[0..], @ptrCast([*c]const u8, data)[0..size]); | ||
| 264 | }, | ||
| 265 | else => @compileError("cannot populate field " ++ field.name ++ " of type array of " ++ @typeName(arr.child)), | ||
| 266 | } | ||
| 267 | }, | ||
| 268 | else => @compileError("cannot populate field " ++ field.name ++ " of type " ++ @typeName(field.field_type)), | ||
| 269 | }, | ||
| 270 | } | ||
| 271 | } | ||
| 272 | |||
| 273 | return value; | ||
| 274 | } | ||
| 275 | }; | ||
| 276 | } | ||
| 277 | |||
| 123 | pub const StatementOptions = struct {}; | 278 | pub const StatementOptions = struct {}; |
| 124 | 279 | ||
| 125 | /// Statement is a wrapper around a SQLite statement, providing high-level functions to execute | 280 | /// Statement is a wrapper around a SQLite statement, providing high-level functions to execute |
| @@ -222,7 +377,7 @@ pub fn Statement(comptime opts: StatementOptions, comptime query: ParsedQuery) t | |||
| 222 | /// } | 377 | /// } |
| 223 | /// | 378 | /// |
| 224 | /// The types are checked at comptime. | 379 | /// The types are checked at comptime. |
| 225 | pub fn bind(self: *Self, values: anytype) void { | 380 | fn bind(self: *Self, values: anytype) void { |
| 226 | const StructType = @TypeOf(values); | 381 | const StructType = @TypeOf(values); |
| 227 | const StructTypeInfo = @typeInfo(StructType).Struct; | 382 | const StructTypeInfo = @typeInfo(StructType).Struct; |
| 228 | 383 | ||
| @@ -285,6 +440,34 @@ pub fn Statement(comptime opts: StatementOptions, comptime query: ParsedQuery) t | |||
| 285 | } | 440 | } |
| 286 | } | 441 | } |
| 287 | 442 | ||
| 443 | /// iterator returns an iterator to read data from the result set, one row at a time. | ||
| 444 | /// | ||
| 445 | /// The data in the row is used to populate a value of the type `Type`. | ||
| 446 | /// This means that `Type` must have as many fields as is returned in the query | ||
| 447 | /// executed by this statement. | ||
| 448 | /// This also means that the type of each field must be compatible with the SQLite type. | ||
| 449 | /// | ||
| 450 | /// Here is an example of how to use the iterator: | ||
| 451 | /// | ||
| 452 | /// var iter = try stmt.iterator(usize, .{}); | ||
| 453 | /// while (true) { | ||
| 454 | /// const row = (try iter.next(.{})) orelse break; | ||
| 455 | /// ... | ||
| 456 | /// } | ||
| 457 | /// | ||
| 458 | /// The `values` tuple is used for the bind parameters. It must have as many fields as there are bind markers | ||
| 459 | /// in the input query string. | ||
| 460 | /// | ||
| 461 | /// The iterator _must not_ outlive the statement. | ||
| 462 | pub fn iterator(self: *Self, comptime Type: type, values: anytype) !Iterator(Type) { | ||
| 463 | self.bind(values); | ||
| 464 | |||
| 465 | var res: Iterator(Type) = undefined; | ||
| 466 | res.stmt = self.stmt; | ||
| 467 | |||
| 468 | return res; | ||
| 469 | } | ||
| 470 | |||
| 288 | /// one reads a single row from the result set of this statement. | 471 | /// one reads a single row from the result set of this statement. |
| 289 | /// | 472 | /// |
| 290 | /// The data in the row is used to populate a value of the type `Type`. | 473 | /// The data in the row is used to populate a value of the type `Type`. |
| @@ -312,32 +495,13 @@ pub fn Statement(comptime opts: StatementOptions, comptime query: ParsedQuery) t | |||
| 312 | /// | 495 | /// |
| 313 | pub fn one(self: *Self, comptime Type: type, options: anytype, values: anytype) !?Type { | 496 | pub fn one(self: *Self, comptime Type: type, options: anytype, values: anytype) !?Type { |
| 314 | if (!comptime std.meta.trait.is(.Struct)(@TypeOf(options))) { | 497 | if (!comptime std.meta.trait.is(.Struct)(@TypeOf(options))) { |
| 315 | @compileError("options passed to all must be a struct"); | 498 | @compileError("options passed to iterator must be a struct"); |
| 316 | } | 499 | } |
| 317 | const TypeInfo = @typeInfo(Type); | ||
| 318 | |||
| 319 | self.bind(values); | ||
| 320 | 500 | ||
| 321 | var result = c.sqlite3_step(self.stmt); | 501 | var iter = try self.iterator(Type, values); |
| 322 | 502 | ||
| 323 | switch (TypeInfo) { | 503 | const row = (try iter.next(options)) orelse return null; |
| 324 | .Int => return switch (result) { | 504 | return row; |
| 325 | c.SQLITE_ROW => try self.readInt(Type, options), | ||
| 326 | c.SQLITE_DONE => null, | ||
| 327 | else => std.debug.panic("invalid result {}", .{result}), | ||
| 328 | }, | ||
| 329 | .Float => return switch (result) { | ||
| 330 | c.SQLITE_ROW => try self.readFloat(Type, options), | ||
| 331 | c.SQLITE_DONE => null, | ||
| 332 | else => std.debug.panic("invalid result {}", .{result}), | ||
| 333 | }, | ||
| 334 | .Struct => return switch (result) { | ||
| 335 | c.SQLITE_ROW => try self.readStruct(Type, options), | ||
| 336 | c.SQLITE_DONE => null, | ||
| 337 | else => std.debug.panic("invalid result {}", .{result}), | ||
| 338 | }, | ||
| 339 | else => @compileError("cannot read into type " ++ @typeName(Type)), | ||
| 340 | } | ||
| 341 | } | 505 | } |
| 342 | 506 | ||
| 343 | /// all reads all rows from the result set of this statement. | 507 | /// all reads all rows from the result set of this statement. |
| @@ -370,137 +534,18 @@ pub fn Statement(comptime opts: StatementOptions, comptime query: ParsedQuery) t | |||
| 370 | /// | 534 | /// |
| 371 | pub fn all(self: *Self, comptime Type: type, options: anytype, values: anytype) ![]Type { | 535 | pub fn all(self: *Self, comptime Type: type, options: anytype, values: anytype) ![]Type { |
| 372 | if (!comptime std.meta.trait.is(.Struct)(@TypeOf(options))) { | 536 | if (!comptime std.meta.trait.is(.Struct)(@TypeOf(options))) { |
| 373 | @compileError("options passed to all must be a struct"); | 537 | @compileError("options passed to iterator must be a struct"); |
| 374 | } | 538 | } |
| 375 | const TypeInfo = @typeInfo(Type); | 539 | var iter = try self.iterator(Type, values); |
| 376 | |||
| 377 | self.bind(values); | ||
| 378 | 540 | ||
| 379 | var rows = std.ArrayList(Type).init(options.allocator); | 541 | var rows = std.ArrayList(Type).init(options.allocator); |
| 380 | 542 | while (true) { | |
| 381 | var result = c.sqlite3_step(self.stmt); | 543 | const row = (try iter.next(options)) orelse break; |
| 382 | while (result == c.SQLITE_ROW) : (result = c.sqlite3_step(self.stmt)) { | 544 | try rows.append(row); |
| 383 | const columns = c.sqlite3_column_count(self.stmt); | ||
| 384 | |||
| 385 | var value = switch (TypeInfo) { | ||
| 386 | .Int => blk: { | ||
| 387 | debug.assert(columns == 1); | ||
| 388 | break :blk try self.readInt(Type, options); | ||
| 389 | }, | ||
| 390 | .Float => blk: { | ||
| 391 | debug.assert(columns == 1); | ||
| 392 | break :blk try self.readFloat(Type, options); | ||
| 393 | }, | ||
| 394 | .Struct => blk: { | ||
| 395 | std.debug.assert(columns == @typeInfo(Type).Struct.fields.len); | ||
| 396 | break :blk try self.readStruct(Type, options); | ||
| 397 | }, | ||
| 398 | else => @compileError("cannot read into type " ++ @typeName(Type)), | ||
| 399 | }; | ||
| 400 | |||
| 401 | try rows.append(value); | ||
| 402 | } | ||
| 403 | |||
| 404 | if (result != c.SQLITE_DONE) { | ||
| 405 | logger.err("unable to iterate, result: {}", .{result}); | ||
| 406 | return error.SQLiteStepError; | ||
| 407 | } | 545 | } |
| 408 | 546 | ||
| 409 | return rows.span(); | 547 | return rows.span(); |
| 410 | } | 548 | } |
| 411 | |||
| 412 | fn readInt(self: *Self, comptime Type: type, options: anytype) !Type { | ||
| 413 | const n = c.sqlite3_column_int64(self.stmt, 0); | ||
| 414 | return @intCast(Type, n); | ||
| 415 | } | ||
| 416 | |||
| 417 | fn readFloat(self: *Self, comptime Type: type, options: anytype) !Type { | ||
| 418 | const d = c.sqlite3_column_double(self.stmt, 0); | ||
| 419 | return @floatCast(Type, d); | ||
| 420 | } | ||
| 421 | |||
| 422 | const ReadBytesMode = enum { | ||
| 423 | Blob, | ||
| 424 | Text, | ||
| 425 | }; | ||
| 426 | |||
| 427 | fn readBytes(self: *Self, allocator: *mem.Allocator, mode: ReadBytesMode, _i: usize, ptr: *[]const u8) !void { | ||
| 428 | const i = @intCast(c_int, _i); | ||
| 429 | switch (mode) { | ||
| 430 | .Blob => { | ||
| 431 | const data = c.sqlite3_column_blob(self.stmt, i); | ||
| 432 | if (data == null) ptr.* = ""; | ||
| 433 | |||
| 434 | const size = @intCast(usize, c.sqlite3_column_bytes(self.stmt, i)); | ||
| 435 | |||
| 436 | var tmp = try allocator.alloc(u8, size); | ||
| 437 | mem.copy(u8, tmp, @ptrCast([*c]const u8, data)[0..size]); | ||
| 438 | |||
| 439 | ptr.* = tmp; | ||
| 440 | }, | ||
| 441 | .Text => { | ||
| 442 | const data = c.sqlite3_column_text(self.stmt, i); | ||
| 443 | if (data == null) ptr.* = ""; | ||
| 444 | |||
| 445 | const size = @intCast(usize, c.sqlite3_column_bytes(self.stmt, i)); | ||
| 446 | |||
| 447 | var tmp = try allocator.alloc(u8, size); | ||
| 448 | mem.copy(u8, tmp, @ptrCast([*c]const u8, data)[0..size]); | ||
| 449 | |||
| 450 | ptr.* = tmp; | ||
| 451 | }, | ||
| 452 | } | ||
| 453 | } | ||
| 454 | |||
| 455 | fn readStruct(self: *Self, comptime Type: type, options: anytype) !Type { | ||
| 456 | var value: Type = undefined; | ||
| 457 | |||
| 458 | inline for (@typeInfo(Type).Struct.fields) |field, _i| { | ||
| 459 | const i = @as(usize, _i); | ||
| 460 | const field_type_info = @typeInfo(field.field_type); | ||
| 461 | |||
| 462 | switch (field.field_type) { | ||
| 463 | []const u8, []u8 => { | ||
| 464 | try self.readBytes(options.allocator, .Blob, i, &@field(value, field.name)); | ||
| 465 | }, | ||
| 466 | Blob => { | ||
| 467 | try self.readBytes(options.allocator, .Blob, i, &@field(value, field.name).data); | ||
| 468 | }, | ||
| 469 | Text => { | ||
| 470 | try self.readBytes(options.allocator, .Text, i, &@field(value, field.name).data); | ||
| 471 | }, | ||
| 472 | else => switch (field_type_info) { | ||
| 473 | .Int => { | ||
| 474 | const n = c.sqlite3_column_int64(self.stmt, i); | ||
| 475 | @field(value, field.name) = @intCast(field.field_type, n); | ||
| 476 | }, | ||
| 477 | .Float => { | ||
| 478 | const f = c.sqlite3_column_double(self.stmt, i); | ||
| 479 | @field(value, field.name) = f; | ||
| 480 | }, | ||
| 481 | .Void => { | ||
| 482 | @field(value, field.name) = {}; | ||
| 483 | }, | ||
| 484 | .Array => |arr| { | ||
| 485 | switch (arr.child) { | ||
| 486 | u8 => { | ||
| 487 | const data = c.sqlite3_column_blob(self.stmt, i); | ||
| 488 | const size = @intCast(usize, c.sqlite3_column_bytes(self.stmt, i)); | ||
| 489 | |||
| 490 | if (size > @as(usize, arr.len)) return error.ArrayTooSmall; | ||
| 491 | |||
| 492 | mem.copy(u8, @field(value, field.name)[0..], @ptrCast([*c]const u8, data)[0..size]); | ||
| 493 | }, | ||
| 494 | else => @compileError("cannot populate field " ++ field.name ++ " of type array of " ++ @typeName(arr.child)), | ||
| 495 | } | ||
| 496 | }, | ||
| 497 | else => @compileError("cannot populate field " ++ field.name ++ " of type " ++ @typeName(field.field_type)), | ||
| 498 | }, | ||
| 499 | } | ||
| 500 | } | ||
| 501 | |||
| 502 | return value; | ||
| 503 | } | ||
| 504 | }; | 549 | }; |
| 505 | } | 550 | } |
| 506 | 551 | ||
| @@ -715,6 +760,65 @@ test "sqlite: statement reset" { | |||
| 715 | } | 760 | } |
| 716 | } | 761 | } |
| 717 | 762 | ||
| 763 | test "sqlite: statement iterator" { | ||
| 764 | var arena = std.heap.ArenaAllocator.init(testing.allocator); | ||
| 765 | defer arena.deinit(); | ||
| 766 | var allocator = &arena.allocator; | ||
| 767 | |||
| 768 | var db: Db = undefined; | ||
| 769 | try db.init(testing.allocator, .{ .mode = dbMode() }); | ||
| 770 | |||
| 771 | // Create the tables | ||
| 772 | inline for (AllDDL) |ddl| { | ||
| 773 | try db.exec(ddl, .{}); | ||
| 774 | } | ||
| 775 | |||
| 776 | // Add data | ||
| 777 | var stmt = try db.prepare("INSERT INTO user(id, name, age) VALUES(?{usize}, ?{[]const u8}, ?{usize})"); | ||
| 778 | defer stmt.deinit(); | ||
| 779 | |||
| 780 | var expected_rows = std.ArrayList(TestUser).init(allocator); | ||
| 781 | var i: usize = 0; | ||
| 782 | while (i < 20) : (i += 1) { | ||
| 783 | const name = try std.fmt.allocPrint(allocator, "Vincent {}", .{i}); | ||
| 784 | const user = TestUser{ .id = i, .name = name, .age = i + 200 }; | ||
| 785 | |||
| 786 | try expected_rows.append(user); | ||
| 787 | |||
| 788 | stmt.reset(); | ||
| 789 | try stmt.exec(user); | ||
| 790 | |||
| 791 | const rows_inserted = db.rowsAffected(); | ||
| 792 | testing.expectEqual(@as(usize, 1), rows_inserted); | ||
| 793 | } | ||
| 794 | |||
| 795 | // Get the data with an iterator | ||
| 796 | var stmt2 = try db.prepare("SELECT name, age FROM user"); | ||
| 797 | defer stmt2.deinit(); | ||
| 798 | |||
| 799 | const Type = struct { | ||
| 800 | name: Text, | ||
| 801 | age: usize, | ||
| 802 | }; | ||
| 803 | |||
| 804 | var iter = try stmt2.iterator(Type, .{}); | ||
| 805 | |||
| 806 | var rows = std.ArrayList(Type).init(allocator); | ||
| 807 | while (true) { | ||
| 808 | const row = (try iter.next(.{ .allocator = allocator })) orelse break; | ||
| 809 | try rows.append(row); | ||
| 810 | } | ||
| 811 | |||
| 812 | // Check the data | ||
| 813 | testing.expectEqual(expected_rows.span().len, rows.span().len); | ||
| 814 | |||
| 815 | for (rows.span()) |row, j| { | ||
| 816 | const exp_row = expected_rows.span()[j]; | ||
| 817 | testing.expectEqualStrings(exp_row.name, row.name.data); | ||
| 818 | testing.expectEqual(exp_row.age, row.age); | ||
| 819 | } | ||
| 820 | } | ||
| 821 | |||
| 718 | fn dbMode() Db.Mode { | 822 | fn dbMode() Db.Mode { |
| 719 | return if (build_options.is_ci) blk: { | 823 | return if (build_options.is_ci) blk: { |
| 720 | break :blk .{ .Memory = {} }; | 824 | break :blk .{ .Memory = {} }; |