1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
|
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});
}
|