diff options
Diffstat (limited to 'src/db/sqlite.zig')
| -rw-r--r-- | src/db/sqlite.zig | 247 |
1 files changed, 247 insertions, 0 deletions
diff --git a/src/db/sqlite.zig b/src/db/sqlite.zig new file mode 100644 index 0000000..8b2c42c --- /dev/null +++ b/src/db/sqlite.zig @@ -0,0 +1,247 @@ +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 }); +} |