diff options
| author | tsne <tsne.dev@outlook.com> | 2025-09-03 22:19:22 +0200 |
|---|---|---|
| committer | tsne <tsne.dev@outlook.com> | 2025-10-09 21:29:13 +0200 |
| commit | 89b08c67ea7a4626550dbb9668edd703d811f1da (patch) | |
| tree | 06bbb01424b7130f5ce83200e78f8fad748da29f /zig/sqlite.zig | |
| download | mulibs-main.tar.gz | |
Diffstat (limited to 'zig/sqlite.zig')
| -rw-r--r-- | zig/sqlite.zig | 277 |
1 files changed, 277 insertions, 0 deletions
diff --git a/zig/sqlite.zig b/zig/sqlite.zig new file mode 100644 index 0000000..084aec4 --- /dev/null +++ b/zig/sqlite.zig @@ -0,0 +1,277 @@ +//! A SQLite database. +//! +//! Put the SQLite amalgamation into the `lib/sqlite` directory and add them to +//! your compile step: +//! +//! var step: *std.Build.Step.Compile; +//! step.addIncludePath(b.path("lib")); +//! step.addCSourceFile(.{ +//! .file = b.path("lib/sqlite/sqlite3.c"), +//! .flags = &[_][]const u8{ "-DSQLITE_OMIT_SHARED_CACHE", "-DSQLITE_THREADSAFE=0" }, +//! }); +//! +const std = @import("std"); +const assert = std.debug.assert; +const c = @cImport({ + @cInclude("sqlite/sqlite3.h"); +}); + +const Self = @This(); + +handle: *c.sqlite3, +err: Error!void, +errmsg: ?[]const u8, +in_tx: bool, + +pub fn open(path: [*c]const u8) Error!Self { + var handle: ?*c.sqlite3 = undefined; + var res = c.sqlite3_open(path, &handle); + if (res & 0xff == c.SQLITE_CANTOPEN) { + // We assume that the directory of the given path does not exist. + // Create the directory and retry the operation. + const dir = std.fs.path.dirname(path).?; + std.fs.cwd().makePath(dir) catch unreachable; // TODO: better error reporting? + res = c.sqlite3_open(path.name(), &handle); + } + try error_from_code(res); + assert(handle != null); + _ = c.sqlite3_extended_result_codes(handle, 1); + + return Self{ + .handle = handle.?, + .err = {}, + .errmsg = null, + .in_tx = false, + }; +} + +pub fn close(self: *Self) void { + assert(!self.in_tx); + _ = c.sqlite3_close(self.handle); +} + +// Execute an SQL statement and returns the number of affected rows. +pub fn exec(self: *Self, sql: [:0]const u8, args: anytype) Error!usize { + self.err catch unreachable; + + const stmt = try Stmt.init(self, sql, args); + defer stmt.deinit(); + const res = stmt.eval(); + if (res != c.SQLITE_DONE) { + assert(res != c.SQLITE_OK and res != c.SQLITE_ROW); + try self.check_errc(res); + } + return @intCast(c.sqlite3_changes(self.handle)); +} + +pub fn query(self: *Self, sql: [:0]const u8, args: anytype) Error!Iter { + self.err catch unreachable; + + const stmt = try Stmt.init(self, sql, args); + return Iter.init(self, stmt); +} + +pub fn begin_tx(self: *Self) Error!void { + assert(!self.in_tx); + _ = try self.exec("begin transaction", .{}); + self.in_tx = true; +} + +pub fn commit_tx(self: *Self) void { + if (self.in_tx) { + // Make sure we can commit a transaction in a `defer` statement + // and only execute a "commit" if no error occured before. + self.err catch return; + _ = self.exec("commit", .{}) catch unreachable; + self.in_tx = false; + } +} + +pub fn rollback_tx(self: *Self) void { + if (self.in_tx) { + const stmt = Stmt.init(self, "rollback", .{}) catch unreachable; + defer stmt.deinit(); + const res = stmt.eval(); + error_from_code(res) catch unreachable; + self.in_tx = false; + } +} + +pub fn last_error(self: *Self) ?[]const u8 { + if (self.errmsg) |errmsg| return errmsg; + self.err catch |err| return @errorName(err); + return null; +} + +fn check_errc(self: *Self, errc: c_int) Error!void { + self.err catch return self.err; + error_from_code(errc) catch |err| { + const msg = c.sqlite3_errmsg(self.handle); + self.errmsg = if (msg == null) null else std.mem.sliceTo(msg, 0); + self.err = err; + }; + return self.err; +} + +pub const Iter = struct { + db: *Self, + stmt: Stmt, + errc: c_int, + + inline fn init(db: *Self, stmt: Stmt) Iter { + return Iter{ .db = db, .stmt = stmt, .errc = c.SQLITE_ROW }; + } + + pub inline fn deinit(self: *Iter) void { + self.stmt.deinit(); + } + + pub fn next(self: *Iter) bool { + assert(self.errc == c.SQLITE_ROW); + self.errc = c.sqlite3_step(self.stmt.handle); + return self.errc != c.SQLITE_DONE; + } + + pub fn read_col(self: *Iter, column: usize, comptime T: type) Error!T { + assert(self.errc != c.SQLITE_DONE and self.errc != c.SQLITE_OK); + if (self.errc != c.SQLITE_ROW) { + try self.db.check_errc(self.errc); + } + return self.stmt.read_col(column, T); + } +}; + +const Stmt = struct { + handle: *c.sqlite3_stmt, + + fn init(db: *Self, sql: [:0]const u8, args: anytype) Error!Stmt { + var handle: ?*c.sqlite3_stmt = undefined; + var rest: [*c]const u8 = undefined; + const res = c.sqlite3_prepare_v2(db.handle, sql.ptr, @intCast(sql.len), &handle, &rest); + if (res != c.SQLITE_OK) { + assert(res != c.SQLITE_DONE and res != c.SQLITE_ROW); + try db.check_errc(res); + } + assert(rest == null or rest.* == 0); + assert(handle != null); + + inline for (@typeInfo(@TypeOf(args)).@"struct".fields, 1..) |arg, i| { + const bind_res = switch (@typeInfo(arg.type)) { + .int => |int| blk: { + if (int.bits <= 64) { + break :blk c.sqlite3_bind_int64(handle, @intCast(i), @intCast(@field(args, arg.name))); + } + comptime assert(int.bits <= 128); + const value = std.mem.nativeToBig(u128, @field(args, arg.name)); + const buf = std.mem.asBytes(&value); + comptime assert(buf.len == 16); + break :blk c.sqlite3_bind_blob(handle, @intCast(i), buf.ptr, @intCast(buf.len), c.SQLITE_STATIC); + }, + .float => c.sqlite3_bind_double(handle, @intCast(i), @floatCast(@field(args, arg.name))), + .pointer => |ptr| blk: { + if (ptr.child != u8 or ptr.size != .slice) { + @compileError("unsupported type for sql statement: " ++ @typeName(arg.type)); + } + const text = @field(args, arg.name); + break :blk c.sqlite3_bind_blob(handle, @intCast(i), text.ptr, @intCast(text.len), c.SQLITE_STATIC); + }, + else => @compileError("unsupported type for sql statement: " ++ @typeName(arg.type)), + }; + if (bind_res != c.SQLITE_OK) { + assert(bind_res != c.SQLITE_DONE and res != c.SQLITE_ROW); + try db.check_errc(bind_res); + } + } + return Stmt{ .handle = handle.? }; + } + + inline fn deinit(self: Stmt) void { + _ = c.sqlite3_finalize(self.handle); + } + + inline fn eval(self: Stmt) c_int { + return c.sqlite3_step(self.handle); + } + + fn read_col(self: Stmt, column: usize, comptime T: type) T { + switch (@typeInfo(T)) { + .int => |int| { + if (int.bits <= 64) { + return c.sqlite3_column_int64(self.stmt.handle, @intCast(column)); + } + comptime assert(int.bits <= 128); + const bytes: [*c]const u8 = @ptrCast(c.sqlite3_column_blob(self.handle, @intCast(column))); + const len = c.sqlite3_column_bytes(self.handle, @intCast(column)); + assert(len == 16); + const value = std.mem.bytesAsValue(u128, bytes[0..16]); + return std.mem.bigToNative(u128, value.*); + }, + .float => { + return c.sqlite3_column_double(self.handle, @intCast(column)); + }, + .pointer => |ptr| { + if (ptr.child != u8 or ptr.size != .slice) { + @compileError("unsupported field type: " ++ @typeName(T)); + } + const text: [*c]const u8 = @ptrCast(c.sqlite3_column_blob(self.handle, @intCast(column))); + const len = c.sqlite3_column_bytes(self.handle, @intCast(column)); + return if (len == 0) "" else text[0..@intCast(len)]; + }, + else => @compileError("unsupported field type: " ++ @typeName(T)), + } + } +}; + +const Error = error{ + Aborted, + Busy, + CantOpen, + Constraint, + Corrupted, + DiskQuota, + Fsync, + IO, + Lock, + PermissionDenied, + SystemResources, + Unexpected, +}; + +fn error_from_code(errc: c_int) Error!void { + return switch (errc & 0xff) { + c.SQLITE_OK, c.SQLITE_ROW, c.SQLITE_DONE => {}, + c.SQLITE_ABORT => error.Aborted, + c.SQLITE_AUTH => unreachable, + c.SQLITE_BUSY => error.Busy, + c.SQLITE_CANTOPEN => error.CantOpen, + c.SQLITE_CONSTRAINT => error.Constraint, + c.SQLITE_CORRUPT => error.Corrupted, + c.SQLITE_EMPTY => unreachable, + c.SQLITE_ERROR => error.Unexpected, + c.SQLITE_FORMAT => unreachable, + c.SQLITE_FULL => error.DiskQuota, + c.SQLITE_INTERNAL => unreachable, + c.SQLITE_INTERRUPT => unreachable, + c.SQLITE_IOERR => switch (errc) { + c.SQLITE_IOERR_CORRUPTFS => error.Corrupted, + c.SQLITE_IOERR_DATA => error.Corrupted, + c.SQLITE_IOERR_FSYNC, c.SQLITE_IOERR_DIR_FSYNC => error.Fsync, + c.SQLITE_IOERR_LOCK, c.SQLITE_IOERR_RDLOCK => error.Lock, + c.SQLITE_IOERR_NOMEM => error.SystemResources, + else => error.IO, + }, + c.SQLITE_LOCKED => unreachable, + c.SQLITE_MISMATCH => unreachable, + c.SQLITE_MISUSE => unreachable, + c.SQLITE_NOLFS => error.DiskQuota, + c.SQLITE_NOMEM => error.SystemResources, + c.SQLITE_NOTADB => error.Corrupted, + c.SQLITE_NOTFOUND => unreachable, + c.SQLITE_NOTICE => unreachable, + c.SQLITE_PERM => error.PermissionDenied, + c.SQLITE_PROTOCOL => unreachable, + c.SQLITE_RANGE => unreachable, + c.SQLITE_READONLY => unreachable, + else => unreachable, + }; +} |