diff --git a/src/comptime.zig b/src/comptime.zig new file mode 100644 index 0000000..0bd152a --- /dev/null +++ b/src/comptime.zig @@ -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; +} diff --git a/src/query.zig b/src/query.zig index 2863c60..408d6a9 100644 --- a/src/query.zig +++ b/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{}, }) }; diff --git a/src/relations.zig b/src/relations.zig index 521496d..b59d259 100644 --- a/src/relations.zig +++ b/src/relations.zig @@ -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. diff --git a/src/repository.zig b/src/repository.zig index feb52c4..8f91410 100644 --- a/src/repository.zig +++ b/src/repository.zig @@ -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); } diff --git a/src/sql.zig b/src/sql.zig index 3d50bae..94cc2ac 100644 --- a/src/sql.zig +++ b/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; +} + diff --git a/tests/relations.zig b/tests/relations.zig index 5d40707..375908e 100644 --- a/tests/relations.zig +++ b/tests/relations.zig @@ -32,10 +32,11 @@ test "belongsTo" { }; // 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(); - // Retrieve parents of submodels from relation. - try myQuery.with(repository.MySubmodelRelations.parent); try myQuery.buildSql();