285 lines
8.6 KiB
Zig
285 lines
8.6 KiB
Zig
const std = @import("std");
|
|
const util = @import("util");
|
|
const builtin = @import("builtin");
|
|
|
|
const String = []const u8;
|
|
const comptimePrint = std.fmt.comptimePrint;
|
|
|
|
fn baseTypeName(comptime T: type) []const u8 {
|
|
comptime {
|
|
const name = @typeName(T);
|
|
const start = for (name) |_, i| {
|
|
if (name[name.len - i] == '.') {
|
|
// This function has an off-by-one error in the self hosted compiler (-fno-stage1)
|
|
// The following code fixes it as of 2022-08-07
|
|
// TODO: Figure out what's going on here
|
|
if (builtin.zig_backend == .stage1) {
|
|
break name.len - i;
|
|
} else {
|
|
break name.len - i + 1;
|
|
}
|
|
}
|
|
} else 0;
|
|
|
|
return name[start..];
|
|
}
|
|
}
|
|
|
|
fn tableName(comptime T: type) String {
|
|
return comptime util.case.pascalToSnake(baseTypeName(T));
|
|
}
|
|
|
|
// Represents a table bound to an identifier in a sql query
|
|
pub const QueryTable = struct {
|
|
Model: type,
|
|
index: comptime_int,
|
|
|
|
// Gets a fully qualified field from a literal
|
|
pub fn field(comptime self: QueryTable, comptime lit: @Type(.EnumLiteral)) String {
|
|
comptime {
|
|
const f = @as(std.meta.FieldEnum(self.Model), lit);
|
|
return comptimePrint("{s}.{s}", .{ self.as(), @tagName(f) });
|
|
}
|
|
}
|
|
|
|
pub fn select(comptime self: QueryTable, comptime lit: @Type(.EnumLiteral)) ResultColumn {
|
|
return .{
|
|
.@"type" = std.meta.fieldInfo(self.Model, lit).field_type,
|
|
.field = self.field(lit),
|
|
};
|
|
}
|
|
|
|
// returns the declaration to put in the FROM clause
|
|
fn declarationStr(comptime self: QueryTable) String {
|
|
comptime {
|
|
return comptimePrint("{s} AS {s}", .{ tableName(self.Model), self.as() });
|
|
}
|
|
}
|
|
|
|
fn as(comptime self: QueryTable) String {
|
|
comptime {
|
|
return comptimePrint("{s}_{}", .{ tableName(self.Model), self.index });
|
|
}
|
|
}
|
|
};
|
|
|
|
fn makeQueryTable(comptime Model: type, comptime table_index: usize) QueryTable {
|
|
return .{ .Model = Model, .index = table_index };
|
|
}
|
|
|
|
pub fn queryTables(comptime models: []const type) *const [models.len]QueryTable {
|
|
return map(type, QueryTable, models, makeQueryTable);
|
|
}
|
|
|
|
test "QueryTable.declarationStr" {
|
|
const MyTable = struct { id: i64 };
|
|
const tbl = QueryTable{
|
|
.Model = MyTable,
|
|
.index = 0,
|
|
};
|
|
|
|
try std.testing.expectEqualStrings("my_table AS my_table_0", tbl.declarationStr());
|
|
try std.testing.expectEqualStrings("my_table_0.id", tbl.field(.id));
|
|
}
|
|
|
|
test "queryTables constructor" {
|
|
const MyTable = struct { id: i64 };
|
|
const MyOtherTable = struct { val: i64 };
|
|
|
|
const qt = queryTables(&.{ MyTable, MyOtherTable });
|
|
|
|
try std.testing.expectEqual(MyTable, qt[0].Model);
|
|
try std.testing.expectEqual(MyOtherTable, qt[1].Model);
|
|
try std.testing.expectEqualStrings("my_table_0", qt[0].as());
|
|
try std.testing.expectEqualStrings("my_other_table_1", qt[1].as());
|
|
}
|
|
|
|
fn map(comptime T: type, comptime R: type, comptime vals: []const T, comptime func: anytype) *const [vals.len]R {
|
|
var result: [vals.len]R = undefined;
|
|
if (@typeInfo(@TypeOf(func)).Fn.args.len == 2) {
|
|
inline for (vals) |v, i| result[i] = @as(R, func(v, i));
|
|
} else {
|
|
inline for (vals) |v, i| result[i] = @as(R, func(v));
|
|
}
|
|
|
|
return &result;
|
|
}
|
|
|
|
// Combines an array/tuple of strings into a single string, with a copy of
|
|
// joiner in between each one
|
|
fn join(comptime vals: []const String, comptime joiner: String) String {
|
|
if (vals.len == 0) return "";
|
|
|
|
var result: String = "";
|
|
for (vals) |v| {
|
|
result = comptimePrint("{s}{s}{s}", .{ result, joiner, v });
|
|
}
|
|
|
|
return result[joiner.len..];
|
|
}
|
|
|
|
// Stringifies and joins an array of conditions into a single string
|
|
fn joinConditions(comptime cs: []const Condition, comptime joiner: String) String {
|
|
var strs: [cs.len]String = undefined;
|
|
for (cs) |v, i| strs[i] = v.str();
|
|
return join(&strs, joiner);
|
|
}
|
|
|
|
// Represents a condition in a SQL statement
|
|
pub const Condition = union(enum) {
|
|
const BinaryOp = struct {
|
|
lhs: String,
|
|
rhs: String,
|
|
};
|
|
|
|
eql: BinaryOp,
|
|
is_null: String,
|
|
val: String,
|
|
not: *const Condition,
|
|
all: []const Condition,
|
|
any: []const Condition,
|
|
|
|
fn str(comptime self: Condition) String {
|
|
comptime {
|
|
return comptimePrint("({s})", .{switch (self) {
|
|
.eql => |op| comptimePrint("{s} = {s}", .{ op.lhs, op.rhs }),
|
|
.is_null => |val| comptimePrint("{s} IS NULL", .{val}),
|
|
.val => |val| val,
|
|
.not => |c| comptimePrint("NOT {s}", .{c.str()}),
|
|
.all => |cs| joinConditions(cs, " AND "),
|
|
.any => |cs| joinConditions(cs, " OR "),
|
|
}});
|
|
}
|
|
}
|
|
|
|
pub fn eql(comptime lhs: String, comptime rhs: String) Condition {
|
|
return .{
|
|
.eql = .{ .lhs = lhs, .rhs = rhs },
|
|
};
|
|
}
|
|
|
|
pub fn all(comptime cs: []const Condition) Condition {
|
|
return .{
|
|
.all = cs,
|
|
};
|
|
}
|
|
};
|
|
|
|
test "Condition.str()" {
|
|
try std.testing.expectEqualStrings(
|
|
"((abc = def) AND (def = abc))",
|
|
(comptime Condition{ .all = &.{
|
|
.{ .eql = .{ .lhs = "abc", .rhs = "def" } },
|
|
.{ .eql = .{ .lhs = "def", .rhs = "abc" } },
|
|
} }).str(),
|
|
);
|
|
|
|
try std.testing.expectEqualStrings(
|
|
"((abc IS NULL) OR (NOT (def)))",
|
|
(comptime Condition{ .any = &.{
|
|
.{ .is_null = "abc" },
|
|
.{ .not = &.{ .val = "def" } },
|
|
} }).str(),
|
|
);
|
|
}
|
|
|
|
const ResultColumn = struct {
|
|
@"type": type,
|
|
field: []const u8,
|
|
|
|
pub fn toSelectClause(comptime self: ResultColumn) String {
|
|
return self.field;
|
|
}
|
|
|
|
pub fn toStructField(comptime self: ResultColumn, comptime index: usize) std.builtin.Type.StructField {
|
|
return .{
|
|
.name = comptimePrint("{}", .{index}),
|
|
.field_type = self.@"type",
|
|
.default_value = null,
|
|
.is_comptime = false,
|
|
.alignment = 0,
|
|
};
|
|
}
|
|
};
|
|
|
|
// Represents a full SQL query
|
|
pub const Query = struct {
|
|
tables: []const QueryTable,
|
|
fields: []const ResultColumn,
|
|
filter: Condition,
|
|
|
|
pub fn from(comptime tables: []const QueryTable) Query {
|
|
return .{
|
|
.tables = tables,
|
|
.fields = &.{},
|
|
.filter = .{ .val = "TRUE" }, // TODO
|
|
};
|
|
}
|
|
|
|
pub fn str(comptime self: Query) String {
|
|
comptime {
|
|
const table_aliases = map(QueryTable, String, self.tables, QueryTable.declarationStr);
|
|
const select_clauses = map(ResultColumn, String, self.fields, ResultColumn.toSelectClause);
|
|
return comptimePrint("SELECT {s} FROM {s} WHERE {s}", .{ join(select_clauses, ", "), join(table_aliases, ", "), self.filter.str() });
|
|
}
|
|
}
|
|
|
|
pub fn rowType(comptime self: *const Query) type {
|
|
const struct_fields = map(ResultColumn, std.builtin.Type.StructField, self.fields, ResultColumn.toStructField);
|
|
|
|
return @Type(.{ .Struct = .{
|
|
.layout = .Auto,
|
|
.fields = struct_fields,
|
|
.decls = &.{},
|
|
.is_tuple = true,
|
|
} });
|
|
}
|
|
|
|
pub fn select(comptime self: Query, comptime fields: []const ResultColumn) Query {
|
|
return .{
|
|
.tables = self.tables,
|
|
.fields = fields,
|
|
.filter = self.filter,
|
|
};
|
|
}
|
|
|
|
pub fn where(comptime self: Query, comptime condition: Condition) Query {
|
|
return .{
|
|
.tables = self.tables,
|
|
.fields = self.fields,
|
|
.filter = condition,
|
|
};
|
|
}
|
|
};
|
|
|
|
test "Query" {
|
|
const C = Condition;
|
|
const MyTable = struct { id: i64 };
|
|
const MyOtherTable = struct {
|
|
val: []const u8,
|
|
};
|
|
const qt = queryTables(&.{ MyTable, MyOtherTable, MyTable });
|
|
const t1 = qt[0];
|
|
const t2 = qt[2];
|
|
const t_other = qt[1];
|
|
|
|
const q = comptime Query
|
|
.from(qt)
|
|
.select(&.{ t1.select(.id), t_other.select(.val) })
|
|
.where(C.all(&.{
|
|
C.eql(t1.field(.id), t2.field(.id)),
|
|
C.eql(t1.field(.id), t2.field(.id)),
|
|
}));
|
|
|
|
try std.testing.expectEqualStrings(
|
|
"SELECT my_table_0.id, my_other_table_1.val " ++
|
|
"FROM my_table AS my_table_0, my_other_table AS my_other_table_1, my_table AS my_table_2 " ++
|
|
"WHERE ((my_table_0.id = my_table_2.id) AND (my_table_0.id = my_table_2.id))",
|
|
comptime q.str(),
|
|
);
|
|
|
|
const fields = std.meta.fields(q.rowType());
|
|
try std.testing.expectEqual(i64, fields[0].field_type);
|
|
try std.testing.expectEqual([]const u8, fields[1].field_type);
|
|
}
|