const std = @import("std"); const sqlite = @import("sqlite"); pub fn Database(comptime T: type) type { return struct { const Schema = DatabaseSchema(T); const Self = @This(); pub const Kind = union(enum) { temp_in_memory, temp_on_disk, path: []const u8, }; pub fn init(kind: Kind) error{}!Self { _ = kind; std.debug.print("{}\n", .{Self.Schema}); return .{}; } pub fn deinit(self: *Self) void { self.* = undefined; } // TODO: make functions out of `table` and `columns`: Table(Schema), []const Column(Schema, table) for readability? pub fn select(self: *Self, comptime table: std.meta.Tag(Schema), comptime columns: []const std.meta.Tag(@FieldType(Schema, @tagName(table)))) Select(Schema, table, columns) { _ = self; std.debug.print("{any}\n", .{ columns }); return .{ // .handle = self.handle, }; } }; } fn Select(comptime Schema: type, comptime table: std.meta.Tag(Schema), comptime columns: []const std.meta.Tag(@FieldType(Schema, @tagName(table)))) type { return struct { table: std.meta.Tag(Schema) = table, // TODO: db handle, etc. // FIXME: pass in columns instead pub fn where(self: @This(), condition: Condition(Schema, table)) void { _ = self; _ = condition; std.debug.print("SELECT {any} FROM {}\n", .{ columns, table }); } }; // const columns = switch(@typeInfo(@TypeOf(T))) { // // }; // // std.debug.assert(columns.len > 0); // // const Int = @Int(.unsigned, std.math.log2_int_ceil(usize, columns.len)); // // var names: [columns.len][]const u8 = undefined; // var values: [columns.len]Int = undefined; // // inline for (&names, &values, columns, 0..) |*name, *value, column, index| { // name.* = @tagName(column); // value.* = index; // } // // return @Enum(Int, .exhaustive, &names, &values); } fn DatabaseSchema(comptime T: type) type { const tables = switch (@typeInfo(T)) { .@"struct" => |s| s.fields, else => @compileError("Schema must be of type `struct`"), }; // NOTE: Unlike `std.math.log2_int_ceil(usize, tables.len)`, this doesn't panic // if `tables.len` is zero. const IntSchema = @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]IntSchema = 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: Needing at least one column is defined by the `create-table-stmt` // syntax; see https://sqlite.org/syntax/create-table-stmt.html. if (columns.len == 0) @compileError("Table `" ++ table.name ++ "` must have at least one column"); const IntColumns = @Int(.unsigned, std.math.log2_int_ceil(usize, columns.len)); var column_names: [columns.len][]const u8 = undefined; var column_types: [columns.len]type = undefined; var column_values: [columns.len]IntColumns = undefined; inline for (&column_names, &column_types, &column_values, columns, 0..) |*column_name, *column_type, *column_value, column, column_index| { column_name.* = column.name; column_type.* = column.type; column_value.* = column_index; // TODO: check well-formedness of tables/translate types into SQL types, // i.e. `[]const u8` to VarChar(slice.len) etc. } const Tag = @Enum(IntColumns, .exhaustive, &column_names, &column_values); const Columns = @Union(.auto, Tag, &column_names, &column_types, &@splat(.{})); table_name.* = table.name; table_type.* = Columns; table_value.* = table_index; } const Tag = @Enum(IntSchema, .exhaustive, &table_names, &table_values); const Schema = @Union(.auto, Tag, &table_names, &table_types, &@splat(.{})); return Schema; } // pub const Integer = struct { // pub const unconstrained: Integer = .{}; // // pub fn constrain(constraint: Constraint) Integer { // _ = constraint; // return .{}; // } // }; // pub const Constraint = enum { // not_null, // primary_key, // }; // fn Constraints(comptime T: type) type { // return struct { // not_null: bool = false, // primary_key: bool = false, // default_value: ?T = null, // FIXME: more complex than that (see literal-value in create-table-stmt) // }; // } fn Integer(comptime T: type) type { // TODO: check T is an integer (zig type) return struct { const Type = T; const string = "INTEGER"; }; } fn Unique(comptime T: type) type { return struct { const Type = T.Type; // TODO: `contains(expected, str)` function const string = if (std.mem.count(u8, T.string, "UNIQUE") > 0) @compileError(std.fmt.comptimePrint("Type `{s}` is already unique", .{ @typeName(T) })) else std.fmt.comptimePrint("UNIQUE {s}", .{ T.string }); }; } fn Default(comptime T: type, comptime value: T.Type) type { return struct { const Type = T.type; const string = if(std.mem.count(u8, T.string, "DEFAULT") > 0) @compileError(std.fmt.comptimePrint("Type `{s}` already has a default value", .{ @typeName(T) })) else std.fmt.comptimePrint("{s} DEFAULT {}", .{ T.string, value }); }; } // ------------------------------------------------- // Constraints // // primary key: feature of a table (if multiple columns) or of a column (if signle) => use decl // -> also applies to foreign keys // const Contacts = struct { id: usize, first_name: []const u8, }; fn Test(comptime Schema: type, comptime table: std.meta.Tag(Schema), comptime columns: []const std.meta.Tag(@FieldType(Schema, @tagName(table)))) type { const ColumnInt = @Int(.unsigned, std.math.log2_int_ceil(usize, columns.len)); var column_names: [columns.len][]const u8 = undefined; var column_values: [columns.len]ColumnInt = undefined; var column_types: [columns.len]type = undefined; inline for (&column_names, &column_values, &column_types, columns, 0..) |*column_name, *column_value, *column_type, column, index| { column_name.* = @tagName(column); column_value.* = index; column_type.* = @FieldType(@FieldType(Schema, @tagName(table)), @tagName(column)); } const ColumnEnum = @Enum(ColumnInt, .exhaustive, &column_names, &column_values); const ColumnUnion = @Union(.auto, ColumnEnum, &column_names, &column_types, &@splat(.{})); return struct { const Self = @This(); const Node = union(enum) { eql: type, }; fn Eql(comptime column: std.meta.Tag(ColumnUnion), value: @FieldType(ColumnUnion, @tagName(column))) type { return struct { const fmt: []const u8 = std.fmt.comptimePrint("{s} = ", .{ @tagName(column) }); const args: @Tuple(&.{ @TypeOf(value) }) = .{ value }; }; } // fn And() type { // todo: concat fmt slices and args tuples // } string: Node, fn eql(comptime column: std.meta.Tag(ColumnUnion), value: @FieldType(ColumnUnion, @tagName(column))) Self { return .{ .string = .{ .eql = Eql(column, value) } }; } // TODO: call this and2 ? fn op_and(comptime lhs: Self, comptime rhs: Self) Self { return .{ .string = std.fmt.comptimePrint("{s} AND {s}", .{ lhs.repr, rhs.repr }) }; } }; } fn Condition(comptime Schema: type, comptime table: std.meta.Tag(Schema)) type { return struct { fn eql(comptime column: std.meta.Tag(@FieldType(Schema, @tagName(table))), value: @FieldType(@FieldType(Schema, @tagName(table)), @tagName(column))) @This() { std.debug.print("column: {}\texpected value: {}\n", .{ column, value }); return .{}; } }; } test "schema" { const Schema = struct { contacts: Contacts, // contacts: struct { // contact_id: Integer = .constrain(.primary_key), // age: Integer = .unconstrained, // contact_id: Integer(usize) = .constrain(.{ .primary_key = true }), // first_name: Default(VarChar(255), "Hans"), // }, }; // const asdf = DatabaseSchema(Schema); // _ = asdf; var db: Database(Schema) = try .init(.temp_in_memory); defer db.deinit(); const x = db.select(.contacts, &.{ .id, .first_name }).where(.eql(.id, 42)); _ = x; const t: Test(@TypeOf(db).Schema, .contacts, &.{ .id, .first_name }) = .eql(.id, 42); // const t: Test(@TypeOf(db).Schema, .contacts, &.{ .id, .first_name }) = .op_and(.eql(.id, 42), .eql(.first_name, "name")); std.debug.print("test output: {s}{}\n", switch (t.string) { .eql => |eq| .{ eq.fmt, eq.args } }); const Int = Default(Unique(Integer(usize)), 42); std.debug.print("default unique integer with default 42: {s}\n", .{ Int.string }); }