aboutsummaryrefslogtreecommitdiff
path: root/src/db/sqlite.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/db/sqlite.zig')
-rw-r--r--src/db/sqlite.zig478
1 files changed, 242 insertions, 236 deletions
diff --git a/src/db/sqlite.zig b/src/db/sqlite.zig
index 4c59b62..28d6487 100644
--- a/src/db/sqlite.zig
+++ b/src/db/sqlite.zig
@@ -1,7 +1,176 @@
const std = @import("std");
const sqlite = @import("sqlite");
const assert = std.debug.assert;
-const math = std.math;
+
+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;
@@ -88,168 +257,16 @@ const math = std.math;
// };
// }
-
-pub const Database = struct {
- schema: Schema,
-
- pub fn foo(self: Database) void {
- _ = self;
- std.debug.print("asdf\n", .{});
- }
-};
-
-pub const Schema = struct {
- tables: type,
-
- // tables = union(enum) {
- // contacts: union(enum) {
- // first_name: []const u8 = "Hans",
- // last_name: []const u8 = "Mustermann",
- // maybe_null: ?usize,
- // },
- // other: union(enum) {
- // ...
- // },
- // }
-
- pub fn from(comptime T: type) Schema {
- const fields = switch (@typeInfo(T)) {
- .@"struct" => |s| s.fields,
- else => @compileError("Schema must be of type `struct`"),
- };
-
- // TODO: check well-formedness of schema
-
-
- const Int = @Int(.unsigned, @bitSizeOf(@TypeOf(fields.len)) - @clz(fields.len));
-
- var table_names: [fields.len][]const u8 = undefined;
- var table_values: [fields.len]Int = undefined;
- var table_types: [fields.len]type = undefined;
-
- inline for (&table_names, &table_value, &table_types, fields, 0..) |table_name, table_value, table_type, field, index| {
- TODO
- }
-
- const Tag = @Enum(Int, .exhaustive, &table_names, &table_values);
- const Tables = @Union(.auto, Tag, &table_names, &table_types, &@splat(.{}));
-
-
- // const tables = tbl: {
- // const Int = @Int(.unsigned, math.log2_int_ceil(usize, schema.len));
- //
- // var names: [schema.len][]const u8 = undefined;
- // var types: [schema.len]type = undefined;
- // var values: [schema.len]Int = undefined;
- //
- // inline for (&names, &types, &values, schema, 0..) |*name, *ty, *value, table, index| {
- // name.* = table.name;
- // ty.* = table.type;
- // value.* = index;
- // }
- //
- // const Tag = @Enum(Int, .exhaustive, &names, &values);
- // const
- //
- // break :tbl @;
- // };
-
-// @Int(comptime signedness: std.builtin.Signedness, comptime bits: u16) type
-// @Union(
-// comptime layout: std.builtin.Type.ContainerLayout,
-// /// Either the integer tag type, or the integer backing type, depending on `layout`.
-// comptime ArgType: ?type,
-// comptime field_names: []const []const u8,
-// comptime field_types: *const [field_names.len]type,
-// comptime field_attrs: *const [field_names.len]std.builtin.Type.UnionField.Attributes,
-// ) type
-// @Enum(
-// comptime TagInt: type,
-// comptime mode: std.builtin.Type.Enum.Mode,
-// comptime field_names: []const []const u8,
-// comptime field_values: *const [field_names.len]TagInt,
-// ) type
-
-
- return .{ .tables = T };
- }
-};
-
-
-
-
-
-// pub const Schema = union(enum) {
-// foo,
-//
-// pub fn from(comptime T: type) Schema {
-// // TODO: build enum from struct fields
-// // TODO: table column name is @EnumLiteral()
-// switch (@typeInfo(T)) {
-// else => |x| @compileLog(x),
-// }
-//
-// return .foo;
-// }
-// };
-
-
-
-// pub const Schema = struct {
-// tables: []const Table,
+// pub const Database = struct {
+// schema: Schema,
//
-// const Table = struct {
-// name: @EnumLiteral(),
-// };
-//
-// pub fn from(comptime S: type) Schema {
-// // TODO: build enum from struct fields
-// // TODO: table column name is @EnumLiteral()
-// switch (@typeInfo(S)) {
-// else => |x| @compileLog(x),
-// }
-// return .{ .tables = &.{} };
+// pub fn foo(self: Database) void {
+// _ = self;
+// std.debug.print("asdf\n", .{});
// }
// };
-
-// const MyTable = struct {
-// const Column1 = struct {
-// pub fn eql(element: []const u8) Condition {
-// return .{
-// .table = MyTable.name,
-// }
-// }
-// }
-// }
-
-
-
-
-
-// const Schema = struct {
-// tables: []Table,
-//
-// pub fn from(comptime schema: anytype) {}
-// }
-//
-// pub const Table = struct {
-// name: @EnumLiteral(),
-// columns: []Column,
-//
-// pub const Column = struct {
-// name: @EnumLiteral(),
-// type: enum {
-// integer,
-// text,
-// },
-// flags: packed struct {
-// .is_primary
-// }
-// };
-// };
-
/// SQLite result codes.
///
/// Primary result codes define a broad category of results, whereas extended
@@ -393,27 +410,22 @@ const Result = enum(i32) {
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)),
- // }
- // }
+ 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 {
@@ -422,72 +434,66 @@ const Result = enum(i32) {
};
test {
- // const schema = &.{
- // .{
- // .name = .contacts,
- // .columns = &.{
- // .{ .name = .contact_id, .type = .integer, .is_primary = true },
- // .{ .name = .first_name, .type = .text, .is_not_null = true },
- // .{ .name = .last_name, .type = .text, .is_not_null = true },
- // .{ .name = .email, .text = .text , .is_not_null = true, .is_unique = true },
- // .{ .name = .phone, .text = .text , .is_not_null = true, .is_unique = true },
- // }
- // },
- // };
- //
- // const schema = .{
- // .contacts = .{
- // .contact_id = .{ .integer, .is_primary },
- // .first_name = .{ .text, .not_null },
- // .last_name = .{ .text, .not_null },
- // .email = .{ .text, .not_null, .unique },
- // .phone = .{ .text, .not_null, .unique },
- // },
- // };
+ const Schema = struct {
+ contacts: Contacts,
+ foo: Foo,
- // 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`.
- const schema: Schema = .from(struct {
- contacts: struct {
- // contact_id: Primary(usize),
- first_name: []const u8 = "Hans",
- last_name: []const u8 = "Mustermann",
- maybe_null: ?usize,
- },
- });
-
- const db: Database = .{ .schema = schema };
+ const Foo = struct {
+ bar: usize,
+ };
- std.debug.print("{}\n", .{ db });
+ const Contacts = struct {
+ contact_id: usize, // CHANGEME: to `Type`
- db.foo();
+ const all = &.{ .contact_id };
+ };
+ };
- // const Contacts = struct {
- // first_name: []const u8,
- // last_name : []const u8,
- //
- // pub fn foo() void {
- // std.debug.print("asdf\n", .{});
- // }
- // };
- // const Other = struct {};
- // const schema: Schema = .from(&.{ Contacts, Other });
+ 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;
- // const db: Database(schema) = try .init(.temp_in_memory);
- // _ = db;
- // defer db.deinit();
+ // // 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),
+ //
+ // )
+}
- // 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
- // \\ );
+// 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}),
+ // },
+ // };
//
- // std.debug.print("{any}\n", .{ query });
-}
+ // // NOTE: init connects to DB and creates tables if not already present
+ // var db: Database(Schema(S)) = try .init(.temp_in_memory);
+ // db.deinit();
+// }