aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNathan Reiner <nathan@nathanreiner.xyz>2026-05-21 16:04:53 +0200
committerNathan Reiner <nathan@nathanreiner.xyz>2026-05-21 16:04:53 +0200
commit79ba17323c33c7e7e6b4b3ef526db85f1cf9c086 (patch)
treeaa5e2f6b6d21ccf6d9f24779e87d7d3c65def345
parent78ba18b890655b12650afedac3a9d57ef95567d0 (diff)
parenta9b6b2495de6c97fd17b37885dd99d8557a9ea0e (diff)
Merge branch 'develop' of https://gitlab.com/Jockerkat/memora into dev
-rw-r--r--build.zig8
-rw-r--r--build.zig.zon1
-rw-r--r--src/db/root.zig7
-rw-r--r--src/db/sqlite.h1
-rw-r--r--src/db/sqlite.zig247
5 files changed, 261 insertions, 3 deletions
diff --git a/build.zig b/build.zig
index 9627782..9f8d9dc 100644
--- a/build.zig
+++ b/build.zig
@@ -4,10 +4,18 @@ pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
+ const sqlite_c = b.addTranslateC(.{
+ .root_source_file = b.path("src/db/sqlite.h"),
+ .target = target,
+ .optimize = optimize,
+ });
+ sqlite_c.linkSystemLibrary("sqlite3", .{});
+
const db_mod = b.addModule("db", .{
.root_source_file = b.path("src/db/root.zig"),
.target = target,
});
+ db_mod.addImport("sqlite", sqlite_c.createModule());
const api_mod = b.addModule("api", .{
.root_source_file = b.path("src/api/root.zig"),
diff --git a/build.zig.zon b/build.zig.zon
index 69dae0f..afb2159 100644
--- a/build.zig.zon
+++ b/build.zig.zon
@@ -1,6 +1,7 @@
.{
.name = .memora,
.version = "0.0.0",
+ .fingerprint = 0xf901ac636a172b52,
.minimum_zig_version = "0.16.0",
.fingerprint = 0xf901ac6387a8536b,
.dependencies = .{
diff --git a/src/db/root.zig b/src/db/root.zig
index 049a63b..4ca9847 100644
--- a/src/db/root.zig
+++ b/src/db/root.zig
@@ -1,9 +1,10 @@
-//! Implements the layer between the sqlite
-//! database and the interface for the api endpoints or
-//! the cli.
+//! Implements the layer between the sqlite database and the interface for the
+//! API endpoints or the CLI.
const std = @import("std");
+pub const sqlite = @import("sqlite.zig");
+
test {
_ = std.testing.refAllDecls(@This());
}
diff --git a/src/db/sqlite.h b/src/db/sqlite.h
new file mode 100644
index 0000000..f52e1f0
--- /dev/null
+++ b/src/db/sqlite.h
@@ -0,0 +1 @@
+#include <sqlite3.h>
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 });
+}