const std = @import("std"); const sqlite = @import("sqlite"); const assert = std.debug.assert; pub fn Database(comptime T: type) type { const tables = switch (@typeInfo(T)) { .@"struct" => |s| s.fields, else => @compileError("Database schema must be of type `struct`"), }; // NOTE: `std.math.log2_int_ceil(@TypeOf(tables.len), tables.len)` panics // if `tables.len` is zero. The below properly handles the case of // a (useless) schema that does not define any tables. const TagIntTables = @Int(.unsigned, @bitSizeOf(@TypeOf(tables.len)) - @clz(tables.len)); var table_names : [tables.len][]const u8 = undefined; var table_types : [tables.len]type = undefined; var table_values: [tables.len]TagIntTables = undefined; inline for (&table_names, &table_types, &table_values, tables, 0..) |*table_name, *table_type, *table_value, table, table_index| { const columns = switch (@typeInfo(table.type)) { .@"struct" => |s| s.fields, else => @compileError("Table `" ++ table.name ++ "` must be of type `struct`"), }; // NOTE: A table must have at least one column // (see `https://sqlite.org/syntax/create-table-stmt.html`). if (columns.len == 0) @compileError("Table `" ++ table.name ++ "` must have at least one column"); const TagIntColumns = @Int(.unsigned, std.math.log2_int_ceil(@TypeOf(columns.len), columns.len)); var column_names : [columns.len][]const u8 = undefined; var column_types : [columns.len]type = undefined; var column_values: [columns.len]TagIntColumns = undefined; inline for (&column_names, &column_types, &column_values, columns, 0..) |*column_name, *column_type, *column_value, column, column_index| { // TODO: check column.type (of type `Type`) column_name.* = column.name; column_type.* = column.type; column_value.* = column_index; } const TagColumns = @Enum(TagIntColumns, .exhaustive, &column_names, &column_values); const Columns = @Union(.auto, TagColumns, &column_names, &column_types, &@splat(.{})); table_name.* = table.name; table_type.* = Columns; table_value.* = table_index; } const TagTables = @Enum(TagIntTables, .exhaustive, &table_names, &table_values); const Tables = @Union(.auto, TagTables, &table_names, &table_types, &@splat(.{})); return struct { inner: Inner, pub const Check = enum { ok, busy, }; 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, }; const Schema = Tables; const Self = @This(); const Inner = struct { handle: *sqlite.sqlite3, }; pub fn init(kind: Kind) Error!Self { 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)); const primary = result.asPrimary(); return if (primary == .ok) .{ .handle = handle.? } else result.toError(Error, handle); } pub fn deinit(self: *Self) Check { const result: Result = .from(sqlite.sqlite3_close(self.handle)); const primary = result.asPrimary(); if (primary == .busy) return .busy; self.* = undefined; return .ok; } // NOTE: SELECT without FROM is non-conforming SQL // https://stackoverflow.com/questions/3732422/select-from-nothing // https://modern-sql.com/use-case/select-without-from pub fn select(self: Self, comptime table: std.meta.Tag(Schema), comptime columns: []const std.meta.Tag(@FieldType(Schema, @tagName(table)))) Select(Self, table, columns) { _ = rows; return .{ .db = self.handle }; } }; } fn Select(comptime Db: type, comptime table: std.meta.Tag(Db.Schema), comptime columns: []const std.meta.Tag(@FieldType(Db.Schema, @tagName(table)))) type { return struct { db: *sqlite.sqlite3, comptime table: std.meta.Tag(Db.Schema) = table, comptime columns: []const std.meta.Tag(@FieldType(Db.Schema, @tagName(table))) = columns, const Self = @This(); // pub fn where(self: Self, ) }; } // pub const Type = struct { // string: String.Static, // type: type, // // pub fn integer() Type { // // end of Type recursion // } // // pub fn unique(Type) Type { // // } // // ... // }; // NEVER PUB!!! const String = struct { const Static = struct { value: []const u8, }; const Dynamic = struct { fmt: []const u8, args: type, }; fn contains(haystack: []const u8, needle: []const u8) bool { if (haystack.len < needle.len) return false; var start: usize = 0; while (start + needle.len <= haystack.len) : (start += 1) { if (std.mem.eql(u8, haystack[start..(start + needle.len)], needle)) return true; } return false; } }; // pub fn Database(comptime S: Schema) type { // _ = S; // return struct { // handle: *sqlite.sqlite3, // // pub const Check = enum { // ok, // unfinalised, // }; // // 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, returning `.ok`. // /// // /// If there exist unfinalised prepared statements, BLOB handlers, and/or // /// unfinished backup objects, `.unfinalised` is returned; // /// // /// See https://sqlite.org/c3ref/close.html // pub fn deinit(self: *Database) Check { // const result: Result = .from(sqlite.sqlite3_close(self.handle)); // // if (!result.isOk()) .unfinalised; // // self.* = undefined; // // return .ok; // } // // // 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 result: Result = .from(sqlite.sqlite3_prepare_v2(self.handle, statement.ptr, @intCast(statement.len), &handle, &unused)); // // return if (result.isOk()) .{ .handle = handle.? } else result.toError(self.handle); // } // }; // } // pub const Database = struct { // schema: Schema, // // pub fn foo(self: Database) void { // _ = self; // std.debug.print("asdf\n", .{}); // } // }; /// 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 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, comptime Error: type, handle: ?*sqlite.sqlite3) Error { if (std.enums.fromInt(Extended, @intFromEnum(self))) |extended| switch (extended) { inline else => |code| return @field(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(Error, @tagName(code)), } else switch (self.asPrimary()) { inline else => |code| return @field(Error, @tagName(code)), } } /// Returns the primary result code. fn asPrimary(self: Result) Primary { return @enumFromInt(0x000000FF & @intFromEnum(self)); } }; test { const Schema = struct { contacts: Contacts, foo: Foo, const Foo = struct { bar: usize, }; const Contacts = struct { contact_id: usize, // CHANGEME: to `Type` const all = &.{ .contact_id }; }; }; var db: Database(Schema) = try .init(.temp_in_memory); defer assert(db.deinit() == .ok); _ = db.select(.contacts, &.{ .contact_id }, .all); var ty = db.select(.contacts, Schema.Contacts.all, .all); ty.table = .foo; // // SELECT * == SELECT ALL * // db.select(.all).from(...) // // db.select(.distinct(...)).from(...)... // // // If the simple SELECT is a SELECT ALL, then the entire set of result rows are returned by the SELECT. If neither ALL or DISTINCT are present, then the behavior is as if ALL were specified. // db.select(enum { all, distinct }, // * == .all(), // table-name.* == .table(.table_name), // // ) } // test { // NOTE: we don't use `union(enum)` here as if a column has a default value, // an integer tag type is needed explicitely, which, from a usability // standpoint, is not great; we can calulate this value in `Schema.from`. // // CREATE TABLE contacts ( // contact_id INTEGER PRIMARY KEY, // first_name TEXT NOT NULL DEFAULT "Hans", // last_name TEXT NOT NULL DEFAULT "Mustermann", // nickname TEXT, // email TEXT NOT NULL UNIQUE, // phone TEXT NOT NULL UNIQUE // ); // const S = struct { // contacts: struct { // contact_id: PrimaryKey(Integer, .{}), // first_name: VarChar(255, .{}) = "Hans", // last_name: VarChar(255, .{}) = "Mustermann", // nickname: ?VarChar(255, .{}), // email: VarChar(255, .{.unique}), // phone: VarChar(255, .{.unique}), // }, // }; // // // NOTE: init connects to DB and creates tables if not already present // var db: Database(Schema(S)) = try .init(.temp_in_memory); // db.deinit(); // }