const std = @import("std"); const sqlite = @import("sqlite"); const assert = std.debug.assert; 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, // Source: 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.toError())); 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.toError(handle); } /// Closes the database and releases all of its resources. /// /// # Panics /// /// Panics in debug mode if there exist unfinalised prepared statements, /// BLOB handlers, and/or unfinished backup objects. /// /// See https://sqlite.org/c3ref/close.html pub fn deinit(self: *Database) void { const result: Result = .from(sqlite.sqlite3_close(self.handle)); if (!result.isOk()) @panic("There are still unfinalised and/or unfinished objects"); self.* = undefined; } // Source: 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.toError(self.handle); } }; /// SQLite result codes. /// /// Primary result codes define a broad category of results, whereas extended /// result codes provide more detail. The primary result code is always part of /// an extended result code. /// /// See https://sqlite.org/rescode.html const Result = enum(i32) { _, // zig fmt: off /// Primary result codes. 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, /// Returns the primary result code as type `T`. fn as(self: Primary, comptime T: type) T { return @as(T, @intFromEnum(self)); } }; /// Extended result codes. 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)), }; // zig fmt: on /// Returns the result code corresponding to the given integer value. fn from(integer: anytype) Result { assert(std.enums.fromInt(Primary, integer) != null or std.enums.fromInt(Extended, integer) != null); return @enumFromInt(integer); } /// Returns `true` if the primary result code is `.ok`. fn isOk(self: Result) bool { return if (self.asPrimary() == .ok) true else false; } /// Returns the error name corresponding to the result code. The `handle` is /// needed to fetch the extended result code, if it exists, only if `self` /// is not already an extended result code. fn toError(self: Result, handle: ?*sqlite.sqlite3) Database.Error { if (std.enums.fromInt(Extended, @intFromEnum(self))) |extended| switch (extended) { inline else => |code| return @field(Database.Error, @tagName(code)), }; const value: Result = .from(sqlite.sqlite3_extended_errcode(handle)); if (std.enums.fromInt(Extended, @intFromEnum(value))) |extended| switch (extended) { inline else => |code| return @field(Database.Error, @tagName(code)), } else switch (self.asPrimary()) { inline else => |code| return @field(Database.Error, @tagName(code)), } } /// Returns the primary result 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 }); }