Set relations to retrieve by a query at compile time.
+ Add compile-time utility functions to build strings and queries. * Relations to retrieve when querying a model must now be set at comptime.
This commit is contained in:
parent
5a2964622c
commit
04b61f9787
6 changed files with 148 additions and 137 deletions
33
src/comptime.zig
Normal file
33
src/comptime.zig
Normal file
|
@ -0,0 +1,33 @@
|
|||
|
||||
/// Append an element to the given array at comptime.
|
||||
pub fn append(array: anytype, element: anytype) @TypeOf(array ++ .{element}) {
|
||||
return array ++ .{element};
|
||||
}
|
||||
|
||||
/// Join strings into one, with the given separator in between.
|
||||
pub fn join(separator: []const u8, slices: []const[]const u8) []const u8 {
|
||||
if (slices.len == 0) return "";
|
||||
|
||||
// Compute total length of the string to make.
|
||||
const totalLen = total: {
|
||||
// Compute separator length.
|
||||
var total = separator.len * (slices.len - 1);
|
||||
// Add length of all slices.
|
||||
for (slices) |slice| total += slice.len;
|
||||
break :total total;
|
||||
};
|
||||
|
||||
var buffer: [totalLen]u8 = undefined;
|
||||
|
||||
// Based on std.mem.joinMaybeZ implementation.
|
||||
@memcpy(buffer[0..slices[0].len], slices[0]);
|
||||
var buf_index: usize = slices[0].len;
|
||||
for (slices[1..]) |slice| {
|
||||
@memcpy(buffer[buf_index .. buf_index + separator.len], separator);
|
||||
buf_index += separator.len;
|
||||
@memcpy(buffer[buf_index .. buf_index + slice.len], slice);
|
||||
buf_index += slice.len;
|
||||
}
|
||||
|
||||
return &buffer;
|
||||
}
|
155
src/query.zig
155
src/query.zig
|
@ -8,25 +8,71 @@ const _sql = @import("sql.zig");
|
|||
const _conditions = @import("conditions.zig");
|
||||
const relations = @import("relations.zig");
|
||||
const repository = @import("repository.zig");
|
||||
|
||||
const InlineRelationsResult = struct {
|
||||
|
||||
};
|
||||
const _comptime = @import("comptime.zig");
|
||||
|
||||
/// Repository query configuration structure.
|
||||
pub const RepositoryQueryConfiguration = struct {
|
||||
select: ?_sql.RawQuery = null,
|
||||
join: ?_sql.RawQuery = null,
|
||||
where: ?_sql.RawQuery = null,
|
||||
with: ?[]const relations.Eager = null,
|
||||
};
|
||||
|
||||
/// Compiled relations structure.
|
||||
const CompiledRelations = struct {
|
||||
inlineSelect: []const u8,
|
||||
inlineJoins: []const u8,
|
||||
};
|
||||
|
||||
/// Repository models query manager.
|
||||
/// Manage query string build and its execution.
|
||||
pub fn RepositoryQuery(comptime Model: type, comptime TableShape: type, comptime repositoryConfig: repository.RepositoryConfiguration(Model, TableShape)) type {
|
||||
// Pre-compute SQL buffer size.
|
||||
pub fn RepositoryQuery(comptime Model: type, comptime TableShape: type, comptime repositoryConfig: repository.RepositoryConfiguration(Model, TableShape), comptime with: ?[]const relations.ModelRelation) type {
|
||||
const compiledRelations = comptime compile: {
|
||||
// Inline relations list.
|
||||
var inlineRelations: []relations.ModelRelation = &[0]relations.ModelRelation{};
|
||||
|
||||
if (with) |_with| {
|
||||
// If there are relations to eager load, prepare their query.
|
||||
|
||||
// Initialize inline select array.
|
||||
var inlineSelect: [][]const u8 = &[0][]const u8{};
|
||||
// Initialize inline joins array.
|
||||
var inlineJoins: [][]const u8 = &[0][]const u8{};
|
||||
|
||||
for (_with) |relation| {
|
||||
// For each relation, determine if it's inline or not.
|
||||
var tt = relation.relation{};
|
||||
const relationInstance = tt.relation();
|
||||
if (relationInstance.inlineMapping()) {
|
||||
// Add the current relation to inline relations.
|
||||
inlineRelations = @ptrCast(@constCast(_comptime.append(inlineRelations, relation)));
|
||||
|
||||
// Build table alias and fields prefix for the relation.
|
||||
const tableAlias = "relations." ++ relation.field;
|
||||
const fieldsPrefix = tableAlias ++ ".";
|
||||
|
||||
// Generate selected columns for the relation.
|
||||
inlineSelect = @ptrCast(@constCast(_comptime.append(inlineSelect, relationInstance.genSelect(tableAlias, fieldsPrefix))));
|
||||
// Generate joined table for the relation.
|
||||
inlineJoins = @ptrCast(@constCast(_comptime.append(inlineJoins, relationInstance.genJoin(tableAlias))));
|
||||
}
|
||||
}
|
||||
|
||||
break :compile CompiledRelations{
|
||||
.inlineSelect = if (inlineSelect.len > 0) ", " ++ _comptime.join(", ", inlineSelect) else "",
|
||||
.inlineJoins = if (inlineJoins.len > 0) " " ++ _comptime.join(" ", inlineJoins) else "",
|
||||
};
|
||||
} else {
|
||||
break :compile CompiledRelations{
|
||||
.inlineSelect = "",
|
||||
.inlineJoins = "",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Pre-compute SQL buffer.
|
||||
const fromClause = " FROM \"" ++ repositoryConfig.table ++ "\"";
|
||||
const defaultSelectSql = "\"" ++ repositoryConfig.table ++ "\".*";
|
||||
const defaultSelectSql = "\"" ++ repositoryConfig.table ++ "\".*" ++ compiledRelations.inlineSelect;
|
||||
const defaultJoin = compiledRelations.inlineJoins;
|
||||
|
||||
// Model key type.
|
||||
const KeyType = repository.ModelKeyType(Model, TableShape, repositoryConfig);
|
||||
|
@ -39,9 +85,6 @@ pub fn RepositoryQuery(comptime Model: type, comptime TableShape: type, comptime
|
|||
connection: *database.Connection = undefined,
|
||||
queryConfig: RepositoryQueryConfiguration,
|
||||
|
||||
/// List of loaded inline relations.
|
||||
inlineRelations: []relations.Eager = undefined,
|
||||
|
||||
query: ?_sql.RawQuery = null,
|
||||
sql: ?[]const u8 = null,
|
||||
|
||||
|
@ -177,103 +220,16 @@ pub fn RepositoryQuery(comptime Model: type, comptime TableShape: type, comptime
|
|||
}
|
||||
}
|
||||
|
||||
/// Set relations to eager load.
|
||||
pub fn with(self: *Self, relation: relations.ModelRelation) !void {
|
||||
// Take an array of eager relations (which can have subrelations).
|
||||
const allocator = self.arena.allocator();
|
||||
|
||||
// Make a relation instance.
|
||||
const relationInstance = try allocator.create(relation.relation);
|
||||
|
||||
// Add the new relation to a newly allocated array, with one more space.
|
||||
const newPos = if (self.queryConfig.with) |_with| _with.len else 0;
|
||||
var newWith = try allocator.alloc(relations.Eager, newPos + 1);
|
||||
newWith[newPos] = .{
|
||||
.field = relation.field,
|
||||
.relation = relationInstance.*.relation(),
|
||||
.with = &[0]relations.Eager{}, //TODO handle subrelations with dotted syntax
|
||||
};
|
||||
|
||||
if (self.queryConfig.with) |_with| {
|
||||
// Copy existing relations.
|
||||
@memcpy(newWith[0..newPos], _with);
|
||||
// Free previous array.
|
||||
allocator.free(_with);
|
||||
}
|
||||
|
||||
// Save the newly allocated array.
|
||||
self.queryConfig.with = newWith;
|
||||
}
|
||||
|
||||
/// Build inline relations query part.
|
||||
fn buildInlineRelations(self: *Self) !?struct{
|
||||
select: []const u8,
|
||||
join: _sql.RawQuery,
|
||||
} {
|
||||
if (self.queryConfig.with) |_with| {
|
||||
// Initialize an ArrayList of query parts for relations.
|
||||
var inlineRelations = try std.ArrayList(_sql.RawQuery).initCapacity(self.arena.allocator(), _with.len);
|
||||
defer inlineRelations.deinit();
|
||||
var inlineRelationsSelect = try std.ArrayList([]const u8).initCapacity(self.arena.allocator(), _with.len);
|
||||
defer inlineRelationsSelect.deinit();
|
||||
|
||||
// Initialize an ArrayList to store all loaded inline relations.
|
||||
var loadedRelations = std.ArrayList(relations.Eager).init(self.arena.allocator());
|
||||
defer loadedRelations.deinit();
|
||||
|
||||
for (_with) |_relation| {
|
||||
// Append each inline relation to the ArrayList.
|
||||
if (_relation.relation.inlineMapping()) {
|
||||
try loadedRelations.append(_relation); // Store the loaded inline relation.
|
||||
|
||||
// Get an allocator for local allocations.
|
||||
const localAllocator = self.arena.allocator();
|
||||
|
||||
// Build table alias and fields prefix.
|
||||
const tableAlias = try std.fmt.allocPrint(localAllocator, "relations.{s}", .{_relation.field});
|
||||
defer localAllocator.free(tableAlias);
|
||||
const prefix = try std.fmt.allocPrint(localAllocator, "{s}.", .{tableAlias});
|
||||
defer localAllocator.free(prefix);
|
||||
|
||||
// Alter query to get relation fields.
|
||||
try inlineRelations.append(try _relation.relation.genJoin(self.arena.allocator(), tableAlias));
|
||||
const relationSelect = try _relation.relation.genSelect(localAllocator, tableAlias, prefix);
|
||||
try inlineRelationsSelect.append(relationSelect);
|
||||
}
|
||||
}
|
||||
|
||||
self.inlineRelations = try loadedRelations.toOwnedSlice();
|
||||
|
||||
// Return the inline relations query part.
|
||||
return .{
|
||||
.select = try std.mem.join(self.arena.allocator(), ", ", inlineRelationsSelect.items),
|
||||
.join = try _sql.RawQuery.fromConcat(self.arena.allocator(), inlineRelations.items),
|
||||
};
|
||||
} else {
|
||||
// Nothing.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Build SQL query.
|
||||
pub fn buildSql(self: *Self) !void {
|
||||
// Build inline relations query part.
|
||||
const inlineRelations = try self.buildInlineRelations();
|
||||
defer if (inlineRelations) |_inlineRelations| self.arena.allocator().free(_inlineRelations.join.sql);
|
||||
defer if (inlineRelations) |_inlineRelations| self.arena.allocator().free(_inlineRelations.join.params);
|
||||
defer if (inlineRelations) |_inlineRelations| self.arena.allocator().free(_inlineRelations.select);
|
||||
|
||||
// Build the full SQL query from all its parts.
|
||||
const sqlQuery = _sql.RawQuery{
|
||||
.sql = try std.mem.join(self.arena.allocator(), "", &[_][]const u8{
|
||||
"SELECT ", if (self.queryConfig.select) |_select| _select.sql else defaultSelectSql,
|
||||
if (inlineRelations) |_| ", " else "",
|
||||
if (inlineRelations) |_inlineRelations| _inlineRelations.select else "",
|
||||
fromClause,
|
||||
defaultJoin,
|
||||
if (self.queryConfig.join) |_| " " else "",
|
||||
if (self.queryConfig.join) |_join| _join.sql else "",
|
||||
if (inlineRelations) |_| " " else "",
|
||||
if (inlineRelations) |_inlineRelations| _inlineRelations.join.sql else "",
|
||||
if (self.queryConfig.where) |_| " WHERE " else "",
|
||||
if (self.queryConfig.where) |_where| _where.sql else "",
|
||||
";",
|
||||
|
@ -281,7 +237,6 @@ pub fn RepositoryQuery(comptime Model: type, comptime TableShape: type, comptime
|
|||
.params = try std.mem.concat(self.arena.allocator(), _sql.RawQueryParameter, &[_][]const _sql.RawQueryParameter{
|
||||
if (self.queryConfig.select) |_select| _select.params else &[0]_sql.RawQueryParameter{},
|
||||
if (self.queryConfig.join) |_join| _join.params else &[0]_sql.RawQueryParameter{},
|
||||
if (inlineRelations) |_inlineRelations| _inlineRelations.join.params else &[0]_sql.RawQueryParameter{},
|
||||
if (self.queryConfig.where) |_where| _where.params else &[0]_sql.RawQueryParameter{},
|
||||
})
|
||||
};
|
||||
|
|
|
@ -62,8 +62,7 @@ pub fn typedMany(
|
|||
};
|
||||
|
||||
const FromKeyType = std.meta.fields(FromModel)[std.meta.fieldIndex(FromModel, fromRepositoryConfig.key[0]).?].type;
|
||||
const QueryType = _query.RepositoryQuery(ToModel, ToTable, toRepositoryConfig);
|
||||
const SelectBuilder = _sql.SelectBuilder(ToTable);
|
||||
const QueryType = _query.RepositoryQuery(ToModel, ToTable, toRepositoryConfig, null);
|
||||
|
||||
return struct {
|
||||
const Self = @This();
|
||||
|
@ -72,12 +71,12 @@ pub fn typedMany(
|
|||
return false;
|
||||
}
|
||||
|
||||
fn genJoin(_: *anyopaque, _: std.mem.Allocator, _: []const u8) !_sql.RawQuery {
|
||||
fn genJoin(_: *anyopaque, comptime _: []const u8) []const u8 {
|
||||
unreachable; // No possible join in a many relation.
|
||||
}
|
||||
|
||||
fn genSelect(_: *anyopaque, allocator: std.mem.Allocator, table: []const u8, prefix: []const u8) ![]const u8 {
|
||||
return SelectBuilder.build(allocator, table, prefix);
|
||||
fn genSelect(_: *anyopaque, comptime table: []const u8, comptime prefix: []const u8) []const u8 {
|
||||
return _sql.SelectBuild(ToTable, table, prefix);
|
||||
}
|
||||
|
||||
fn buildQuery(_: *anyopaque, opaqueModels: []const anyopaque, opaqueQuery: *anyopaque) !void {
|
||||
|
@ -181,8 +180,7 @@ fn typedOne(
|
|||
comptime config: OneConfiguration) type {
|
||||
|
||||
const FromKeyType = std.meta.fields(FromModel)[std.meta.fieldIndex(FromModel, fromRepositoryConfig.key[0]).?].type;
|
||||
const QueryType = _query.RepositoryQuery(ToModel, ToTable, toRepositoryConfig);
|
||||
const SelectBuilder = _sql.SelectBuilder(ToTable);
|
||||
const QueryType = _query.RepositoryQuery(ToModel, ToTable, toRepositoryConfig, null);
|
||||
|
||||
// Get foreign key from relation config or repository config.
|
||||
const foreignKey = switch (config) {
|
||||
|
@ -205,32 +203,29 @@ fn typedOne(
|
|||
return true;
|
||||
}
|
||||
|
||||
fn genJoin(_: *anyopaque, allocator: std.mem.Allocator, alias: []const u8) !_sql.RawQuery {
|
||||
fn genJoin(_: *anyopaque, comptime alias: []const u8) []const u8 {
|
||||
return switch (config) {
|
||||
.direct => (.{
|
||||
.sql = try std.fmt.allocPrint(allocator, "LEFT JOIN \"" ++ toRepositoryConfig.table ++ "\" AS \"{s}\" ON " ++
|
||||
"\"" ++ fromRepositoryConfig.table ++ "\"." ++ foreignKey ++ " = \"{s}\"." ++ modelKey, .{alias, alias}),
|
||||
.params = &[0]_sql.RawQueryParameter{},
|
||||
}),
|
||||
.direct => (
|
||||
"LEFT JOIN \"" ++ toRepositoryConfig.table ++ "\" AS \"" ++ alias ++ "\" ON " ++
|
||||
"\"" ++ fromRepositoryConfig.table ++ "\"." ++ foreignKey ++ " = \"" ++ alias ++ "\"." ++ modelKey
|
||||
),
|
||||
|
||||
.reverse => (.{
|
||||
.sql = try std.fmt.allocPrint(allocator, "LEFT JOIN \"" ++ toRepositoryConfig.table ++ "\" AS \"{s}\" ON " ++
|
||||
"\"" ++ fromRepositoryConfig.table ++ "\"." ++ modelKey ++ " = \"{s}\"." ++ foreignKey, .{alias, alias}),
|
||||
.params = &[0]_sql.RawQueryParameter{},
|
||||
}),
|
||||
.reverse => (
|
||||
"LEFT JOIN \"" ++ toRepositoryConfig.table ++ "\" AS \"" ++ alias ++ "\" ON " ++
|
||||
"\"" ++ fromRepositoryConfig.table ++ "\"." ++ modelKey ++ " = \"" ++ alias ++ "\"." ++ foreignKey
|
||||
),
|
||||
|
||||
.through => |through| (.{
|
||||
.sql = try std.fmt.allocPrint(allocator, "LEFT JOIN \"" ++ through.table ++ "\" AS \"{s}_pivot\" ON " ++
|
||||
"\"" ++ fromRepositoryConfig.table ++ "\"." ++ foreignKey ++ " = " ++ "\"{s}_pivot\"." ++ through.joinForeignKey ++
|
||||
"LEFT JOIN \"" ++ toRepositoryConfig.table ++ "\" AS \"{s}\" ON " ++
|
||||
"\"{s}_pivot\"." ++ through.joinModelKey ++ " = " ++ "\"{s}\"." ++ modelKey, .{alias, alias, alias, alias, alias}),
|
||||
.params = &[0]_sql.RawQueryParameter{},
|
||||
}),
|
||||
.through => |through| (
|
||||
"LEFT JOIN \"" ++ through.table ++ "\" AS \"" ++ alias ++ "_pivot\" ON " ++
|
||||
"\"" ++ fromRepositoryConfig.table ++ "\"." ++ foreignKey ++ " = " ++ "\"" ++ alias ++ "_pivot\"." ++ through.joinForeignKey ++
|
||||
"LEFT JOIN \"" ++ toRepositoryConfig.table ++ "\" AS \"" ++ alias ++ "\" ON " ++
|
||||
"\"" ++ alias ++ "_pivot\"." ++ through.joinModelKey ++ " = " ++ "\"" ++ alias ++ "\"." ++ modelKey
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
fn genSelect(_: *anyopaque, allocator: std.mem.Allocator, table: []const u8, prefix: []const u8) ![]const u8 {
|
||||
return SelectBuilder.build(allocator, table, prefix);
|
||||
fn genSelect(_: *anyopaque, comptime table: []const u8, comptime prefix: []const u8) []const u8 {
|
||||
return _sql.SelectBuild(ToTable, table, prefix);
|
||||
}
|
||||
|
||||
fn buildQuery(_: *anyopaque, opaqueModels: []const anyopaque, opaqueQuery: *anyopaque) !void {
|
||||
|
@ -296,8 +291,8 @@ pub const Relation = struct {
|
|||
instance: *anyopaque,
|
||||
|
||||
inlineMapping: *const fn (self: *anyopaque) bool,
|
||||
genJoin: *const fn (self: *anyopaque, allocator: std.mem.Allocator, alias: []const u8) anyerror!_sql.RawQuery,
|
||||
genSelect: *const fn (self: *anyopaque, allocator: std.mem.Allocator, table: []const u8, prefix: []const u8) anyerror![]const u8,
|
||||
genJoin: *const fn (self: *anyopaque, comptime alias: []const u8) []const u8,
|
||||
genSelect: *const fn (self: *anyopaque, comptime table: []const u8, comptime prefix: []const u8) []const u8,
|
||||
buildQuery: *const fn (self: *anyopaque, models: []const anyopaque, query: *anyopaque) anyerror!void,
|
||||
},
|
||||
|
||||
|
@ -308,13 +303,13 @@ pub const Relation = struct {
|
|||
}
|
||||
|
||||
/// In case of inline mapping, generate a JOIN clause to retrieve the associated data.
|
||||
pub fn genJoin(self: Self, allocator: std.mem.Allocator, alias: []const u8) !_sql.RawQuery {
|
||||
return self._interface.genJoin(self._interface.instance, allocator, alias);
|
||||
pub fn genJoin(self: Self, comptime alias: []const u8) []const u8 {
|
||||
return self._interface.genJoin(self._interface.instance, alias);
|
||||
}
|
||||
|
||||
/// Generate a SELECT clause to retrieve the associated data, with the given table and prefix.
|
||||
pub fn genSelect(self: Self, allocator: std.mem.Allocator, table: []const u8, prefix: []const u8) ![]const u8 {
|
||||
return self._interface.genSelect(self._interface.instance, allocator, table, prefix);
|
||||
pub fn genSelect(self: Self, comptime table: []const u8, comptime prefix: []const u8) []const u8 {
|
||||
return self._interface.genSelect(self._interface.instance, table, prefix);
|
||||
}
|
||||
|
||||
/// Build the query to retrieve relation data.
|
||||
|
|
|
@ -110,7 +110,7 @@ pub fn Repository(comptime Model: type, comptime TableShape: type, comptime repo
|
|||
pub const TableType = TableShape;
|
||||
pub const config = repositoryConfig;
|
||||
|
||||
pub const Query: type = query.RepositoryQuery(Model, TableShape, config);
|
||||
pub const Query: type = query.RepositoryQuery(Model, TableShape, config, null);
|
||||
pub const Insert: type = insert.RepositoryInsert(Model, TableShape, config, config.insertShape);
|
||||
|
||||
/// Type of one model key.
|
||||
|
@ -152,6 +152,10 @@ pub fn Repository(comptime Model: type, comptime TableShape: type, comptime repo
|
|||
}
|
||||
};
|
||||
|
||||
pub fn QueryWith(comptime with: []const _relations.ModelRelation) type {
|
||||
return query.RepositoryQuery(Model, TableShape, config, with);
|
||||
}
|
||||
|
||||
pub fn InsertCustom(comptime InsertShape: type) type {
|
||||
return insert.RepositoryInsert(Model, TableShape, config, InsertShape);
|
||||
}
|
||||
|
|
23
src/sql.zig
23
src/sql.zig
|
@ -213,6 +213,29 @@ pub fn SelectBuilder(comptime TableShape: type) type {
|
|||
};
|
||||
}
|
||||
|
||||
/// Build a SELECT query part for a given table, renaming columns with the given prefix, at comptime.
|
||||
pub fn SelectBuild(comptime TableShape: type, comptime table: []const u8, comptime prefix: []const u8) []const u8 {
|
||||
// Initialize the selected columns string.
|
||||
var columnsSelect: []const u8 = "";
|
||||
|
||||
var first = true;
|
||||
// For each field, generate a format string.
|
||||
for (@typeInfo(TableShape).Struct.fields) |field| {
|
||||
// Add ", " between all selected columns (just not the first one).
|
||||
if (first) {
|
||||
first = false;
|
||||
} else {
|
||||
columnsSelect = @ptrCast(columnsSelect ++ ", ");
|
||||
}
|
||||
|
||||
// Select the current field column.
|
||||
columnsSelect = @ptrCast(columnsSelect ++ "\"" ++ table ++ "\".\"" ++ field.name ++ "\" AS \"" ++ prefix ++ field.name ++ "\"");
|
||||
}
|
||||
|
||||
// Return built columns selection.
|
||||
return columnsSelect;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -32,10 +32,11 @@ test "belongsTo" {
|
|||
};
|
||||
|
||||
// Build a query of submodels.
|
||||
var myQuery = repository.MySubmodelRepository.Query.init(std.testing.allocator, poolConnector.connector(), .{});
|
||||
defer myQuery.deinit();
|
||||
var myQuery = repository.MySubmodelRepository.QueryWith(
|
||||
// Retrieve parents of submodels from relation.
|
||||
try myQuery.with(repository.MySubmodelRelations.parent);
|
||||
&[_]zrm.relations.ModelRelation{repository.MySubmodelRelations.parent}
|
||||
).init(std.testing.allocator, poolConnector.connector(), .{});
|
||||
defer myQuery.deinit();
|
||||
|
||||
try myQuery.buildSql();
|
||||
|
||||
|
|
Loading…
Reference in a new issue