From 28bf7944043cad990f61e8e0e91d05ae3a8163cf Mon Sep 17 00:00:00 2001 From: Vincent Rischmann Date: Sun, 25 Oct 2020 01:40:44 +0200 Subject: refactor API It doesn't make sense to pass the bind parameters in prepare; instead pass them in `exec`, `one`, and `all`. --- sqlite.zig | 474 ++++++++++++++++++++++++++++++++----------------------------- 1 file changed, 246 insertions(+), 228 deletions(-) (limited to 'sqlite.zig') diff --git a/sqlite.zig b/sqlite.zig index 116807a..d140e56 100644 --- a/sqlite.zig +++ b/sqlite.zig @@ -88,9 +88,9 @@ pub const Db = struct { /// exec is a convenience function which prepares a statement and executes it directly. pub fn exec(self: *Self, comptime query: []const u8, values: anytype) !void { - var stmt = try self.prepare(query, values); + var stmt = try self.prepare(query); defer stmt.deinit(); - try stmt.exec(); + try stmt.exec(values); } /// prepare prepares a statement for the `query` provided. @@ -107,8 +107,8 @@ pub const Db = struct { /// defer stmt.deinit(); /// /// Note that the name of the fields in the tuple are irrelevant, only the types are. - pub fn prepare(self: *Self, comptime query: []const u8, values: anytype) !Statement { - return Statement.prepare(self, 0, query, values); + pub fn prepare(self: *Self, comptime query: []const u8) !Statement(StatementOptions.from(query)) { + return Statement(comptime StatementOptions.from(query)).prepare(self, 0, query); } /// rowsAffected returns the number of rows affected by the last statement executed. @@ -128,6 +128,18 @@ pub const Bytes = union(enum) { 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, "?"), + }; + } +}; + /// Statement is a wrapper around a SQLite statement, providing high-level functions to execute /// a statement and retrieve rows for SELECT queries. /// @@ -159,259 +171,264 @@ pub const Bytes = union(enum) { /// /// Look at aach function for more complete documentation. /// -pub const Statement = struct { - const Self = @This(); +pub fn Statement(comptime opts: StatementOptions) type { + return struct { + const Self = @This(); - stmt: *c.sqlite3_stmt, + stmt: *c.sqlite3_stmt, - const BytesType = enum { - Text, - Blob, - }; - - fn prepare(db: *Db, flags: c_uint, comptime query: []const u8, values: anytype) !Self { - const StructType = @TypeOf(values); - const StructTypeInfo = @typeInfo(StructType).Struct; - comptime { - const bind_parameter_count = std.mem.count(u8, query, "?"); - if (bind_parameter_count != StructTypeInfo.fields.len) { - @compileError("bind parameter count != number of fields in tuple/struct"); - } - } - - // prepare - - var stmt = blk: { - var tmp: ?*c.sqlite3_stmt = undefined; - const result = c.sqlite3_prepare_v3( - db.db, - query.ptr, - @intCast(c_int, query.len), - flags, - &tmp, - null, - ); - if (result != c.SQLITE_OK) { - logger.warn("unable to prepare statement, result: {}", .{result}); - return error.CannotPrepareStatement; - } - break :blk tmp.?; + const BytesType = enum { + Text, + Blob, }; - // Bind + fn prepare(db: *Db, flags: c_uint, comptime query: []const u8) !Self { + // prepare + var stmt = blk: { + var tmp: ?*c.sqlite3_stmt = undefined; + const result = c.sqlite3_prepare_v3( + db.db, + query.ptr, + @intCast(c_int, query.len), + flags, + &tmp, + null, + ); + if (result != c.SQLITE_OK) { + logger.warn("unable to prepare statement, result: {}", .{result}); + return error.CannotPrepareStatement; + } + break :blk tmp.?; + }; - inline for (StructTypeInfo.fields) |struct_field, _i| { - const i = @as(usize, _i); - const field_type_info = @typeInfo(struct_field.field_type); - const field_value = @field(values, struct_field.name); - const column = i + 1; + return Self{ + .stmt = stmt, + }; + } - switch (struct_field.field_type) { - []const u8, []u8 => { - _ = c.sqlite3_bind_text(stmt, column, field_value.ptr, @intCast(c_int, field_value.len), null); - }, - Bytes => switch (field_value) { - .Text => |v| _ = c.sqlite3_bind_text(stmt, column, v.ptr, @intCast(c_int, v.len), null), - .Blob => |v| _ = c.sqlite3_bind_blob(stmt, column, v.ptr, @intCast(c_int, v.len), null), - }, - else => switch (field_type_info) { - .Int, .ComptimeInt => _ = c.sqlite3_bind_int64(stmt, column, @intCast(c_longlong, field_value)), - .Float, .ComptimeFloat => _ = c.sqlite3_bind_double(stmt, column, field_value), - .Array => |arr| { - switch (arr.child) { - u8 => { - const data: []const u8 = field_value[0..field_value.len]; - - _ = c.sqlite3_bind_text(stmt, column, data.ptr, @intCast(c_int, data.len), null); - }, - else => @compileError("cannot populate field " ++ field.name ++ " of type array of " ++ @typeName(arr.child)), - } - }, - else => @compileError("cannot bind field " ++ struct_field.name ++ " of type " ++ @typeName(struct_field.field_type)), - }, + pub fn deinit(self: *Self) void { + const result = c.sqlite3_finalize(self.stmt); + if (result != c.SQLITE_OK) { + logger.err("unable to finalize prepared statement, result: {}", .{result}); } } - return Self{ - .stmt = stmt, - }; - } + pub fn bind(self: *Self, values: anytype) void { + const StructType = @TypeOf(values); + const StructTypeInfo = @typeInfo(StructType).Struct; - pub fn deinit(self: *Self) void { - const result = c.sqlite3_finalize(self.stmt); - if (result != c.SQLITE_OK) { - logger.err("unable to finalize prepared statement, result: {}", .{result}); - } - } + if (comptime opts.bind_markers != StructTypeInfo.fields.len) { + @compileError("number of bind markers not equal to number of fields"); + } - pub fn exec(self: *Self) !void { - const result = c.sqlite3_step(self.stmt); - switch (result) { - c.SQLITE_DONE => {}, - c.SQLITE_BUSY => return error.SQLiteBusy, - else => std.debug.panic("invalid result {}", .{result}), - } - } + inline for (StructTypeInfo.fields) |struct_field, _i| { + const i = @as(usize, _i); + const field_type_info = @typeInfo(struct_field.field_type); + const field_value = @field(values, struct_field.name); + const column = i + 1; - /// one reads a single row from the result set of this statement. - /// - /// The data in the row is used to populate a value of the type `Type`. - /// This means that `Type` must have as many fields as is returned in the query - /// executed by this statement. - /// This also means that the type of each field must be compatible with the SQLite type. - /// - /// Here is an example of how to use an anonymous struct type: - /// - /// const row = try stmt.one( - /// struct { - /// id: usize, - /// name: []const u8, - /// age: usize, - /// }, - /// .{ .allocator = allocator }, - /// ); - /// - /// The `options` tuple is used to provide additional state in some cases, for example - /// an allocator used to read text and blobs. - /// - pub fn one(self: *Self, comptime Type: type, options: anytype) !?Type { - if (!comptime std.meta.trait.is(.Struct)(@TypeOf(options))) { - @compileError("options passed to all must be a struct"); + switch (struct_field.field_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), + }, + 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), + .Array => |arr| { + switch (arr.child) { + u8 => { + const data: []const u8 = field_value[0..field_value.len]; + + _ = c.sqlite3_bind_text(self.stmt, column, data.ptr, @intCast(c_int, data.len), null); + }, + else => @compileError("cannot populate field " ++ field.name ++ " of type array of " ++ @typeName(arr.child)), + } + }, + else => @compileError("cannot bind field " ++ struct_field.name ++ " of type " ++ @typeName(struct_field.field_type)), + }, + } + } } - const TypeInfo = @typeInfo(Type); - var result = c.sqlite3_step(self.stmt); + pub fn exec(self: *Self, values: anytype) !void { + self.bind(values); - switch (TypeInfo) { - .Int => return switch (result) { - c.SQLITE_ROW => try self.readInt(Type, options), - c.SQLITE_DONE => null, - else => std.debug.panic("invalid result {}", .{result}), - }, - .Struct => return switch (result) { - c.SQLITE_ROW => try self.readStruct(Type, options), - c.SQLITE_DONE => null, + const result = c.sqlite3_step(self.stmt); + switch (result) { + c.SQLITE_DONE => {}, + c.SQLITE_BUSY => return error.SQLiteBusy, else => std.debug.panic("invalid result {}", .{result}), - }, - else => @compileError("cannot read into type " ++ @typeName(Type)), + } } - } - /// all reads all rows from the result set of this statement. - /// - /// The data in each row is used to populate a value of the type `Type`. - /// This means that `Type` must have as many fields as is returned in the query - /// executed by this statement. - /// This also means that the type of each field must be compatible with the SQLite type. - /// - /// Here is an example of how to use an anonymous struct type: - /// - /// const rows = try stmt.all( - /// struct { - /// id: usize, - /// name: []const u8, - /// age: usize, - /// }, - /// .{ .allocator = allocator }, - /// ); - /// - /// The `options` tuple is used to provide additional state in some cases. - /// Note that for this function the allocator is mandatory. - /// - pub fn all(self: *Self, comptime Type: type, options: anytype) ![]Type { - if (!comptime std.meta.trait.is(.Struct)(@TypeOf(options))) { - @compileError("options passed to all must be a struct"); - } - const TypeInfo = @typeInfo(Type); + /// one reads a single row from the result set of this statement. + /// + /// The data in the row is used to populate a value of the type `Type`. + /// This means that `Type` must have as many fields as is returned in the query + /// executed by this statement. + /// This also means that the type of each field must be compatible with the SQLite type. + /// + /// Here is an example of how to use an anonymous struct type: + /// + /// const row = try stmt.one( + /// struct { + /// id: usize, + /// name: []const u8, + /// age: usize, + /// }, + /// .{ .allocator = allocator }, + /// ); + /// + /// The `options` tuple is used to provide additional state in some cases, for example + /// an allocator used to read text and blobs. + /// + pub fn one(self: *Self, comptime Type: type, options: anytype, values: anytype) !?Type { + if (!comptime std.meta.trait.is(.Struct)(@TypeOf(options))) { + @compileError("options passed to all must be a struct"); + } + const TypeInfo = @typeInfo(Type); - var rows = std.ArrayList(Type).init(options.allocator); + self.bind(values); - var result = c.sqlite3_step(self.stmt); - while (result == c.SQLITE_ROW) : (result = c.sqlite3_step(self.stmt)) { - const columns = c.sqlite3_column_count(self.stmt); + var result = c.sqlite3_step(self.stmt); - var value = switch (TypeInfo) { - .Int => blk: { - debug.assert(columns == 1); - break :blk try self.readInt(Type, options); + switch (TypeInfo) { + .Int => return switch (result) { + c.SQLITE_ROW => try self.readInt(Type, options), + c.SQLITE_DONE => null, + else => std.debug.panic("invalid result {}", .{result}), }, - .Struct => blk: { - std.debug.assert(columns == @typeInfo(Type).Struct.fields.len); - break :blk try self.readStruct(Type, options); + .Struct => return switch (result) { + c.SQLITE_ROW => try self.readStruct(Type, options), + c.SQLITE_DONE => null, + else => std.debug.panic("invalid result {}", .{result}), }, else => @compileError("cannot read into type " ++ @typeName(Type)), - }; - - try rows.append(value); + } } - if (result != c.SQLITE_DONE) { - logger.err("unable to iterate, result: {}", .{result}); - return error.SQLiteStepError; - } + /// all reads all rows from the result set of this statement. + /// + /// The data in each row is used to populate a value of the type `Type`. + /// This means that `Type` must have as many fields as is returned in the query + /// executed by this statement. + /// This also means that the type of each field must be compatible with the SQLite type. + /// + /// Here is an example of how to use an anonymous struct type: + /// + /// const rows = try stmt.all( + /// struct { + /// id: usize, + /// name: []const u8, + /// age: usize, + /// }, + /// .{ .allocator = allocator }, + /// ); + /// + /// The `options` tuple is used to provide additional state in some cases. + /// Note that for this function the allocator is mandatory. + /// + pub fn all(self: *Self, comptime Type: type, options: anytype, values: anytype) ![]Type { + if (!comptime std.meta.trait.is(.Struct)(@TypeOf(options))) { + @compileError("options passed to all must be a struct"); + } + const TypeInfo = @typeInfo(Type); - return rows.span(); - } + self.bind(values); - fn readInt(self: *Self, comptime Type: type, options: anytype) !Type { - const n = c.sqlite3_column_int64(self.stmt, 0); - return @intCast(Type, n); - } + var rows = std.ArrayList(Type).init(options.allocator); - fn readStruct(self: *Self, comptime Type: type, options: anytype) !Type { - var value: Type = undefined; + var result = c.sqlite3_step(self.stmt); + while (result == c.SQLITE_ROW) : (result = c.sqlite3_step(self.stmt)) { + const columns = c.sqlite3_column_count(self.stmt); - inline for (@typeInfo(Type).Struct.fields) |field, _i| { - const i = @as(usize, _i); - const field_type_info = @typeInfo(field.field_type); + var value = switch (TypeInfo) { + .Int => blk: { + debug.assert(columns == 1); + break :blk try self.readInt(Type, options); + }, + .Struct => blk: { + std.debug.assert(columns == @typeInfo(Type).Struct.fields.len); + break :blk try self.readStruct(Type, options); + }, + else => @compileError("cannot read into type " ++ @typeName(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)); + try rows.append(value); + } - var tmp = try options.allocator.alloc(u8, size); - mem.copy(u8, tmp, @ptrCast([*c]const u8, data)[0..size]); + if (result != c.SQLITE_DONE) { + logger.err("unable to iterate, result: {}", .{result}); + return error.SQLiteStepError; + } - @field(value, field.name) = tmp; - } - }, - else => switch (field_type_info) { - .Int => { - const n = c.sqlite3_column_int64(self.stmt, i); - @field(value, field.name) = @intCast(field.field_type, n); - }, - .Float => { - const f = c.sqlite3_column_double(self.stmt, i); - @field(value, field.name) = f; - }, - .Void => { - @field(value, field.name) = {}; - }, - .Array => |arr| { - switch (arr.child) { - u8 => { - const data = c.sqlite3_column_blob(self.stmt, i); - const size = @intCast(usize, c.sqlite3_column_bytes(self.stmt, i)); + return rows.span(); + } + + fn readInt(self: *Self, comptime Type: type, options: anytype) !Type { + const n = c.sqlite3_column_int64(self.stmt, 0); + return @intCast(Type, n); + } + + fn readStruct(self: *Self, comptime Type: type, options: anytype) !Type { + var value: Type = undefined; + + inline for (@typeInfo(Type).Struct.fields) |field, _i| { + const i = @as(usize, _i); + 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)); - if (size > @as(usize, arr.len)) return error.ArrayTooSmall; + var tmp = try options.allocator.alloc(u8, size); + mem.copy(u8, tmp, @ptrCast([*c]const u8, data)[0..size]); - mem.copy(u8, @field(value, field.name)[0..], @ptrCast([*c]const u8, data)[0..size]); - }, - else => @compileError("cannot populate field " ++ field.name ++ " of type array of " ++ @typeName(arr.child)), + @field(value, field.name) = tmp; } }, - else => @compileError("cannot populate field " ++ field.name ++ " of type " ++ @typeName(field.field_type)), - }, + else => switch (field_type_info) { + .Int => { + const n = c.sqlite3_column_int64(self.stmt, i); + @field(value, field.name) = @intCast(field.field_type, n); + }, + .Float => { + const f = c.sqlite3_column_double(self.stmt, i); + @field(value, field.name) = f; + }, + .Void => { + @field(value, field.name) = {}; + }, + .Array => |arr| { + switch (arr.child) { + u8 => { + const data = c.sqlite3_column_blob(self.stmt, i); + const size = @intCast(usize, c.sqlite3_column_bytes(self.stmt, i)); + + if (size > @as(usize, arr.len)) return error.ArrayTooSmall; + + mem.copy(u8, @field(value, field.name)[0..], @ptrCast([*c]const u8, data)[0..size]); + }, + else => @compileError("cannot populate field " ++ field.name ++ " of type array of " ++ @typeName(arr.child)), + } + }, + else => @compileError("cannot populate field " ++ field.name ++ " of type " ++ @typeName(field.field_type)), + }, + } } - } - return value; - } -}; + return value; + } + }; +} test "sqlite: db init" { var db: Db = undefined; @@ -444,9 +461,7 @@ test "sqlite: statement exec" { \\) }; inline for (all_ddl) |ddl| { - var stmt = try db.prepare(ddl, .{}); - defer stmt.deinit(); - try stmt.exec(); + try db.exec(ddl, .{}); } // Add data @@ -473,10 +488,10 @@ test "sqlite: statement exec" { // Read a single user { - var stmt = try db.prepare("SELECT id, name, age FROM user WHERE id = ?", .{ .id = 20 }); + var stmt = try db.prepare("SELECT id, name, age FROM user WHERE id = ?"); defer stmt.deinit(); - var rows = try stmt.all(User, .{ .allocator = allocator }); + var rows = try stmt.all(User, .{ .allocator = allocator }, .{ .id = 20 }); for (rows) |row| { testing.expectEqual(users[0].id, row.id); testing.expectEqualStrings(users[0].name, row.name); @@ -487,10 +502,10 @@ test "sqlite: statement exec" { // Read all users { - var stmt = try db.prepare("SELECT id, name, age FROM user", .{}); + var stmt = try db.prepare("SELECT id, name, age FROM user"); defer stmt.deinit(); - var rows = try stmt.all(User, .{ .allocator = allocator }); + var rows = try stmt.all(User, .{ .allocator = allocator }, .{}); testing.expectEqual(@as(usize, 3), rows.len); for (rows) |row, i| { const exp = users[i]; @@ -503,7 +518,7 @@ test "sqlite: statement exec" { // Test with anonymous structs { - var stmt = try db.prepare("SELECT id, name, age FROM user WHERE id = ?", .{ .id = 20 }); + var stmt = try db.prepare("SELECT id, name, age FROM user WHERE id = ?"); defer stmt.deinit(); var row = try stmt.one( @@ -513,6 +528,7 @@ test "sqlite: statement exec" { age: usize, }, .{ .allocator = allocator }, + .{ .id = 20 }, ); testing.expect(row != null); @@ -525,10 +541,12 @@ test "sqlite: statement exec" { // Test with a single integer { - var stmt = try db.prepare("SELECT age FROM user WHERE id = ?", .{ .id = 20 }); + const query = "SELECT age FROM user WHERE id = ?"; + + var stmt: Statement(StatementOptions.from(query)) = try db.prepare(query); defer stmt.deinit(); - var age = try stmt.one(usize, .{}); + var age = try stmt.one(usize, .{}, .{ .id = 20 }); testing.expect(age != null); testing.expectEqual(@as(usize, 33), age.?); -- cgit v1.2.3