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:
Madeorsk 2024-11-22 22:36:51 +01:00
parent 5a2964622c
commit 04b61f9787
Signed by: Madeorsk
GPG key ID: 677E51CA765BB79F
6 changed files with 148 additions and 137 deletions

33
src/comptime.zig Normal file
View 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;
}

View file

@ -8,25 +8,71 @@ const _sql = @import("sql.zig");
const _conditions = @import("conditions.zig"); const _conditions = @import("conditions.zig");
const relations = @import("relations.zig"); const relations = @import("relations.zig");
const repository = @import("repository.zig"); const repository = @import("repository.zig");
const _comptime = @import("comptime.zig");
const InlineRelationsResult = struct {
};
/// Repository query configuration structure. /// Repository query configuration structure.
pub const RepositoryQueryConfiguration = struct { pub const RepositoryQueryConfiguration = struct {
select: ?_sql.RawQuery = null, select: ?_sql.RawQuery = null,
join: ?_sql.RawQuery = null, join: ?_sql.RawQuery = null,
where: ?_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. /// Repository models query manager.
/// Manage query string build and its execution. /// Manage query string build and its execution.
pub fn RepositoryQuery(comptime Model: type, comptime TableShape: type, comptime repositoryConfig: repository.RepositoryConfiguration(Model, TableShape)) type { pub fn RepositoryQuery(comptime Model: type, comptime TableShape: type, comptime repositoryConfig: repository.RepositoryConfiguration(Model, TableShape), comptime with: ?[]const relations.ModelRelation) type {
// Pre-compute SQL buffer size. 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 fromClause = " FROM \"" ++ repositoryConfig.table ++ "\"";
const defaultSelectSql = "\"" ++ repositoryConfig.table ++ "\".*"; const defaultSelectSql = "\"" ++ repositoryConfig.table ++ "\".*" ++ compiledRelations.inlineSelect;
const defaultJoin = compiledRelations.inlineJoins;
// Model key type. // Model key type.
const KeyType = repository.ModelKeyType(Model, TableShape, repositoryConfig); 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, connection: *database.Connection = undefined,
queryConfig: RepositoryQueryConfiguration, queryConfig: RepositoryQueryConfiguration,
/// List of loaded inline relations.
inlineRelations: []relations.Eager = undefined,
query: ?_sql.RawQuery = null, query: ?_sql.RawQuery = null,
sql: ?[]const u8 = 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. /// Build SQL query.
pub fn buildSql(self: *Self) !void { 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. // Build the full SQL query from all its parts.
const sqlQuery = _sql.RawQuery{ const sqlQuery = _sql.RawQuery{
.sql = try std.mem.join(self.arena.allocator(), "", &[_][]const u8{ .sql = try std.mem.join(self.arena.allocator(), "", &[_][]const u8{
"SELECT ", if (self.queryConfig.select) |_select| _select.sql else defaultSelectSql, "SELECT ", if (self.queryConfig.select) |_select| _select.sql else defaultSelectSql,
if (inlineRelations) |_| ", " else "",
if (inlineRelations) |_inlineRelations| _inlineRelations.select else "",
fromClause, fromClause,
defaultJoin,
if (self.queryConfig.join) |_| " " else "", if (self.queryConfig.join) |_| " " else "",
if (self.queryConfig.join) |_join| _join.sql 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 " else "",
if (self.queryConfig.where) |_where| _where.sql 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{ .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.select) |_select| _select.params else &[0]_sql.RawQueryParameter{},
if (self.queryConfig.join) |_join| _join.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{}, if (self.queryConfig.where) |_where| _where.params else &[0]_sql.RawQueryParameter{},
}) })
}; };

View file

@ -62,8 +62,7 @@ pub fn typedMany(
}; };
const FromKeyType = std.meta.fields(FromModel)[std.meta.fieldIndex(FromModel, fromRepositoryConfig.key[0]).?].type; const FromKeyType = std.meta.fields(FromModel)[std.meta.fieldIndex(FromModel, fromRepositoryConfig.key[0]).?].type;
const QueryType = _query.RepositoryQuery(ToModel, ToTable, toRepositoryConfig); const QueryType = _query.RepositoryQuery(ToModel, ToTable, toRepositoryConfig, null);
const SelectBuilder = _sql.SelectBuilder(ToTable);
return struct { return struct {
const Self = @This(); const Self = @This();
@ -72,12 +71,12 @@ pub fn typedMany(
return false; 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. unreachable; // No possible join in a many relation.
} }
fn genSelect(_: *anyopaque, allocator: std.mem.Allocator, table: []const u8, prefix: []const u8) ![]const u8 { fn genSelect(_: *anyopaque, comptime table: []const u8, comptime prefix: []const u8) []const u8 {
return SelectBuilder.build(allocator, table, prefix); return _sql.SelectBuild(ToTable, table, prefix);
} }
fn buildQuery(_: *anyopaque, opaqueModels: []const anyopaque, opaqueQuery: *anyopaque) !void { fn buildQuery(_: *anyopaque, opaqueModels: []const anyopaque, opaqueQuery: *anyopaque) !void {
@ -181,8 +180,7 @@ fn typedOne(
comptime config: OneConfiguration) type { comptime config: OneConfiguration) type {
const FromKeyType = std.meta.fields(FromModel)[std.meta.fieldIndex(FromModel, fromRepositoryConfig.key[0]).?].type; const FromKeyType = std.meta.fields(FromModel)[std.meta.fieldIndex(FromModel, fromRepositoryConfig.key[0]).?].type;
const QueryType = _query.RepositoryQuery(ToModel, ToTable, toRepositoryConfig); const QueryType = _query.RepositoryQuery(ToModel, ToTable, toRepositoryConfig, null);
const SelectBuilder = _sql.SelectBuilder(ToTable);
// Get foreign key from relation config or repository config. // Get foreign key from relation config or repository config.
const foreignKey = switch (config) { const foreignKey = switch (config) {
@ -205,32 +203,29 @@ fn typedOne(
return true; 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) { return switch (config) {
.direct => (.{ .direct => (
.sql = try std.fmt.allocPrint(allocator, "LEFT JOIN \"" ++ toRepositoryConfig.table ++ "\" AS \"{s}\" ON " ++ "LEFT JOIN \"" ++ toRepositoryConfig.table ++ "\" AS \"" ++ alias ++ "\" ON " ++
"\"" ++ fromRepositoryConfig.table ++ "\"." ++ foreignKey ++ " = \"{s}\"." ++ modelKey, .{alias, alias}), "\"" ++ fromRepositoryConfig.table ++ "\"." ++ foreignKey ++ " = \"" ++ alias ++ "\"." ++ modelKey
.params = &[0]_sql.RawQueryParameter{}, ),
}),
.reverse => (.{ .reverse => (
.sql = try std.fmt.allocPrint(allocator, "LEFT JOIN \"" ++ toRepositoryConfig.table ++ "\" AS \"{s}\" ON " ++ "LEFT JOIN \"" ++ toRepositoryConfig.table ++ "\" AS \"" ++ alias ++ "\" ON " ++
"\"" ++ fromRepositoryConfig.table ++ "\"." ++ modelKey ++ " = \"{s}\"." ++ foreignKey, .{alias, alias}), "\"" ++ fromRepositoryConfig.table ++ "\"." ++ modelKey ++ " = \"" ++ alias ++ "\"." ++ foreignKey
.params = &[0]_sql.RawQueryParameter{}, ),
}),
.through => |through| (.{ .through => |through| (
.sql = try std.fmt.allocPrint(allocator, "LEFT JOIN \"" ++ through.table ++ "\" AS \"{s}_pivot\" ON " ++ "LEFT JOIN \"" ++ through.table ++ "\" AS \"" ++ alias ++ "_pivot\" ON " ++
"\"" ++ fromRepositoryConfig.table ++ "\"." ++ foreignKey ++ " = " ++ "\"{s}_pivot\"." ++ through.joinForeignKey ++ "\"" ++ fromRepositoryConfig.table ++ "\"." ++ foreignKey ++ " = " ++ "\"" ++ alias ++ "_pivot\"." ++ through.joinForeignKey ++
"LEFT JOIN \"" ++ toRepositoryConfig.table ++ "\" AS \"{s}\" ON " ++ "LEFT JOIN \"" ++ toRepositoryConfig.table ++ "\" AS \"" ++ alias ++ "\" ON " ++
"\"{s}_pivot\"." ++ through.joinModelKey ++ " = " ++ "\"{s}\"." ++ modelKey, .{alias, alias, alias, alias, alias}), "\"" ++ alias ++ "_pivot\"." ++ through.joinModelKey ++ " = " ++ "\"" ++ alias ++ "\"." ++ modelKey
.params = &[0]_sql.RawQueryParameter{}, ),
}),
}; };
} }
fn genSelect(_: *anyopaque, allocator: std.mem.Allocator, table: []const u8, prefix: []const u8) ![]const u8 { fn genSelect(_: *anyopaque, comptime table: []const u8, comptime prefix: []const u8) []const u8 {
return SelectBuilder.build(allocator, table, prefix); return _sql.SelectBuild(ToTable, table, prefix);
} }
fn buildQuery(_: *anyopaque, opaqueModels: []const anyopaque, opaqueQuery: *anyopaque) !void { fn buildQuery(_: *anyopaque, opaqueModels: []const anyopaque, opaqueQuery: *anyopaque) !void {
@ -296,8 +291,8 @@ pub const Relation = struct {
instance: *anyopaque, instance: *anyopaque,
inlineMapping: *const fn (self: *anyopaque) bool, inlineMapping: *const fn (self: *anyopaque) bool,
genJoin: *const fn (self: *anyopaque, allocator: std.mem.Allocator, alias: []const u8) anyerror!_sql.RawQuery, genJoin: *const fn (self: *anyopaque, comptime alias: []const u8) []const u8,
genSelect: *const fn (self: *anyopaque, allocator: std.mem.Allocator, table: []const u8, prefix: []const u8) anyerror![]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, 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. /// 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 { pub fn genJoin(self: Self, comptime alias: []const u8) []const u8 {
return self._interface.genJoin(self._interface.instance, allocator, alias); return self._interface.genJoin(self._interface.instance, alias);
} }
/// Generate a SELECT clause to retrieve the associated data, with the given table and prefix. /// 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 { pub fn genSelect(self: Self, comptime table: []const u8, comptime prefix: []const u8) []const u8 {
return self._interface.genSelect(self._interface.instance, allocator, table, prefix); return self._interface.genSelect(self._interface.instance, table, prefix);
} }
/// Build the query to retrieve relation data. /// Build the query to retrieve relation data.

View file

@ -110,7 +110,7 @@ pub fn Repository(comptime Model: type, comptime TableShape: type, comptime repo
pub const TableType = TableShape; pub const TableType = TableShape;
pub const config = repositoryConfig; 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); pub const Insert: type = insert.RepositoryInsert(Model, TableShape, config, config.insertShape);
/// Type of one model key. /// 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 { pub fn InsertCustom(comptime InsertShape: type) type {
return insert.RepositoryInsert(Model, TableShape, config, InsertShape); return insert.RepositoryInsert(Model, TableShape, config, InsertShape);
} }

View file

@ -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;
}

View file

@ -32,10 +32,11 @@ test "belongsTo" {
}; };
// Build a query of submodels. // Build a query of submodels.
var myQuery = repository.MySubmodelRepository.Query.init(std.testing.allocator, poolConnector.connector(), .{}); var myQuery = repository.MySubmodelRepository.QueryWith(
// Retrieve parents of submodels from relation.
&[_]zrm.relations.ModelRelation{repository.MySubmodelRelations.parent}
).init(std.testing.allocator, poolConnector.connector(), .{});
defer myQuery.deinit(); defer myQuery.deinit();
// Retrieve parents of submodels from relation.
try myQuery.with(repository.MySubmodelRelations.parent);
try myQuery.buildSql(); try myQuery.buildSql();