diff options
| author | 2020-11-12 15:10:56 +0100 | |
|---|---|---|
| committer | 2020-11-12 15:10:56 +0100 | |
| commit | 55c1a3c35c35e19b5ca2b83f076e818f3ded9f94 (patch) | |
| tree | c9444ab595c27cecc2bf8ee718437fc2055c0858 /sqlite.zig | |
| parent | update requirements (diff) | |
| parent | allow untyped bind markers (diff) | |
| download | zig-sqlite-55c1a3c35c35e19b5ca2b83f076e818f3ded9f94.tar.gz zig-sqlite-55c1a3c35c35e19b5ca2b83f076e818f3ded9f94.tar.xz zig-sqlite-55c1a3c35c35e19b5ca2b83f076e818f3ded9f94.zip | |
Merge branch 'bind-parameters-types' into master
Diffstat (limited to '')
| -rw-r--r-- | sqlite.zig | 164 |
1 files changed, 107 insertions, 57 deletions
| @@ -8,6 +8,8 @@ const c = @cImport({ | |||
| 8 | @cInclude("sqlite3.h"); | 8 | @cInclude("sqlite3.h"); |
| 9 | }); | 9 | }); |
| 10 | 10 | ||
| 11 | usingnamespace @import("query.zig"); | ||
| 12 | |||
| 11 | const logger = std.log.scoped(.sqlite); | 13 | const logger = std.log.scoped(.sqlite); |
| 12 | 14 | ||
| 13 | /// Db is a wrapper around a SQLite database, providing high-level functions for executing queries. | 15 | /// Db is a wrapper around a SQLite database, providing high-level functions for executing queries. |
| @@ -106,8 +108,10 @@ pub const Db = struct { | |||
| 106 | /// The statement returned is only compatible with the number of bind markers in the input query. | 108 | /// The statement returned is only compatible with the number of bind markers in the input query. |
| 107 | /// This is done because we type check the bind parameters when executing the statement later. | 109 | /// This is done because we type check the bind parameters when executing the statement later. |
| 108 | /// | 110 | /// |
| 109 | pub fn prepare(self: *Self, comptime query: []const u8) !Statement(StatementOptions.from(query)) { | 111 | pub fn prepare(self: *Self, comptime query: []const u8) !Statement(.{}, ParsedQuery.from(query)) { |
| 110 | return Statement(comptime StatementOptions.from(query)).prepare(self, 0, query); | 112 | @setEvalBranchQuota(3000); |
| 113 | const parsed_query = ParsedQuery.from(query); | ||
| 114 | return Statement(.{}, comptime parsed_query).prepare(self, 0); | ||
| 111 | } | 115 | } |
| 112 | 116 | ||
| 113 | /// rowsAffected returns the number of rows affected by the last statement executed. | 117 | /// rowsAffected returns the number of rows affected by the last statement executed. |
| @@ -116,28 +120,7 @@ pub const Db = struct { | |||
| 116 | } | 120 | } |
| 117 | }; | 121 | }; |
| 118 | 122 | ||
| 119 | /// Bytes is used to represent a byte slice with its SQLite datatype. | 123 | pub const StatementOptions = struct {}; |
| 120 | /// | ||
| 121 | /// Since Zig doesn't have strings we can't tell if a []u8 must be stored as a SQLite TEXT or BLOB, | ||
| 122 | /// this type can be used to communicate this when executing a statement. | ||
| 123 | /// | ||
| 124 | /// If a []u8 or []const u8 is passed as bind parameter it will be treated as TEXT. | ||
| 125 | pub const Bytes = union(enum) { | ||
| 126 | Blob: []const u8, | ||
| 127 | Text: []const u8, | ||
| 128 | }; | ||
| 129 | |||
| 130 | pub const StatementOptions = struct { | ||
| 131 | const Self = @This(); | ||
| 132 | |||
| 133 | bind_markers: usize, | ||
| 134 | |||
| 135 | fn from(comptime query: []const u8) Self { | ||
| 136 | return Self{ | ||
| 137 | .bind_markers = std.mem.count(u8, query, "?"), | ||
| 138 | }; | ||
| 139 | } | ||
| 140 | }; | ||
| 141 | 124 | ||
| 142 | /// Statement is a wrapper around a SQLite statement, providing high-level functions to execute | 125 | /// Statement is a wrapper around a SQLite statement, providing high-level functions to execute |
| 143 | /// a statement and retrieve rows for SELECT queries. | 126 | /// a statement and retrieve rows for SELECT queries. |
| @@ -172,19 +155,21 @@ pub const StatementOptions = struct { | |||
| 172 | /// | 155 | /// |
| 173 | /// Look at aach function for more complete documentation. | 156 | /// Look at aach function for more complete documentation. |
| 174 | /// | 157 | /// |
| 175 | pub fn Statement(comptime opts: StatementOptions) type { | 158 | pub fn Statement(comptime opts: StatementOptions, comptime query: ParsedQuery) type { |
| 176 | return struct { | 159 | return struct { |
| 177 | const Self = @This(); | 160 | const Self = @This(); |
| 178 | 161 | ||
| 179 | stmt: *c.sqlite3_stmt, | 162 | stmt: *c.sqlite3_stmt, |
| 180 | 163 | ||
| 181 | fn prepare(db: *Db, flags: c_uint, comptime query: []const u8) !Self { | 164 | fn prepare(db: *Db, flags: c_uint) !Self { |
| 182 | var stmt = blk: { | 165 | var stmt = blk: { |
| 166 | const real_query = query.getQuery(); | ||
| 167 | |||
| 183 | var tmp: ?*c.sqlite3_stmt = undefined; | 168 | var tmp: ?*c.sqlite3_stmt = undefined; |
| 184 | const result = c.sqlite3_prepare_v3( | 169 | const result = c.sqlite3_prepare_v3( |
| 185 | db.db, | 170 | db.db, |
| 186 | query.ptr, | 171 | real_query.ptr, |
| 187 | @intCast(c_int, query.len), | 172 | @intCast(c_int, real_query.len), |
| 188 | flags, | 173 | flags, |
| 189 | &tmp, | 174 | &tmp, |
| 190 | null, | 175 | null, |
| @@ -212,11 +197,19 @@ pub fn Statement(comptime opts: StatementOptions) type { | |||
| 212 | const StructType = @TypeOf(values); | 197 | const StructType = @TypeOf(values); |
| 213 | const StructTypeInfo = @typeInfo(StructType).Struct; | 198 | const StructTypeInfo = @typeInfo(StructType).Struct; |
| 214 | 199 | ||
| 215 | if (comptime opts.bind_markers != StructTypeInfo.fields.len) { | 200 | if (comptime query.nb_bind_markers != StructTypeInfo.fields.len) { |
| 216 | @compileError("number of bind markers not equal to number of fields"); | 201 | @compileError("number of bind markers not equal to number of fields"); |
| 217 | } | 202 | } |
| 218 | 203 | ||
| 219 | inline for (StructTypeInfo.fields) |struct_field, _i| { | 204 | inline for (StructTypeInfo.fields) |struct_field, _i| { |
| 205 | const bind_marker = query.bind_markers[_i]; | ||
| 206 | switch (bind_marker) { | ||
| 207 | .Typed => |typ| if (struct_field.field_type != typ) { | ||
| 208 | @compileError("value type " ++ @typeName(struct_field.field_type) ++ " is not the bind marker type " ++ @typeName(typ)); | ||
| 209 | }, | ||
| 210 | .Untyped => {}, | ||
| 211 | } | ||
| 212 | |||
| 220 | const i = @as(usize, _i); | 213 | const i = @as(usize, _i); |
| 221 | const field_type_info = @typeInfo(struct_field.field_type); | 214 | const field_type_info = @typeInfo(struct_field.field_type); |
| 222 | const field_value = @field(values, struct_field.name); | 215 | const field_value = @field(values, struct_field.name); |
| @@ -226,10 +219,8 @@ pub fn Statement(comptime opts: StatementOptions) type { | |||
| 226 | []const u8, []u8 => { | 219 | []const u8, []u8 => { |
| 227 | _ = c.sqlite3_bind_text(self.stmt, column, field_value.ptr, @intCast(c_int, field_value.len), null); | 220 | _ = c.sqlite3_bind_text(self.stmt, column, field_value.ptr, @intCast(c_int, field_value.len), null); |
| 228 | }, | 221 | }, |
| 229 | Bytes => switch (field_value) { | 222 | Text => _ = c.sqlite3_bind_text(self.stmt, column, field_value.data.ptr, @intCast(c_int, field_value.data.len), null), |
| 230 | .Text => |v| _ = c.sqlite3_bind_text(self.stmt, column, v.ptr, @intCast(c_int, v.len), null), | 223 | Blob => _ = c.sqlite3_bind_blob(self.stmt, column, field_value.data.ptr, @intCast(c_int, field_value.data.len), null), |
| 231 | .Blob => |v| _ = c.sqlite3_bind_blob(self.stmt, column, v.ptr, @intCast(c_int, v.len), null), | ||
| 232 | }, | ||
| 233 | else => switch (field_type_info) { | 224 | else => switch (field_type_info) { |
| 234 | .Int, .ComptimeInt => _ = c.sqlite3_bind_int64(self.stmt, column, @intCast(c_longlong, field_value)), | 225 | .Int, .ComptimeInt => _ = c.sqlite3_bind_int64(self.stmt, column, @intCast(c_longlong, field_value)), |
| 235 | .Float, .ComptimeFloat => _ = c.sqlite3_bind_double(self.stmt, column, field_value), | 226 | .Float, .ComptimeFloat => _ = c.sqlite3_bind_double(self.stmt, column, field_value), |
| @@ -385,6 +376,39 @@ pub fn Statement(comptime opts: StatementOptions) type { | |||
| 385 | return @intCast(Type, n); | 376 | return @intCast(Type, n); |
| 386 | } | 377 | } |
| 387 | 378 | ||
| 379 | const ReadBytesMode = enum { | ||
| 380 | Blob, | ||
| 381 | Text, | ||
| 382 | }; | ||
| 383 | |||
| 384 | fn readBytes(self: *Self, allocator: *mem.Allocator, mode: ReadBytesMode, _i: usize, ptr: *[]const u8) !void { | ||
| 385 | const i = @intCast(c_int, _i); | ||
| 386 | switch (mode) { | ||
| 387 | .Blob => { | ||
| 388 | const data = c.sqlite3_column_blob(self.stmt, i); | ||
| 389 | if (data == null) ptr.* = ""; | ||
| 390 | |||
| 391 | const size = @intCast(usize, c.sqlite3_column_bytes(self.stmt, i)); | ||
| 392 | |||
| 393 | var tmp = try allocator.alloc(u8, size); | ||
| 394 | mem.copy(u8, tmp, @ptrCast([*c]const u8, data)[0..size]); | ||
| 395 | |||
| 396 | ptr.* = tmp; | ||
| 397 | }, | ||
| 398 | .Text => { | ||
| 399 | const data = c.sqlite3_column_text(self.stmt, i); | ||
| 400 | if (data == null) ptr.* = ""; | ||
| 401 | |||
| 402 | const size = @intCast(usize, c.sqlite3_column_bytes(self.stmt, i)); | ||
| 403 | |||
| 404 | var tmp = try allocator.alloc(u8, size); | ||
| 405 | mem.copy(u8, tmp, @ptrCast([*c]const u8, data)[0..size]); | ||
| 406 | |||
| 407 | ptr.* = tmp; | ||
| 408 | }, | ||
| 409 | } | ||
| 410 | } | ||
| 411 | |||
| 388 | fn readStruct(self: *Self, comptime Type: type, options: anytype) !Type { | 412 | fn readStruct(self: *Self, comptime Type: type, options: anytype) !Type { |
| 389 | var value: Type = undefined; | 413 | var value: Type = undefined; |
| 390 | 414 | ||
| @@ -394,17 +418,13 @@ pub fn Statement(comptime opts: StatementOptions) type { | |||
| 394 | 418 | ||
| 395 | switch (field.field_type) { | 419 | switch (field.field_type) { |
| 396 | []const u8, []u8 => { | 420 | []const u8, []u8 => { |
| 397 | const data = c.sqlite3_column_blob(self.stmt, i); | 421 | try self.readBytes(options.allocator, .Blob, i, &@field(value, field.name)); |
| 398 | if (data == null) { | 422 | }, |
| 399 | @field(value, field.name) = ""; | 423 | Blob => { |
| 400 | } else { | 424 | try self.readBytes(options.allocator, .Blob, i, &@field(value, field.name).data); |
| 401 | const size = @intCast(usize, c.sqlite3_column_bytes(self.stmt, i)); | 425 | }, |
| 402 | 426 | Text => { | |
| 403 | var tmp = try options.allocator.alloc(u8, size); | 427 | try self.readBytes(options.allocator, .Text, i, &@field(value, field.name).data); |
| 404 | mem.copy(u8, tmp, @ptrCast([*c]const u8, data)[0..size]); | ||
| 405 | |||
| 406 | @field(value, field.name) = tmp; | ||
| 407 | } | ||
| 408 | }, | 428 | }, |
| 409 | else => switch (field_type_info) { | 429 | else => switch (field_type_info) { |
| 410 | .Int => { | 430 | .Int => { |
| @@ -490,7 +510,7 @@ test "sqlite: statement exec" { | |||
| 490 | }; | 510 | }; |
| 491 | 511 | ||
| 492 | for (users) |user| { | 512 | for (users) |user| { |
| 493 | try db.exec("INSERT INTO user(id, name, age) VALUES(?, ?, ?)", user); | 513 | try db.exec("INSERT INTO user(id, name, age) VALUES(?{usize}, ?{[]const u8}, ?{usize})", user); |
| 494 | 514 | ||
| 495 | const rows_inserted = db.rowsAffected(); | 515 | const rows_inserted = db.rowsAffected(); |
| 496 | testing.expectEqual(@as(usize, 1), rows_inserted); | 516 | testing.expectEqual(@as(usize, 1), rows_inserted); |
| @@ -499,10 +519,10 @@ test "sqlite: statement exec" { | |||
| 499 | // Read a single user | 519 | // Read a single user |
| 500 | 520 | ||
| 501 | { | 521 | { |
| 502 | var stmt = try db.prepare("SELECT id, name, age FROM user WHERE id = ?"); | 522 | var stmt = try db.prepare("SELECT id, name, age FROM user WHERE id = ?{usize}"); |
| 503 | defer stmt.deinit(); | 523 | defer stmt.deinit(); |
| 504 | 524 | ||
| 505 | var rows = try stmt.all(User, .{ .allocator = allocator }, .{ .id = 20 }); | 525 | var rows = try stmt.all(User, .{ .allocator = allocator }, .{ .id = @as(usize, 20) }); |
| 506 | for (rows) |row| { | 526 | for (rows) |row| { |
| 507 | testing.expectEqual(users[0].id, row.id); | 527 | testing.expectEqual(users[0].id, row.id); |
| 508 | testing.expectEqualStrings(users[0].name, row.name); | 528 | testing.expectEqualStrings(users[0].name, row.name); |
| @@ -529,7 +549,7 @@ test "sqlite: statement exec" { | |||
| 529 | // Test with anonymous structs | 549 | // Test with anonymous structs |
| 530 | 550 | ||
| 531 | { | 551 | { |
| 532 | var stmt = try db.prepare("SELECT id, name, age FROM user WHERE id = ?"); | 552 | var stmt = try db.prepare("SELECT id, name, age FROM user WHERE id = ?{usize}"); |
| 533 | defer stmt.deinit(); | 553 | defer stmt.deinit(); |
| 534 | 554 | ||
| 535 | var row = try stmt.one( | 555 | var row = try stmt.one( |
| @@ -539,7 +559,7 @@ test "sqlite: statement exec" { | |||
| 539 | age: usize, | 559 | age: usize, |
| 540 | }, | 560 | }, |
| 541 | .{ .allocator = allocator }, | 561 | .{ .allocator = allocator }, |
| 542 | .{ .id = 20 }, | 562 | .{ .id = @as(usize, 20) }, |
| 543 | ); | 563 | ); |
| 544 | testing.expect(row != null); | 564 | testing.expect(row != null); |
| 545 | 565 | ||
| @@ -552,26 +572,56 @@ test "sqlite: statement exec" { | |||
| 552 | // Test with a single integer | 572 | // Test with a single integer |
| 553 | 573 | ||
| 554 | { | 574 | { |
| 555 | const query = "SELECT age FROM user WHERE id = ?"; | 575 | const query = "SELECT age FROM user WHERE id = ?{usize}"; |
| 556 | 576 | ||
| 557 | var stmt: Statement(StatementOptions.from(query)) = try db.prepare(query); | 577 | var stmt: Statement(.{}, ParsedQuery.from(query)) = try db.prepare(query); |
| 558 | defer stmt.deinit(); | 578 | defer stmt.deinit(); |
| 559 | 579 | ||
| 560 | var age = try stmt.one(usize, .{}, .{ .id = 20 }); | 580 | var age = try stmt.one(usize, .{}, .{ .id = @as(usize, 20) }); |
| 561 | testing.expect(age != null); | 581 | testing.expect(age != null); |
| 562 | 582 | ||
| 563 | testing.expectEqual(@as(usize, 33), age.?); | 583 | testing.expectEqual(@as(usize, 33), age.?); |
| 564 | } | 584 | } |
| 565 | 585 | ||
| 566 | // Test with a Bytes struct | 586 | // Test with a Blob struct |
| 587 | { | ||
| 588 | try db.exec("INSERT INTO user(id, name, age) VALUES(?{usize}, ?{blob}, ?{u32})", .{ | ||
| 589 | .id = @as(usize, 200), | ||
| 590 | .name = Blob{ .data = "hello" }, | ||
| 591 | .age = @as(u32, 20), | ||
| 592 | }); | ||
| 593 | } | ||
| 567 | 594 | ||
| 595 | // Test with a Text struct | ||
| 568 | { | 596 | { |
| 569 | try db.exec("INSERT INTO user(id, name, age) VALUES(?, ?, ?)", .{ | 597 | try db.exec("INSERT INTO user(id, name, age) VALUES(?{usize}, ?{text}, ?{u32})", .{ |
| 570 | .id = 200, | 598 | .id = @as(usize, 201), |
| 571 | .name = Bytes{ .Text = "hello" }, | 599 | .name = Text{ .data = "hello" }, |
| 572 | .age = 20, | 600 | .age = @as(u32, 20), |
| 573 | }); | 601 | }); |
| 574 | } | 602 | } |
| 603 | |||
| 604 | // Read in a Text struct | ||
| 605 | { | ||
| 606 | var stmt = try db.prepare("SELECT id, name, age FROM user WHERE id = ?{usize}"); | ||
| 607 | defer stmt.deinit(); | ||
| 608 | |||
| 609 | var row = try stmt.one( | ||
| 610 | struct { | ||
| 611 | id: usize, | ||
| 612 | name: Text, | ||
| 613 | age: usize, | ||
| 614 | }, | ||
| 615 | .{ .allocator = allocator }, | ||
| 616 | .{@as(usize, 20)}, | ||
| 617 | ); | ||
| 618 | testing.expect(row != null); | ||
| 619 | |||
| 620 | const exp = users[0]; | ||
| 621 | testing.expectEqual(exp.id, row.?.id); | ||
| 622 | testing.expectEqualStrings(exp.name, row.?.name.data); | ||
| 623 | testing.expectEqual(exp.age, row.?.age); | ||
| 624 | } | ||
| 575 | } | 625 | } |
| 576 | 626 | ||
| 577 | fn dbMode() Db.Mode { | 627 | fn dbMode() Db.Mode { |