diff options
| -rw-r--r-- | sqlite.zig | 359 |
1 files changed, 211 insertions, 148 deletions
| @@ -120,6 +120,139 @@ pub const Db = struct { | |||
| 120 | } | 120 | } |
| 121 | }; | 121 | }; |
| 122 | 122 | ||
| 123 | pub fn Iterator(comptime Type: type) type { | ||
| 124 | return struct { | ||
| 125 | const Self = @This(); | ||
| 126 | |||
| 127 | const TypeInfo = @typeInfo(Type); | ||
| 128 | |||
| 129 | stmt: *c.sqlite3_stmt, | ||
| 130 | |||
| 131 | pub fn next(self: *Self, options: anytype) !?Type { | ||
| 132 | var result = c.sqlite3_step(self.stmt); | ||
| 133 | if (result == c.SQLITE_DONE) { | ||
| 134 | return null; | ||
| 135 | } | ||
| 136 | |||
| 137 | if (result != c.SQLITE_ROW) { | ||
| 138 | logger.err("unable to iterate, result: {}", .{result}); | ||
| 139 | return error.SQLiteStepError; | ||
| 140 | } | ||
| 141 | |||
| 142 | const columns = c.sqlite3_column_count(self.stmt); | ||
| 143 | |||
| 144 | return switch (TypeInfo) { | ||
| 145 | .Int => blk: { | ||
| 146 | debug.assert(columns == 1); | ||
| 147 | break :blk try self.readInt(options); | ||
| 148 | }, | ||
| 149 | .Float => blk: { | ||
| 150 | debug.assert(columns == 1); | ||
| 151 | break :blk try self.readFloat(options); | ||
| 152 | }, | ||
| 153 | .Struct => blk: { | ||
| 154 | std.debug.assert(columns == TypeInfo.Struct.fields.len); | ||
| 155 | break :blk try self.readStruct(options); | ||
| 156 | }, | ||
| 157 | else => @compileError("cannot read into type " ++ @typeName(Type)), | ||
| 158 | }; | ||
| 159 | } | ||
| 160 | |||
| 161 | fn readInt(self: *Self, options: anytype) !Type { | ||
| 162 | const n = c.sqlite3_column_int64(self.stmt, 0); | ||
| 163 | return @intCast(Type, n); | ||
| 164 | } | ||
| 165 | |||
| 166 | fn readFloat(self: *Self, options: anytype) !Type { | ||
| 167 | const d = c.sqlite3_column_double(self.stmt, 0); | ||
| 168 | return @floatCast(Type, d); | ||
| 169 | } | ||
| 170 | |||
| 171 | const ReadBytesMode = enum { | ||
| 172 | Blob, | ||
| 173 | Text, | ||
| 174 | }; | ||
| 175 | |||
| 176 | fn readBytes(self: *Self, options: anytype, mode: ReadBytesMode, _i: usize, ptr: *[]const u8) !void { | ||
| 177 | const i = @intCast(c_int, _i); | ||
| 178 | switch (mode) { | ||
| 179 | .Blob => { | ||
| 180 | const data = c.sqlite3_column_blob(self.stmt, i); | ||
| 181 | if (data == null) ptr.* = ""; | ||
| 182 | |||
| 183 | const size = @intCast(usize, c.sqlite3_column_bytes(self.stmt, i)); | ||
| 184 | |||
| 185 | var tmp = try options.allocator.alloc(u8, size); | ||
| 186 | mem.copy(u8, tmp, @ptrCast([*c]const u8, data)[0..size]); | ||
| 187 | |||
| 188 | ptr.* = tmp; | ||
| 189 | }, | ||
| 190 | .Text => { | ||
| 191 | const data = c.sqlite3_column_text(self.stmt, i); | ||
| 192 | if (data == null) ptr.* = ""; | ||
| 193 | |||
| 194 | const size = @intCast(usize, c.sqlite3_column_bytes(self.stmt, i)); | ||
| 195 | |||
| 196 | var tmp = try options.allocator.alloc(u8, size); | ||
| 197 | mem.copy(u8, tmp, @ptrCast([*c]const u8, data)[0..size]); | ||
| 198 | |||
| 199 | ptr.* = tmp; | ||
| 200 | }, | ||
| 201 | } | ||
| 202 | } | ||
| 203 | |||
| 204 | fn readStruct(self: *Self, options: anytype) !Type { | ||
| 205 | var value: Type = undefined; | ||
| 206 | |||
| 207 | inline for (@typeInfo(Type).Struct.fields) |field, _i| { | ||
| 208 | const i = @as(usize, _i); | ||
| 209 | const field_type_info = @typeInfo(field.field_type); | ||
| 210 | |||
| 211 | switch (field.field_type) { | ||
| 212 | []const u8, []u8 => { | ||
| 213 | try self.readBytes(options, .Blob, i, &@field(value, field.name)); | ||
| 214 | }, | ||
| 215 | Blob => { | ||
| 216 | try self.readBytes(options, .Blob, i, &@field(value, field.name).data); | ||
| 217 | }, | ||
| 218 | Text => { | ||
| 219 | try self.readBytes(options, .Text, i, &@field(value, field.name).data); | ||
| 220 | }, | ||
| 221 | else => switch (field_type_info) { | ||
| 222 | .Int => { | ||
| 223 | const n = c.sqlite3_column_int64(self.stmt, i); | ||
| 224 | @field(value, field.name) = @intCast(field.field_type, n); | ||
| 225 | }, | ||
| 226 | .Float => { | ||
| 227 | const f = c.sqlite3_column_double(self.stmt, i); | ||
| 228 | @field(value, field.name) = f; | ||
| 229 | }, | ||
| 230 | .Void => { | ||
| 231 | @field(value, field.name) = {}; | ||
| 232 | }, | ||
| 233 | .Array => |arr| { | ||
| 234 | switch (arr.child) { | ||
| 235 | u8 => { | ||
| 236 | const data = c.sqlite3_column_blob(self.stmt, i); | ||
| 237 | const size = @intCast(usize, c.sqlite3_column_bytes(self.stmt, i)); | ||
| 238 | |||
| 239 | if (size > @as(usize, arr.len)) return error.ArrayTooSmall; | ||
| 240 | |||
| 241 | mem.copy(u8, @field(value, field.name)[0..], @ptrCast([*c]const u8, data)[0..size]); | ||
| 242 | }, | ||
| 243 | else => @compileError("cannot populate field " ++ field.name ++ " of type array of " ++ @typeName(arr.child)), | ||
| 244 | } | ||
| 245 | }, | ||
| 246 | else => @compileError("cannot populate field " ++ field.name ++ " of type " ++ @typeName(field.field_type)), | ||
| 247 | }, | ||
| 248 | } | ||
| 249 | } | ||
| 250 | |||
| 251 | return value; | ||
| 252 | } | ||
| 253 | }; | ||
| 254 | } | ||
| 255 | |||
| 123 | pub const StatementOptions = struct {}; | 256 | pub const StatementOptions = struct {}; |
| 124 | 257 | ||
| 125 | /// Statement is a wrapper around a SQLite statement, providing high-level functions to execute | 258 | /// Statement is a wrapper around a SQLite statement, providing high-level functions to execute |
| @@ -222,7 +355,7 @@ pub fn Statement(comptime opts: StatementOptions, comptime query: ParsedQuery) t | |||
| 222 | /// } | 355 | /// } |
| 223 | /// | 356 | /// |
| 224 | /// The types are checked at comptime. | 357 | /// The types are checked at comptime. |
| 225 | pub fn bind(self: *Self, values: anytype) void { | 358 | fn bind(self: *Self, values: anytype) void { |
| 226 | const StructType = @TypeOf(values); | 359 | const StructType = @TypeOf(values); |
| 227 | const StructTypeInfo = @typeInfo(StructType).Struct; | 360 | const StructTypeInfo = @typeInfo(StructType).Struct; |
| 228 | 361 | ||
| @@ -285,6 +418,15 @@ pub fn Statement(comptime opts: StatementOptions, comptime query: ParsedQuery) t | |||
| 285 | } | 418 | } |
| 286 | } | 419 | } |
| 287 | 420 | ||
| 421 | pub fn iterator(self: *Self, comptime Type: type, values: anytype) !Iterator(Type) { | ||
| 422 | self.bind(values); | ||
| 423 | |||
| 424 | var res: Iterator(Type) = undefined; | ||
| 425 | res.stmt = self.stmt; | ||
| 426 | |||
| 427 | return res; | ||
| 428 | } | ||
| 429 | |||
| 288 | /// one reads a single row from the result set of this statement. | 430 | /// one reads a single row from the result set of this statement. |
| 289 | /// | 431 | /// |
| 290 | /// The data in the row is used to populate a value of the type `Type`. | 432 | /// The data in the row is used to populate a value of the type `Type`. |
| @@ -312,32 +454,13 @@ pub fn Statement(comptime opts: StatementOptions, comptime query: ParsedQuery) t | |||
| 312 | /// | 454 | /// |
| 313 | pub fn one(self: *Self, comptime Type: type, options: anytype, values: anytype) !?Type { | 455 | pub fn one(self: *Self, comptime Type: type, options: anytype, values: anytype) !?Type { |
| 314 | if (!comptime std.meta.trait.is(.Struct)(@TypeOf(options))) { | 456 | if (!comptime std.meta.trait.is(.Struct)(@TypeOf(options))) { |
| 315 | @compileError("options passed to all must be a struct"); | 457 | @compileError("options passed to iterator must be a struct"); |
| 316 | } | 458 | } |
| 317 | const TypeInfo = @typeInfo(Type); | ||
| 318 | |||
| 319 | self.bind(values); | ||
| 320 | 459 | ||
| 321 | var result = c.sqlite3_step(self.stmt); | 460 | var iter = try self.iterator(Type, values); |
| 322 | 461 | ||
| 323 | switch (TypeInfo) { | 462 | const row = (try iter.next(options)) orelse return null; |
| 324 | .Int => return switch (result) { | 463 | 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 | } | 464 | } |
| 342 | 465 | ||
| 343 | /// all reads all rows from the result set of this statement. | 466 | /// all reads all rows from the result set of this statement. |
| @@ -370,137 +493,18 @@ pub fn Statement(comptime opts: StatementOptions, comptime query: ParsedQuery) t | |||
| 370 | /// | 493 | /// |
| 371 | pub fn all(self: *Self, comptime Type: type, options: anytype, values: anytype) ![]Type { | 494 | pub fn all(self: *Self, comptime Type: type, options: anytype, values: anytype) ![]Type { |
| 372 | if (!comptime std.meta.trait.is(.Struct)(@TypeOf(options))) { | 495 | if (!comptime std.meta.trait.is(.Struct)(@TypeOf(options))) { |
| 373 | @compileError("options passed to all must be a struct"); | 496 | @compileError("options passed to iterator must be a struct"); |
| 374 | } | 497 | } |
| 375 | const TypeInfo = @typeInfo(Type); | 498 | var iter = try self.iterator(Type, values); |
| 376 | |||
| 377 | self.bind(values); | ||
| 378 | 499 | ||
| 379 | var rows = std.ArrayList(Type).init(options.allocator); | 500 | var rows = std.ArrayList(Type).init(options.allocator); |
| 380 | 501 | while (true) { | |
| 381 | var result = c.sqlite3_step(self.stmt); | 502 | const row = (try iter.next(options)) orelse break; |
| 382 | while (result == c.SQLITE_ROW) : (result = c.sqlite3_step(self.stmt)) { | 503 | 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 | } | 504 | } |
| 408 | 505 | ||
| 409 | return rows.span(); | 506 | return rows.span(); |
| 410 | } | 507 | } |
| 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 | }; | 508 | }; |
| 505 | } | 509 | } |
| 506 | 510 | ||
| @@ -715,6 +719,65 @@ test "sqlite: statement reset" { | |||
| 715 | } | 719 | } |
| 716 | } | 720 | } |
| 717 | 721 | ||
| 722 | test "sqlite: statement iterator" { | ||
| 723 | var arena = std.heap.ArenaAllocator.init(testing.allocator); | ||
| 724 | defer arena.deinit(); | ||
| 725 | var allocator = &arena.allocator; | ||
| 726 | |||
| 727 | var db: Db = undefined; | ||
| 728 | try db.init(testing.allocator, .{ .mode = dbMode() }); | ||
| 729 | |||
| 730 | // Create the tables | ||
| 731 | inline for (AllDDL) |ddl| { | ||
| 732 | try db.exec(ddl, .{}); | ||
| 733 | } | ||
| 734 | |||
| 735 | // Add data | ||
| 736 | var stmt = try db.prepare("INSERT INTO user(id, name, age) VALUES(?{usize}, ?{[]const u8}, ?{usize})"); | ||
| 737 | defer stmt.deinit(); | ||
| 738 | |||
| 739 | var expected_rows = std.ArrayList(TestUser).init(allocator); | ||
| 740 | var i: usize = 0; | ||
| 741 | while (i < 20) : (i += 1) { | ||
| 742 | const name = try std.fmt.allocPrint(allocator, "Vincent {}", .{i}); | ||
| 743 | const user = TestUser{ .id = i, .name = name, .age = i + 200 }; | ||
| 744 | |||
| 745 | try expected_rows.append(user); | ||
| 746 | |||
| 747 | stmt.reset(); | ||
| 748 | try stmt.exec(user); | ||
| 749 | |||
| 750 | const rows_inserted = db.rowsAffected(); | ||
| 751 | testing.expectEqual(@as(usize, 1), rows_inserted); | ||
| 752 | } | ||
| 753 | |||
| 754 | // Get the data with an iterator | ||
| 755 | var stmt2 = try db.prepare("SELECT name, age FROM user"); | ||
| 756 | defer stmt2.deinit(); | ||
| 757 | |||
| 758 | const Type = struct { | ||
| 759 | name: Text, | ||
| 760 | age: usize, | ||
| 761 | }; | ||
| 762 | |||
| 763 | var iter = try stmt2.iterator(Type, .{}); | ||
| 764 | |||
| 765 | var rows = std.ArrayList(Type).init(allocator); | ||
| 766 | while (true) { | ||
| 767 | const row = (try iter.next(.{ .allocator = allocator })) orelse break; | ||
| 768 | try rows.append(row); | ||
| 769 | } | ||
| 770 | |||
| 771 | // Check the data | ||
| 772 | testing.expectEqual(expected_rows.span().len, rows.span().len); | ||
| 773 | |||
| 774 | for (rows.span()) |row, j| { | ||
| 775 | const exp_row = expected_rows.span()[j]; | ||
| 776 | testing.expectEqualStrings(exp_row.name, row.name.data); | ||
| 777 | testing.expectEqual(exp_row.age, row.age); | ||
| 778 | } | ||
| 779 | } | ||
| 780 | |||
| 718 | fn dbMode() Db.Mode { | 781 | fn dbMode() Db.Mode { |
| 719 | return if (build_options.is_ci) blk: { | 782 | return if (build_options.is_ci) blk: { |
| 720 | break :blk .{ .Memory = {} }; | 783 | break :blk .{ .Memory = {} }; |