//! 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, }; }