const std = @import("std"); const log = @import("log.zig").scoped_log_t(.db); /// Database abstraction layer pub const Db = @This(); /// Type erased pointer to actual implementation, and reference to implementation functions /// Inspired by std.mem.Allocator ptr: *anyopaque, vtable: VTable, fn init(ptr: *anyopaque, vtable: VTable) Db { return Db{ .ptr = ptr, .vtable = vtable, }; } pub const OpenError = error{ Failed, NotThreadSafe } || std.mem.Allocator.Error; pub const PrepareError = error{Failed} || std.mem.Allocator.Error; pub const BindError = error{}; pub const StepError = error{Failed} || std.mem.Allocator.Error; pub const ColumnError = error{WrongType} || std.fmt.ParseIntError; // Dispatcher for concrete implementations. Inspired by std.mem.Allocator. const VTable = struct { prepare: *const fn (*anyopaque, query: [:0]const u8) PrepareError!*anyopaque, // bind: *const fn (db: *anyopaque, stmt: *anyopaque, idx: usize, val: anytype) BindError!void, step: *const fn (db: *anyopaque, stmt: *anyopaque) StepError!bool, // TODO support more types // TODO think of something clever to avoid this function proliferation. // Values returned should live at least until next call to .step() column_i64: *const fn (db: *anyopaque, stmt: *anyopaque, idx: u31) ColumnError!?i64, column_slice_const_u8: *const fn (db: *anyopaque, stmt: *anyopaque, idx: u31) ColumnError!?[:0]const u8, close_stmt: *const fn (db: *anyopaque, stmt: *anyopaque) void, close_db: *const fn (db: *anyopaque) void, }; pub const Stmt = struct { db: *Db, ptr: *anyopaque, fn bind(self: *Stmt, idx: u31, val: anytype) !void { try self.db.vtable.bind(self.db.ptr, self.ptr, idx, val); } fn bind_named(self: *Stmt, name: [:0]const u8, val: anytype) !void { try self.db.vtable.bind_named(self.db.ptr, self.ptr, name, val); } /// Advance the result set to the next row. pub fn step(self: *Stmt) !bool { return try self.db.vtable.step(self.db.ptr, self.ptr); } /// Read a column pub fn column(self: *Stmt, comptime T: type, index: u31) !?T { switch (@typeInfo(T)) { .Int => |intinfo| { if (intinfo.signedness != .signed) @compileError("integer type i64 only is supported in Stmt#column"); if (intinfo.bits != 64) @compileError("integer type i64 only is supported in Stmt#column"); return try self.db.vtable.column_i64(self.db.ptr, self.ptr, index); }, .Pointer => |ptrinfo| { if (ptrinfo.size != .Slice) @compileError("pointer type []const u8 only is supported in Stmt#column"); if (ptrinfo.child != u8) @compileError("pointer type []const u8 only is supported in Stmt#column"); return try self.db.vtable.column_slice_const_u8(self.db.ptr, self.ptr, index); }, else => @compileError("unhandled type " ++ @tagName(@typeInfo(T)) ++ " in Stmt#column"), } } pub fn close(self: *Stmt) void { self.db.vtable.close_stmt(self.db.ptr, self.ptr); } }; /// Interface for running queries /// params should be a tuple with values for placeholders. pub fn query(self: *Db, comptime qry: [:0]const u8, params: anytype) !Stmt { const ti = @typeInfo(@TypeOf(params)); if (ti != .Struct) @compileError("Db.query params must be a tuple struct but it's a " ++ @tagName(ti)); const si = ti.Struct; if (!si.is_tuple) @compileError("Db.query params must be a tuple"); const stmt_ptr = try self.vtable.prepare(self.ptr, qry); var stmt = Stmt{ .db = self, .ptr = stmt_ptr }; inline for (0..si.fields.len) |idx| { try stmt.bind(idx, params[idx]); } return stmt; } /// Shortcut for query/step/close pub fn exec(self: *Db, comptime qry: [:0]const u8) !void { var stmt = try self.query(qry, .{}); defer stmt.close(); _ = try stmt.step(); } pub fn close(self: *Db) void { self.vtable.close_db(self.ptr); } test { _ = @import("test.zig"); }