main.zig (4082B)
1 const std = @import("std"); 2 const log = @import("log.zig").scoped_log_t(.db); 3 4 /// Database abstraction layer 5 pub const Db = @This(); 6 7 /// Type erased pointer to actual implementation, and reference to implementation functions 8 /// Inspired by std.mem.Allocator 9 ptr: *anyopaque, 10 vtable: VTable, 11 12 fn init(ptr: *anyopaque, vtable: VTable) Db { 13 return Db{ 14 .ptr = ptr, 15 .vtable = vtable, 16 }; 17 } 18 19 pub const OpenError = error{ Failed, NotThreadSafe } || std.mem.Allocator.Error; 20 pub const PrepareError = error{Failed} || std.mem.Allocator.Error; 21 pub const BindError = error{}; 22 pub const StepError = error{Failed} || std.mem.Allocator.Error; 23 pub const ColumnError = error{WrongType} || std.fmt.ParseIntError; 24 25 // Dispatcher for concrete implementations. Inspired by std.mem.Allocator. 26 const VTable = struct { 27 prepare: *const fn (*anyopaque, query: [:0]const u8) PrepareError!*anyopaque, 28 // bind: *const fn (db: *anyopaque, stmt: *anyopaque, idx: usize, val: anytype) BindError!void, 29 step: *const fn (db: *anyopaque, stmt: *anyopaque) StepError!bool, 30 // TODO support more types 31 // TODO think of something clever to avoid this function proliferation. 32 // Values returned should live at least until next call to .step() 33 column_i64: *const fn (db: *anyopaque, stmt: *anyopaque, idx: u31) ColumnError!?i64, 34 column_slice_const_u8: *const fn (db: *anyopaque, stmt: *anyopaque, idx: u31) ColumnError!?[:0]const u8, 35 close_stmt: *const fn (db: *anyopaque, stmt: *anyopaque) void, 36 close_db: *const fn (db: *anyopaque) void, 37 }; 38 39 pub const Stmt = struct { 40 db: *Db, 41 ptr: *anyopaque, 42 43 fn bind(self: *Stmt, idx: u31, val: anytype) !void { 44 try self.db.vtable.bind(self.db.ptr, self.ptr, idx, val); 45 } 46 fn bind_named(self: *Stmt, name: [:0]const u8, val: anytype) !void { 47 try self.db.vtable.bind_named(self.db.ptr, self.ptr, name, val); 48 } 49 /// Advance the result set to the next row. 50 pub fn step(self: *Stmt) !bool { 51 return try self.db.vtable.step(self.db.ptr, self.ptr); 52 } 53 /// Read a column 54 pub fn column(self: *Stmt, comptime T: type, index: u31) !?T { 55 switch (@typeInfo(T)) { 56 .Int => |intinfo| { 57 if (intinfo.signedness != .signed) @compileError("integer type i64 only is supported in Stmt#column"); 58 if (intinfo.bits != 64) @compileError("integer type i64 only is supported in Stmt#column"); 59 return try self.db.vtable.column_i64(self.db.ptr, self.ptr, index); 60 }, 61 .Pointer => |ptrinfo| { 62 if (ptrinfo.size != .Slice) @compileError("pointer type []const u8 only is supported in Stmt#column"); 63 if (ptrinfo.child != u8) @compileError("pointer type []const u8 only is supported in Stmt#column"); 64 return try self.db.vtable.column_slice_const_u8(self.db.ptr, self.ptr, index); 65 }, 66 else => @compileError("unhandled type " ++ @tagName(@typeInfo(T)) ++ " in Stmt#column"), 67 } 68 } 69 pub fn close(self: *Stmt) void { 70 self.db.vtable.close_stmt(self.db.ptr, self.ptr); 71 } 72 }; 73 74 /// Interface for running queries 75 /// params should be a tuple with values for placeholders. 76 pub fn query(self: *Db, comptime qry: [:0]const u8, params: anytype) !Stmt { 77 const ti = @typeInfo(@TypeOf(params)); 78 if (ti != .Struct) @compileError("Db.query params must be a tuple struct but it's a " ++ @tagName(ti)); 79 const si = ti.Struct; 80 if (!si.is_tuple) @compileError("Db.query params must be a tuple"); 81 82 const stmt_ptr = try self.vtable.prepare(self.ptr, qry); 83 var stmt = Stmt{ .db = self, .ptr = stmt_ptr }; 84 inline for (0..si.fields.len) |idx| { 85 try stmt.bind(idx, params[idx]); 86 } 87 return stmt; 88 } 89 90 /// Shortcut for query/step/close 91 pub fn exec(self: *Db, comptime qry: [:0]const u8) !void { 92 var stmt = try self.query(qry, .{}); 93 defer stmt.close(); 94 _ = try stmt.step(); 95 } 96 97 pub fn close(self: *Db) void { 98 self.vtable.close_db(self.ptr); 99 } 100 101 test { 102 _ = @import("test.zig"); 103 }