const std = @import("std"); const sqlite = @import("sqlite"); pub const Database = struct { handle: *sqlite.sqlite3, pub const Error = set: { var set: type = error{}; for (std.meta.fieldNames(Result.Primary)) |name| { set = set || @TypeOf(@field(anyerror, name)); } for (std.meta.fieldNames(Result.Extended)) |name| { set = set || @TypeOf(@field(anyerror, name)); } break :set set; }; pub const Kind = union(enum) { temp_in_memory, temp_on_disk, path: []const u8, }; pub const Query = struct { handle: *sqlite.sqlite3_stmt, // https://sqlite.org/c3ref/finalize.html pub fn deinit(self: *Query) void { const result: Result = .from(sqlite.sqlite3_finalize(self.handle)); // TODO: maybe don't panic? if (!result.isOk()) @panic("Last error was " ++ @errorName(result.asError())); self.* = undefined; } }; // Source: https://sqlite.org/c3ref/open.html pub fn init(kind: Kind) Error!Database { const name = switch (kind) { .temp_in_memory => ":memory:", .temp_on_disk => "", .path => |path| path, }; var handle: ?*sqlite.sqlite3 = null; const result: Result = .from(sqlite.sqlite3_open(name.ptr, &handle)); return if (result.isOk()) .{ .handle = handle.? } else result.asError(handle); } // https://sqlite.org/c3ref/close.html pub fn deinit(self: *Database) void { const result: Result = .from(sqlite.sqlite3_close(self.handle)); // TODO: check if we can loop over the unfinalised objects and commit them if (!result.isOk()) @panic("There are still unfinalised objects"); self.* = undefined; } // https://sqlite.org/c3ref/prepare.html pub fn query(self: *Database, statement: []const u8) Error!Query { var handle: ?*sqlite.sqlite3_stmt = null; var unused: [*c]const u8 = null; const length: c_int = @truncate(@as(isize, @intCast(statement.len))); const result: Result = .from(sqlite.sqlite3_prepare_v2(self.handle, statement.ptr, length, &handle, &unused)); return if (result.isOk()) .{ .handle = handle.? } else result.asError(self.handle); } }; // Source: https://sqlite.org/rescode.html const Result = enum(i32) { _, // zig fmt: off const Primary = enum(u8) { ok = 0, @"error" = 1, internal = 2, perm = 3, abort = 4, busy = 5, locked = 6, nomem = 7, readonly = 8, interrupt = 9, ioerr = 10, corrupt = 11, notfound = 12, full = 13, cantopen = 14, protocol = 15, empty = 16, schema = 17, toobig = 18, constraint = 19, mismatch = 20, misuse = 21, nolfs = 22, auth = 23, format = 24, range = 25, notadb = 26, notice = 27, warning = 28, row = 100, done = 101, fn as(self: Primary, comptime T: type) T { return @as(T, @intFromEnum(self)); } }; // zig fmt: off const Extended = enum(i32) { error_missing_collseq = (Primary.@"error".as(i32) | ( 1 << 8)), error_retry = (Primary.@"error".as(i32) | ( 2 << 8)), error_snapshot = (Primary.@"error".as(i32) | ( 3 << 8)), error_reservesize = (Primary.@"error".as(i32) | ( 4 << 8)), error_key = (Primary.@"error".as(i32) | ( 5 << 8)), error_unable = (Primary.@"error".as(i32) | ( 6 << 8)), ioerr_read = (Primary.ioerr.as(i32) | ( 1 << 8)), ioerr_short_read = (Primary.ioerr.as(i32) | ( 2 << 8)), ioerr_write = (Primary.ioerr.as(i32) | ( 3 << 8)), ioerr_fsync = (Primary.ioerr.as(i32) | ( 4 << 8)), ioerr_dir_fsync = (Primary.ioerr.as(i32) | ( 5 << 8)), ioerr_truncate = (Primary.ioerr.as(i32) | ( 6 << 8)), ioerr_fstat = (Primary.ioerr.as(i32) | ( 7 << 8)), ioerr_unlock = (Primary.ioerr.as(i32) | ( 8 << 8)), ioerr_rdlock = (Primary.ioerr.as(i32) | ( 9 << 8)), ioerr_delete = (Primary.ioerr.as(i32) | (10 << 8)), ioerr_blocked = (Primary.ioerr.as(i32) | (11 << 8)), ioerr_nomem = (Primary.ioerr.as(i32) | (12 << 8)), ioerr_access = (Primary.ioerr.as(i32) | (13 << 8)), ioerr_checkreservedlock = (Primary.ioerr.as(i32) | (14 << 8)), ioerr_lock = (Primary.ioerr.as(i32) | (15 << 8)), ioerr_close = (Primary.ioerr.as(i32) | (16 << 8)), ioerr_dir_close = (Primary.ioerr.as(i32) | (17 << 8)), ioerr_shmopen = (Primary.ioerr.as(i32) | (18 << 8)), ioerr_shmsize = (Primary.ioerr.as(i32) | (19 << 8)), ioerr_shmlock = (Primary.ioerr.as(i32) | (20 << 8)), ioerr_shmmap = (Primary.ioerr.as(i32) | (21 << 8)), ioerr_seek = (Primary.ioerr.as(i32) | (22 << 8)), ioerr_delete_noent = (Primary.ioerr.as(i32) | (23 << 8)), ioerr_mmap = (Primary.ioerr.as(i32) | (24 << 8)), ioerr_gettemppath = (Primary.ioerr.as(i32) | (25 << 8)), ioerr_convpath = (Primary.ioerr.as(i32) | (26 << 8)), ioerr_vnode = (Primary.ioerr.as(i32) | (27 << 8)), ioerr_auth = (Primary.ioerr.as(i32) | (28 << 8)), ioerr_begin_atomic = (Primary.ioerr.as(i32) | (29 << 8)), ioerr_commit_atomic = (Primary.ioerr.as(i32) | (30 << 8)), ioerr_rollback_atomic = (Primary.ioerr.as(i32) | (31 << 8)), ioerr_data = (Primary.ioerr.as(i32) | (32 << 8)), ioerr_corruptfs = (Primary.ioerr.as(i32) | (33 << 8)), ioerr_in_page = (Primary.ioerr.as(i32) | (34 << 8)), ioerr_badkey = (Primary.ioerr.as(i32) | (35 << 8)), ioerr_codec = (Primary.ioerr.as(i32) | (36 << 8)), locked_sharedcache = (Primary.locked.as(i32) | ( 1 << 8)), locked_vtab = (Primary.locked.as(i32) | ( 2 << 8)), busy_recovery = (Primary.busy.as(i32) | ( 1 << 8)), busy_snapshot = (Primary.busy.as(i32) | ( 2 << 8)), busy_timeout = (Primary.busy.as(i32) | ( 3 << 8)), cantopen_notempdir = (Primary.cantopen.as(i32) | ( 1 << 8)), cantopen_isdir = (Primary.cantopen.as(i32) | ( 2 << 8)), cantopen_fullpath = (Primary.cantopen.as(i32) | ( 3 << 8)), cantopen_convpath = (Primary.cantopen.as(i32) | ( 4 << 8)), cantopen_symlink = (Primary.cantopen.as(i32) | ( 6 << 8)), corrupt_vtab = (Primary.corrupt.as(i32) | ( 1 << 8)), corrupt_sequence = (Primary.corrupt.as(i32) | ( 2 << 8)), corrupt_index = (Primary.corrupt.as(i32) | ( 3 << 8)), readonly_recovery = (Primary.readonly.as(i32) | ( 1 << 8)), readonly_cantlock = (Primary.readonly.as(i32) | ( 2 << 8)), readonly_rollback = (Primary.readonly.as(i32) | ( 3 << 8)), readonly_dbmoved = (Primary.readonly.as(i32) | ( 4 << 8)), readonly_cantinit = (Primary.readonly.as(i32) | ( 5 << 8)), readonly_directory = (Primary.readonly.as(i32) | ( 6 << 8)), abort_rollback = (Primary.abort.as(i32) | ( 2 << 8)), constraint_check = (Primary.constraint.as(i32) | ( 1 << 8)), constraint_commithook = (Primary.constraint.as(i32) | ( 2 << 8)), constraint_foreignkey = (Primary.constraint.as(i32) | ( 3 << 8)), constraint_function = (Primary.constraint.as(i32) | ( 4 << 8)), constraint_notnull = (Primary.constraint.as(i32) | ( 5 << 8)), constraint_primarykey = (Primary.constraint.as(i32) | ( 6 << 8)), constraint_trigger = (Primary.constraint.as(i32) | ( 7 << 8)), constraint_unique = (Primary.constraint.as(i32) | ( 8 << 8)), constraint_vtab = (Primary.constraint.as(i32) | ( 9 << 8)), constraint_rowid = (Primary.constraint.as(i32) | (10 << 8)), constraint_pinned = (Primary.constraint.as(i32) | (11 << 8)), constraint_datatype = (Primary.constraint.as(i32) | (12 << 8)), notice_recover_wal = (Primary.notice.as(i32) | ( 1 << 8)), notice_recover_rollback = (Primary.notice.as(i32) | ( 2 << 8)), notice_rbu = (Primary.notice.as(i32) | ( 3 << 8)), warning_autoindex = (Primary.warning.as(i32) | ( 1 << 8)), auth_user = (Primary.auth.as(i32) | ( 1 << 8)), ok_load_permanently = (Primary.ok.as(i32) | ( 1 << 8)), }; fn from(value: anytype) Result { return @enumFromInt(value); } fn isOk(self: Result) bool { return if (self.asPrimary() == .ok) true else false; } fn asError(self: Result, handle: ?*sqlite.sqlite3) Database.Error { const result: Result = .from(sqlite.sqlite3_extended_errcode(handle)); const primary = self.asPrimary(); if (self == result) switch (primary) { inline else => |code| return @field(Database.Error, @tagName(code)), }; const extended: Extended = @enumFromInt(@intFromEnum(result)); return switch (extended) { inline else => |code| @field(Database.Error, @tagName(code)), }; } fn asPrimary(self: Result) Primary { return @enumFromInt(0x000000FF & @intFromEnum(self)); } }; test { var db: Database = try .init(.temp_in_memory); // defer db.deinit(); const query = db.query( \\ CREATE TABLE contacts ( \\ contact_id INTEGER PRIMARY KEY, \\ first_name TEXT NOT NULL, \\ last_name TEXT NOT NULL, \\ email TEXT NOT NULL UNIQUE, \\ phone TEXT NOT NULL UNIQUE \\ ); ); std.debug.print("{any}\n", .{ query }); }