From be6902d0736178c113d6b11c9b056c4a33a966f3 Mon Sep 17 00:00:00 2001 From: Vincent Rischmann Date: Fri, 30 Oct 2020 13:49:05 +0100 Subject: add types to bind markers and check them at comptime --- sqlite.zig | 76 +++++++++++++++++++++++++------------------------------------- 1 file changed, 31 insertions(+), 45 deletions(-) (limited to 'sqlite.zig') diff --git a/sqlite.zig b/sqlite.zig index 5844c34..a30da70 100644 --- a/sqlite.zig +++ b/sqlite.zig @@ -8,6 +8,8 @@ const c = @cImport({ @cInclude("sqlite3.h"); }); +usingnamespace @import("query.zig"); + const logger = std.log.scoped(.sqlite); /// Db is a wrapper around a SQLite database, providing high-level functions for executing queries. @@ -106,8 +108,9 @@ pub const Db = struct { /// The statement returned is only compatible with the number of bind markers in the input query. /// This is done because we type check the bind parameters when executing the statement later. /// - pub fn prepare(self: *Self, comptime query: []const u8) !Statement(StatementOptions.from(query)) { - return Statement(comptime StatementOptions.from(query)).prepare(self, 0, query); + pub fn prepare(self: *Self, comptime query: []const u8) !Statement(.{}, ParsedQuery.from(query)) { + const parsed_query = ParsedQuery.from(query); + return Statement(.{}, comptime parsed_query).prepare(self, 0); } /// rowsAffected returns the number of rows affected by the last statement executed. @@ -116,28 +119,7 @@ pub const Db = struct { } }; -/// Bytes is used to represent a byte slice with its SQLite datatype. -/// -/// Since Zig doesn't have strings we can't tell if a []u8 must be stored as a SQLite TEXT or BLOB, -/// this type can be used to communicate this when executing a statement. -/// -/// If a []u8 or []const u8 is passed as bind parameter it will be treated as TEXT. -pub const Bytes = union(enum) { - Blob: []const u8, - Text: []const u8, -}; - -pub const StatementOptions = struct { - const Self = @This(); - - bind_markers: usize, - - fn from(comptime query: []const u8) Self { - return Self{ - .bind_markers = std.mem.count(u8, query, "?"), - }; - } -}; +pub const StatementOptions = struct {}; /// Statement is a wrapper around a SQLite statement, providing high-level functions to execute /// a statement and retrieve rows for SELECT queries. @@ -172,19 +154,21 @@ pub const StatementOptions = struct { /// /// Look at aach function for more complete documentation. /// -pub fn Statement(comptime opts: StatementOptions) type { +pub fn Statement(comptime opts: StatementOptions, comptime query: ParsedQuery) type { return struct { const Self = @This(); stmt: *c.sqlite3_stmt, - fn prepare(db: *Db, flags: c_uint, comptime query: []const u8) !Self { + fn prepare(db: *Db, flags: c_uint) !Self { var stmt = blk: { + const real_query = query.getQuery(); + var tmp: ?*c.sqlite3_stmt = undefined; const result = c.sqlite3_prepare_v3( db.db, - query.ptr, - @intCast(c_int, query.len), + real_query.ptr, + @intCast(c_int, real_query.len), flags, &tmp, null, @@ -212,11 +196,15 @@ pub fn Statement(comptime opts: StatementOptions) type { const StructType = @TypeOf(values); const StructTypeInfo = @typeInfo(StructType).Struct; - if (comptime opts.bind_markers != StructTypeInfo.fields.len) { + if (comptime query.nb_bind_markers != StructTypeInfo.fields.len) { @compileError("number of bind markers not equal to number of fields"); } inline for (StructTypeInfo.fields) |struct_field, _i| { + if (struct_field.field_type != query.bind_markers[_i].Type) { + @compileError("value type " ++ @typeName(struct_field.field_type) ++ " is not the bind marker type " ++ @typeName(query.bind_markers[_i].Type)); + } + const i = @as(usize, _i); const field_type_info = @typeInfo(struct_field.field_type); const field_value = @field(values, struct_field.name); @@ -226,10 +214,8 @@ pub fn Statement(comptime opts: StatementOptions) type { []const u8, []u8 => { _ = c.sqlite3_bind_text(self.stmt, column, field_value.ptr, @intCast(c_int, field_value.len), null); }, - Bytes => switch (field_value) { - .Text => |v| _ = c.sqlite3_bind_text(self.stmt, column, v.ptr, @intCast(c_int, v.len), null), - .Blob => |v| _ = c.sqlite3_bind_blob(self.stmt, column, v.ptr, @intCast(c_int, v.len), null), - }, + Text => _ = c.sqlite3_bind_text(self.stmt, column, field_value.data.ptr, @intCast(c_int, field_value.data.len), null), + Blob => _ = c.sqlite3_bind_blob(self.stmt, column, field_value.data.ptr, @intCast(c_int, field_value.data.len), null), else => switch (field_type_info) { .Int, .ComptimeInt => _ = c.sqlite3_bind_int64(self.stmt, column, @intCast(c_longlong, field_value)), .Float, .ComptimeFloat => _ = c.sqlite3_bind_double(self.stmt, column, field_value), @@ -490,7 +476,7 @@ test "sqlite: statement exec" { }; for (users) |user| { - try db.exec("INSERT INTO user(id, name, age) VALUES(?, ?, ?)", user); + try db.exec("INSERT INTO user(id, name, age) VALUES(?{usize}, ?{[]const u8}, ?{usize})", user); const rows_inserted = db.rowsAffected(); testing.expectEqual(@as(usize, 1), rows_inserted); @@ -499,10 +485,10 @@ test "sqlite: statement exec" { // Read a single user { - var stmt = try db.prepare("SELECT id, name, age FROM user WHERE id = ?"); + var stmt = try db.prepare("SELECT id, name, age FROM user WHERE id = ?{usize}"); defer stmt.deinit(); - var rows = try stmt.all(User, .{ .allocator = allocator }, .{ .id = 20 }); + var rows = try stmt.all(User, .{ .allocator = allocator }, .{ .id = @as(usize, 20) }); for (rows) |row| { testing.expectEqual(users[0].id, row.id); testing.expectEqualStrings(users[0].name, row.name); @@ -529,7 +515,7 @@ test "sqlite: statement exec" { // Test with anonymous structs { - var stmt = try db.prepare("SELECT id, name, age FROM user WHERE id = ?"); + var stmt = try db.prepare("SELECT id, name, age FROM user WHERE id = ?{usize}"); defer stmt.deinit(); var row = try stmt.one( @@ -539,7 +525,7 @@ test "sqlite: statement exec" { age: usize, }, .{ .allocator = allocator }, - .{ .id = 20 }, + .{ .id = @as(usize, 20) }, ); testing.expect(row != null); @@ -552,12 +538,12 @@ test "sqlite: statement exec" { // Test with a single integer { - const query = "SELECT age FROM user WHERE id = ?"; + const query = "SELECT age FROM user WHERE id = ?{usize}"; - var stmt: Statement(StatementOptions.from(query)) = try db.prepare(query); + var stmt: Statement(.{}, ParsedQuery.from(query)) = try db.prepare(query); defer stmt.deinit(); - var age = try stmt.one(usize, .{}, .{ .id = 20 }); + var age = try stmt.one(usize, .{}, .{ .id = @as(usize, 20) }); testing.expect(age != null); testing.expectEqual(@as(usize, 33), age.?); @@ -566,10 +552,10 @@ test "sqlite: statement exec" { // Test with a Bytes struct { - try db.exec("INSERT INTO user(id, name, age) VALUES(?, ?, ?)", .{ - .id = 200, - .name = Bytes{ .Text = "hello" }, - .age = 20, + try db.exec("INSERT INTO user(id, name, age) VALUES(?{usize}, ?{blob}, ?{u32})", .{ + .id = @as(usize, 200), + .name = Blob{ .data = "hello" }, + .age = @as(u32, 20), }); } } -- cgit v1.2.3 From ad4092a87a1f81b217d63ffaa36fb2f1e4a77711 Mon Sep 17 00:00:00 2001 From: Vincent Rischmann Date: Wed, 11 Nov 2020 13:51:00 +0100 Subject: add readBytes and allow reading into a Text or Blob struct --- sqlite.zig | 94 ++++++++++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 80 insertions(+), 14 deletions(-) (limited to 'sqlite.zig') diff --git a/sqlite.zig b/sqlite.zig index a30da70..4e92e6b 100644 --- a/sqlite.zig +++ b/sqlite.zig @@ -109,6 +109,7 @@ pub const Db = struct { /// This is done because we type check the bind parameters when executing the statement later. /// pub fn prepare(self: *Self, comptime query: []const u8) !Statement(.{}, ParsedQuery.from(query)) { + @setEvalBranchQuota(3000); const parsed_query = ParsedQuery.from(query); return Statement(.{}, comptime parsed_query).prepare(self, 0); } @@ -371,6 +372,39 @@ pub fn Statement(comptime opts: StatementOptions, comptime query: ParsedQuery) t return @intCast(Type, n); } + const ReadBytesMode = enum { + Blob, + Text, + }; + + fn readBytes(self: *Self, allocator: *mem.Allocator, mode: ReadBytesMode, _i: usize) !?[]const u8 { + const i = @intCast(c_int, _i); + switch (mode) { + .Blob => { + const data = c.sqlite3_column_blob(self.stmt, i); + if (data == null) return null; + + const size = @intCast(usize, c.sqlite3_column_bytes(self.stmt, i)); + + var tmp = try allocator.alloc(u8, size); + mem.copy(u8, tmp, @ptrCast([*c]const u8, data)[0..size]); + + return tmp; + }, + .Text => { + const data = c.sqlite3_column_text(self.stmt, i); + if (data == null) return null; + + const size = @intCast(usize, c.sqlite3_column_bytes(self.stmt, i)); + + var tmp = try allocator.alloc(u8, size); + mem.copy(u8, tmp, @ptrCast([*c]const u8, data)[0..size]); + + return tmp; + }, + } + } + fn readStruct(self: *Self, comptime Type: type, options: anytype) !Type { var value: Type = undefined; @@ -379,18 +413,20 @@ pub fn Statement(comptime opts: StatementOptions, comptime query: ParsedQuery) t const field_type_info = @typeInfo(field.field_type); switch (field.field_type) { - []const u8, []u8 => { - const data = c.sqlite3_column_blob(self.stmt, i); - if (data == null) { - @field(value, field.name) = ""; - } else { - const size = @intCast(usize, c.sqlite3_column_bytes(self.stmt, i)); - - var tmp = try options.allocator.alloc(u8, size); - mem.copy(u8, tmp, @ptrCast([*c]const u8, data)[0..size]); - - @field(value, field.name) = tmp; - } + []const u8, []u8 => if (try self.readBytes(options.allocator, .Blob, i)) |tmp| { + @field(value, field.name) = tmp; + } else { + @field(value, field.name) = ""; + }, + Blob => if (try self.readBytes(options.allocator, .Blob, i)) |tmp| { + @field(value, field.name).data = tmp; + } else { + @field(value, field.name).data = ""; + }, + Text => if (try self.readBytes(options.allocator, .Text, i)) |tmp| { + @field(value, field.name).data = tmp; + } else { + @field(value, field.name).data = ""; }, else => switch (field_type_info) { .Int => { @@ -549,8 +585,7 @@ test "sqlite: statement exec" { testing.expectEqual(@as(usize, 33), age.?); } - // Test with a Bytes struct - + // Test with a Blob struct { try db.exec("INSERT INTO user(id, name, age) VALUES(?{usize}, ?{blob}, ?{u32})", .{ .id = @as(usize, 200), @@ -558,6 +593,37 @@ test "sqlite: statement exec" { .age = @as(u32, 20), }); } + + // Test with a Text struct + { + try db.exec("INSERT INTO user(id, name, age) VALUES(?{usize}, ?{text}, ?{u32})", .{ + .id = @as(usize, 201), + .name = Text{ .data = "hello" }, + .age = @as(u32, 20), + }); + } + + // Read in a Text struct + { + var stmt = try db.prepare("SELECT id, name, age FROM user WHERE id = ?{usize}"); + defer stmt.deinit(); + + var row = try stmt.one( + struct { + id: usize, + name: Text, + age: usize, + }, + .{ .allocator = allocator }, + .{@as(usize, 20)}, + ); + testing.expect(row != null); + + const exp = users[0]; + testing.expectEqual(exp.id, row.?.id); + testing.expectEqualStrings(exp.name, row.?.name.data); + testing.expectEqual(exp.age, row.?.age); + } } fn dbMode() Db.Mode { -- cgit v1.2.3 From 57cab92162e2f39b1e6a02a537453b3104396970 Mon Sep 17 00:00:00 2001 From: Vincent Rischmann Date: Wed, 11 Nov 2020 13:54:45 +0100 Subject: refactor readBytes --- sqlite.zig | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) (limited to 'sqlite.zig') diff --git a/sqlite.zig b/sqlite.zig index 4e92e6b..5dd6cdb 100644 --- a/sqlite.zig +++ b/sqlite.zig @@ -377,30 +377,30 @@ pub fn Statement(comptime opts: StatementOptions, comptime query: ParsedQuery) t Text, }; - fn readBytes(self: *Self, allocator: *mem.Allocator, mode: ReadBytesMode, _i: usize) !?[]const u8 { + fn readBytes(self: *Self, allocator: *mem.Allocator, mode: ReadBytesMode, _i: usize, ptr: *[]const u8) !void { const i = @intCast(c_int, _i); switch (mode) { .Blob => { const data = c.sqlite3_column_blob(self.stmt, i); - if (data == null) return null; + if (data == null) ptr.* = ""; const size = @intCast(usize, c.sqlite3_column_bytes(self.stmt, i)); var tmp = try allocator.alloc(u8, size); mem.copy(u8, tmp, @ptrCast([*c]const u8, data)[0..size]); - return tmp; + ptr.* = tmp; }, .Text => { const data = c.sqlite3_column_text(self.stmt, i); - if (data == null) return null; + if (data == null) ptr.* = ""; const size = @intCast(usize, c.sqlite3_column_bytes(self.stmt, i)); var tmp = try allocator.alloc(u8, size); mem.copy(u8, tmp, @ptrCast([*c]const u8, data)[0..size]); - return tmp; + ptr.* = tmp; }, } } @@ -413,20 +413,14 @@ pub fn Statement(comptime opts: StatementOptions, comptime query: ParsedQuery) t const field_type_info = @typeInfo(field.field_type); switch (field.field_type) { - []const u8, []u8 => if (try self.readBytes(options.allocator, .Blob, i)) |tmp| { - @field(value, field.name) = tmp; - } else { - @field(value, field.name) = ""; + []const u8, []u8 => { + try self.readBytes(options.allocator, .Blob, i, &@field(value, field.name)); }, - Blob => if (try self.readBytes(options.allocator, .Blob, i)) |tmp| { - @field(value, field.name).data = tmp; - } else { - @field(value, field.name).data = ""; + Blob => { + try self.readBytes(options.allocator, .Blob, i, &@field(value, field.name).data); }, - Text => if (try self.readBytes(options.allocator, .Text, i)) |tmp| { - @field(value, field.name).data = tmp; - } else { - @field(value, field.name).data = ""; + Text => { + try self.readBytes(options.allocator, .Text, i, &@field(value, field.name).data); }, else => switch (field_type_info) { .Int => { -- cgit v1.2.3 From 0d766361474ae47a2f21464913e348702705cf4e Mon Sep 17 00:00:00 2001 From: Vincent Rischmann Date: Wed, 11 Nov 2020 14:13:31 +0100 Subject: allow untyped bind markers --- sqlite.zig | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) (limited to 'sqlite.zig') diff --git a/sqlite.zig b/sqlite.zig index 5dd6cdb..53e1a6a 100644 --- a/sqlite.zig +++ b/sqlite.zig @@ -202,8 +202,12 @@ pub fn Statement(comptime opts: StatementOptions, comptime query: ParsedQuery) t } inline for (StructTypeInfo.fields) |struct_field, _i| { - if (struct_field.field_type != query.bind_markers[_i].Type) { - @compileError("value type " ++ @typeName(struct_field.field_type) ++ " is not the bind marker type " ++ @typeName(query.bind_markers[_i].Type)); + const bind_marker = query.bind_markers[_i]; + switch (bind_marker) { + .Typed => |typ| if (struct_field.field_type != typ) { + @compileError("value type " ++ @typeName(struct_field.field_type) ++ " is not the bind marker type " ++ @typeName(typ)); + }, + .Untyped => {}, } const i = @as(usize, _i); -- cgit v1.2.3